91 Commits
Author SHA1 Message Date
vladp 5700adafb2 NCBACK-33
Test Workflow / test (push) Successful in 3s
2026-01-27 08:09:54 +07:00
vladp 816a984891 Merge branch 'dev' into NCBACK-33
# Conflicts:
#	src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java
#	src/main/java/ru/soune/nocopy/entity/user/User.java
#	src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java
#	src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
2026-01-27 05:46:50 +07:00
vladp 21e31c7d0f dev add google search by image
Test Workflow / test (push) Successful in 3s
2026-01-27 05:41:47 +07:00
vladp e7c6df12d6 dev update file ext
Test Workflow / test (push) Successful in 3s
2026-01-26 22:33:49 +07:00
vladp aa87ae32bc dev update search methods,update find endpoints
Test Workflow / test (push) Successful in 3s
2026-01-26 21:03:45 +07:00
vladp 9ba3d8b973 dev fix pipline
Test Workflow / test (push) Successful in 4s
2026-01-26 21:00:22 +07:00
vladp 300383e523 dev
Test Workflow / test (push) Successful in 2s
2026-01-26 14:48:41 +07:00
vladp 329000d284 dev
Test Workflow / test (push) Successful in 3s
2026-01-26 14:43:57 +07:00
vladp 4c8c4bb5c1 dev
Test Workflow / test (push) Successful in 3s
2026-01-26 14:28:47 +07:00
vladp 82eedb02d1 dev
Test Workflow / test (push) Successful in 3s
2026-01-26 14:20:00 +07:00
vladp b2993e00a1 dev
Test Workflow / test (push) Successful in 2s
2026-01-26 14:14:56 +07:00
vladp 6197b26a81 dev
Test Workflow / test (push) Successful in 3s
2026-01-26 14:08:44 +07:00
vladp 0e2ef1d2e6 dev add indexes
Test Workflow / test (push) Successful in 3s
2026-01-26 14:02:10 +07:00
vladp 3e5eef3b42 dev add indexes
Test Workflow / test (push) Successful in 5s
2026-01-26 11:41:21 +07:00
vladp 89f83054ef NCBACK-25
Test Workflow / test (push) Successful in 3s
2026-01-25 22:05:19 +07:00
vladp f8234291b1 NCBACK-25 delete endpoint
Test Workflow / test (push) Successful in 3s
2026-01-25 21:38:56 +07:00
vladp 4cf36d7b17 NCBACK-25 change limits
Test Workflow / test (push) Successful in 3s
2026-01-25 21:10:17 +07:00
vladp 6dacbde522 NCBACK-25 fix check duplicate exception
Test Workflow / test (push) Successful in 3s
2026-01-25 20:45:29 +07:00
vladp 0a4ab562a4 NCBACK-25 fix check duplicate exception
Test Workflow / test (push) Successful in 3s
2026-01-25 20:08:01 +07:00
vladp 76103b9d96 NCBACK-25 fix check duplicate exception
Test Workflow / test (push) Successful in 3s
2026-01-25 19:59:16 +07:00
vladp 69a79b7ac7 NCBACK-25 merge to dev
Test Workflow / test (push) Successful in 4s
2026-01-25 18:59:55 +07:00
vladp c97f205a17 Merge branch 'NCBACK-25' into dev
# Conflicts:
#	src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java
#	src/main/java/ru/soune/nocopy/service/FileSimilarityService.java
2026-01-25 18:41:05 +07:00
vladp ab3eef1668 NCBACK-25 change path to file
Test Workflow / test (push) Successful in 3s
2026-01-25 12:10:56 +07:00
vladp 85449d73fd fix
Test Workflow / test (push) Successful in 5s
2026-01-24 12:23:05 +07:00
vladp 859239e77a fix
Test Workflow / test (push) Successful in 7s
2026-01-24 12:22:15 +07:00
vladp 63c3794aa4 fix
Test Workflow / test (push) Successful in 4s
2026-01-24 11:34:30 +07:00
vladp 349edbeb91 fix
Test Workflow / test (push) Successful in 5s
2026-01-24 11:32:02 +07:00
vladp e26e50454a Merge branch 'dev' into NCBACK-33
Test Workflow / test (push) Successful in 5s
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/dto/MessageCode.java
#	src/main/resources/application.yaml
2026-01-24 11:23:06 +07:00
vladp 8d026a485f dev
Test Workflow / test (push) Successful in 5s
2026-01-24 11:09:49 +07:00
vladp 204e13ca85 Merge branch 'NCBACK-34' into dev
Test Workflow / test (push) Successful in 4s
2026-01-24 11:07:20 +07:00
vladp 79b13731a6 NCBACK-34
Test Workflow / test (push) Successful in 4s
2026-01-24 10:59:02 +07:00
vladp 8555002415 NCBACK-33 fix check null user
Test Workflow / test (push) Successful in 1s
2026-01-24 00:45:57 +07:00
vladp 4560925183 NCBACK-25
Test Workflow / test (push) Successful in 3s
2026-01-23 16:09:51 +07:00
vladp 4314032a3b Merge remote-tracking branch 'origin/dev' into dev
Test Workflow / test (push) Successful in 3s
2026-01-23 14:58:09 +07:00
vladp d638819ee9 NCBACK-25
Test Workflow / test (push) Successful in 2s
2026-01-23 13:25:56 +07:00
vladp 51654fd060 NCBACK-25 add protection for audio and use hash method from library
Test Workflow / test (push) Successful in 2s
2026-01-23 13:22:02 +07:00
vladp e37cc06cd0 NCBACK-25 add protection for audio and use hash method from library
Test Workflow / test (push) Successful in 3s
2026-01-22 20:53:25 +07:00
vladp 5a3e26b6a3 NCBACK-25 add protection for audio and use hash method from library
Test Workflow / test (push) Successful in 4s
2026-01-22 20:52:45 +07:00
vladp 409c677ba6 NCBACK-25 add protection for audio and use hash method from library
Test Workflow / test (push) Successful in 3s
2026-01-22 19:55:09 +07:00
vladp 95deda12f3 Merge branch 'refs/heads/dev' into NCBACK-25
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/entity/file/FileEntity.java
2026-01-22 14:15:09 +07:00
milidev c2a780db2d referral module init commit
Test Workflow / test (push) Successful in 8s
2026-01-22 13:00:21 +07:00
vladp ad9cbd96f6 NCBACK-34 update dev
Test Workflow / test (push) Successful in 4s
2026-01-22 11:24:37 +07:00
vladp 211b99b986 Merge branch 'dev' into NCBACK-34
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
2026-01-22 11:23:58 +07:00
vladp d1b5b6eb5c add check user
Test Workflow / test (push) Successful in 3s
2026-01-21 12:34:40 +07:00
vladp 251bb37105 fix authService
Test Workflow / test (push) Successful in 4s
2026-01-21 12:21:14 +07:00
vladp 1fc065a46b Сохранение 2026-01-21 12:02:38 +07:00
vladp 60678ef889 Dev add support id
Test Workflow / test (push) Successful in 4s
2026-01-20 21:55:42 +07:00
backdev f0a87648cf Merge pull request 'NCBACK-27' (#12) from NCBACK-27 into dev
Test Workflow / test (push) Successful in 3s
Reviewed-on: #12
2026-01-20 16:20:38 +08:00
vladp c2ffcdcd6e Merge branch 'dev' into NCBACK-27
Test Workflow / test (push) Successful in 3s
# Conflicts:
#	src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java
2026-01-20 15:20:10 +07:00
vladp f9accd7af1 NCBACK-33 add verification logic
Test Workflow / test (push) Successful in 3s
2026-01-20 15:12:24 +07:00
vladp 55fa8b84e2 NCBACK-33 add email send
Test Workflow / test (push) Successful in 4s
2026-01-20 03:47:51 +07:00
vladp 4a8e561258 NCBACK-33 add email send
Test Workflow / test (push) Successful in 2s
2026-01-19 19:26:59 +07:00
vladp 0c0fadf5ec NCBACK-34 fix send exception
Test Workflow / test (push) Successful in 3s
2026-01-19 15:49:27 +07:00
vladp e979712b7c Merge branch 'dev' into NCBACK-34 2026-01-17 01:31:21 +07:00
vladp 5027920199 dev info for docker user
Test Workflow / test (push) Has been cancelled
2026-01-16 23:59:07 +07:00
vladp 4704f780e5 NCBACK-33 add start logic for verify by token
Test Workflow / test (push) Successful in 4s
2026-01-16 20:20:14 +07:00
vladp 3d679440f6 NCBACK-34 add logic for clear does not exist files,add dto for similar file search controller
Test Workflow / test (push) Successful in 4s
2026-01-16 14:30:45 +07:00
vladp 4a45f40e9f NCBACK-34 need fix delete filentity and hash
Test Workflow / test (push) Successful in 4s
2026-01-15 20:41:35 +07:00
vladp bbb0d7b17a Merge branch 'dev' into NCBACK-34 2026-01-15 16:55:47 +07:00
vladp 0ed3f958ff DEV_DB change db creds
Test Workflow / test (push) Successful in 7s
2026-01-15 16:52:33 +07:00
vladp de810e32b4 DEV_DB change db creds
Test Workflow / test (push) Successful in 4s
2026-01-15 16:42:37 +07:00
vladp 1a6823f7a9 DEV_DB change db creds
Test Workflow / test (push) Successful in 4s
2026-01-15 16:17:03 +07:00
vladp 82a57a2983 NCBACK-32 add all case for json
Test Workflow / test (push) Successful in 4s
2026-01-15 11:09:05 +07:00
vladp d3e50aa8d1 NCBACK-19 add all case for json
Test Workflow / test (push) Successful in 3s
2026-01-14 19:10:22 +07:00
vladp 5c6309e228 NCBACK-33 fix check company name
Test Workflow / test (push) Successful in 3s
2026-01-14 18:47:34 +07:00
vladp 8ac73ff045 NCBACK-34 add logic to find similar image in db
Test Workflow / test (push) Successful in 3s
2026-01-13 22:40:52 +07:00
vladp 20e1ab321d NCBACK-25 work version for protect file
Test Workflow / test (push) Successful in 3s
2026-01-13 13:28:09 +07:00
vladp 2b7a8b97e1 NCBACK-27 fix token life
Test Workflow / test (push) Successful in 3s
2026-01-10 02:09:39 +07:00
vladp 344ec075d4 Dev
Test Workflow / test (push) Successful in 2s
2026-01-10 01:19:30 +07:00
vladp 7b8546fd4d Dev
Test Workflow / test (push) Successful in 2s
2026-01-10 01:06:32 +07:00
vladp eb12e219ec Dev
Test Workflow / test (push) Successful in 2s
2026-01-10 00:54:49 +07:00
vladp 920cde3cdf Dev 2026-01-10 00:51:03 +07:00
backdev 5b4dca23fb Merge pull request 'NCBACK-3' (#14) from NCBACK-3 into dev
Test Workflow / test (push) Successful in 3s
Reviewed-on: #14
2026-01-10 01:43:36 +08:00
vladp 7b1ed5fe67 NCBACK-3
Test Workflow / test (push) Successful in 3s
2026-01-10 00:35:29 +07:00
backdev 8102d72982 Merge pull request 'NCBACK-3 Add found by yandex image' (#10) from NCBACK-3 into dev
Test Workflow / test (push) Successful in 10s
Reviewed-on: #10
2026-01-10 01:03:33 +08:00
vladp b3a3d57ee3 NCBACK-3
Test Workflow / test (push) Successful in 3s
2026-01-09 23:41:19 +07:00
vladp d896f23872 add user_files_info for avaibale actions
Test Workflow / test (push) Successful in 2s
2026-01-09 23:05:37 +07:00
vladp 3e06040123 NCBACK-27 fix build
Test Workflow / test (push) Successful in 3s
2026-01-09 22:55:06 +07:00
vladp 18d35cb0fb Merge branch 'dev' into NCBACK-27
Test Workflow / test (push) Successful in 2s
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java
2026-01-09 12:41:32 +07:00
vladp edd508e5d8 Merge branch 'dev' into NCBACK-3
Test Workflow / test (push) Successful in 5s
# Conflicts:
#	docker-compose.yaml
#	src/main/java/ru/soune/nocopy/dto/MessageCode.java
2026-01-09 12:34:24 +07:00
vladp 63baaf7c80 update types
Test Workflow / test (push) Successful in 4s
2026-01-08 12:59:30 +07:00
vladp f136ac1f0a add methods and lib for protection files
Test Workflow / test (push) Successful in 4s
2026-01-08 03:22:32 +07:00
vladp 3a9a5b48a2 update types
Test Workflow / test (push) Successful in 4s
2026-01-07 14:10:35 +07:00
vladp 6fff5884d2 set chunk
Test Workflow / test (push) Successful in 3s
2026-01-07 13:16:53 +07:00
vladp f8673dd581 set chunk
Test Workflow / test (push) Successful in 3s
2026-01-07 12:50:40 +07:00
vladp b9a401f113 correct file_extensions
Test Workflow / test (push) Successful in 8s
2026-01-07 11:56:03 +07:00
vladp 918b4894fc add endpoint with files user info
Test Workflow / test (push) Successful in 6s
2026-01-06 17:27:38 +07:00
vladp f1222ede57 add token life by hour
Test Workflow / test (push) Successful in 3s
2026-01-05 16:31:40 +07:00
vladp 0da0404488 add token life by hour
Test Workflow / test (push) Successful in 4s
2026-01-05 16:26:46 +07:00
vladp ffb147d716 NCBACK-3 Add found by yandex image
Test Workflow / test (push) Successful in 4s
2025-12-22 14:42:27 +07:00
vladp 647fd6e1a7 NCBACK-3 Add found by yandex image
Test Workflow / test (push) Successful in 3s
2025-12-22 14:07:44 +07:00
116 changed files with 3350 additions and 1385 deletions
+11 -2
View File
@@ -1,10 +1,19 @@
# Postgres
POSTGRES_DB=no_copy_
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_USER=ncp_db
POSTGRES_PASSWORD=ncpDbApp
POSTGRES_PORT=5432
POSTGRES_HOST=postgres
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# Backend server
SERVER_PORT=8080
# Postfix
MAIL_HOST_PROD=postfix-production
MAIL_PORT_PROD=25
MAIL_USERNAME_PROD=noreply@nocopy.com
SMTP_PASSWORD_PROD=nocopy!nocopy!
+1 -1
View File
@@ -16,4 +16,4 @@ RUN mkdir -p /data/uploads && chmod 755 /data/uploads
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
CMD ["java", "-jar", "app.jar"]
+78
View File
@@ -0,0 +1,78 @@
Создаем сеть:
docker network create app-network
Войти в БД:
docker exec -it postgres psql -U ncp_db -d no_copy_
Выполнить команду:
\du
Должен быть один пользователь:
List of roles
Role name | Attributes
-----------+------------------------------------------------------------
ncp_db | Superuser, Create role, Create DB, Replication, Bypass RLS
суперпользователь БД:
postgres/postgres
пользователь бд для бэка
ncp_db / ncpDbApp
Подключение к БД для приложения,если volume не существует :
Создаться автоматически.
Если уже существует volume :
docker exec -it postgres psql -U postgres -d postgres
CREATE USER ncp_db WITH PASSWORD 'ncpDbApp';
GRANT ALL PRIVILEGES ON DATABASE no_copy_ TO ncp_db;
\du
\q
-------
psql -U postgres -d no_copy_
-- база
GRANT CONNECT ON DATABASE no_copy_ TO ncp_db;
-- схема
GRANT USAGE ON SCHEMA public TO ncp_db;
-- существующие таблицы
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ncp_db;
-- существующие sequence (очень важно для id)
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ncp_db;
-- будущие таблицы
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO ncp_db;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO ncp_db;
Зайти в контейнер:
docker exec -it {name} bash
------
Создать для сущности FileEntity sequence для икремента индетификатора
CREATE SEQUENCE IF NOT EXISTS file_support_id_seq START 1;
ALTER TABLE file_entities
ALTER COLUMN support_id
SET DEFAULT nextval('file_support_id_seq');
+16
View File
@@ -21,6 +21,9 @@ configurations {
repositories {
mavenCentral()
flatDir {
dirs 'libs'
}
}
dependencies {
@@ -30,6 +33,16 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto:6.5.3'
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'commons-validator:commons-validator:1.7'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '4.0.1'
implementation group: 'com.google.cloud', name: 'google-cloud-vision', version: '3.55.0'
implementation group: 'com.google.api-client', name: 'google-api-client', version: '2.7.2'
implementation 'org.flywaydb:flyway-core:9.22.0'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0'
implementation 'tools.jackson.core:jackson-core:3.0.3'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
annotationProcessor 'org.projectlombok:lombok'
@@ -45,6 +58,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.3.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation name: 'testlib-fat-0.2.1-all'
}
tasks.named('test') {
+57 -12
View File
@@ -17,8 +17,31 @@ services:
command: tail -f /dev/null
restart: unless-stopped
# postfix:
# image: boky/postfix:latest
# container_name: postfix-production
# hostname: ${DOMAIN:-no-copy.ru}
# environment:
# - DOMAIN=${DOMAIN:-no-copy.ru}
# - ALLOWED_SENDER_DOMAINS=*
# - RELAYHOST=
# - DISABLE_SMTP_AUTH=
# - SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru}
# - SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!}
# - RELAY_NETWORKS=172.16.0.0/12
# - POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12
# - POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
# - POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
# - DISABLE_SIGNING=true
# ports:
# - "25:25"
# - "587:587"
# networks:
# - app-network
# restart: unless-stopped
db:
image: postgres:17
image: postgres:17.7
restart: always
deploy:
resources:
@@ -30,14 +53,17 @@ services:
memory: 1G
environment:
POSTGRES_DB: no_copy_
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_SHARED_BUFFERS: 512MB
POSTGRES_EFFECTIVE_CACHE_SIZE: 1536MB
ports:
- "54320:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
container_name: postgres
networks:
app-network:
@@ -51,20 +77,35 @@ services:
resources:
limits:
cpus: '1.5'
memory: 1G
memory: 3G
reservations:
cpus: '0.5'
memory: 512M
memory: 2G
environment:
JAVA_TOOL_OPTIONS: "-Xmx2g -Xms1g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:+UseContainerSupport -XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75"
FILE_STORAGE_PATH: /data/uploads
MAX_FILE_SIZE: 10737418240
FILE_CHUNK_SIZE: 1048576
# FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432
POSTGRES_HOST: db
STORAGE_SERVICE_URL: http://storage:8081
SPRING_PROFILES_ACTIVE: docker
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
# MAIL_HOST: postfix
# MAIL_PORT: 25
# MAIL_USERNAME: noreply@no-copy.ru
# SMTP_PASSWORD: nocopy!nocopy!
# SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false"
# SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false"
# SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true"
depends_on:
- db
ports:
@@ -77,6 +118,10 @@ services:
- api
volumes:
- uploads_data:/data/uploads:rw
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_" ]
interval: 10s
retries: 5
grafana:
image: grafana/grafana:10.3.1
@@ -120,7 +165,7 @@ services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
deploy: # ← ДОБАВЛЕНО
deploy:
resources:
limits:
cpus: '0.5'
@@ -148,7 +193,7 @@ services:
loki:
image: grafana/loki:2.9.2
container_name: loki
deploy: # ← ДОБАВЛЕНО
deploy:
resources:
limits:
cpus: '1.0'
@@ -173,7 +218,7 @@ services:
tempo:
image: grafana/tempo:2.4.1
container_name: tempo
deploy: # ← ДОБАВЛЕНО
deploy:
resources:
limits:
cpus: '0.5'
@@ -229,7 +274,7 @@ volumes:
loki_rules:
uploads_data:
prometheus_data:
# postfix-data:
networks:
app-network:
external: true
+74 -165
View File
@@ -7,7 +7,6 @@ pipeline {
defaultValue: 'dev',
description: 'Ветка для деплоя'
)
string(
name: 'SERVER',
defaultValue: '92.242.61.23',
@@ -18,6 +17,7 @@ pipeline {
stages {
stage('Git pull') {
steps {
cleanWs()
script {
checkout([
$class: 'GitSCM',
@@ -39,169 +39,80 @@ pipeline {
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
),
string(credentialsId: 'DB_USER', variable: 'DB_USER'),
string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD')
]) {
sh """
echo "Deploying branch: ${params.BRANCH}"
echo "Copying files to server..."
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "mkdir -p /opt/deployments/${params.BRANCH}"
sshpass -p '$SSH_PASS' scp -r -o StrictHostKeyChecking=no ./* $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH}
echo '========================================'
echo 'Step 1: Checking current state...'
echo '========================================'
docker ps --filter 'name=postgres|app-backend|storage|grafana|prometheus|loki|tempo|alloy' --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}'
echo '========================================'
echo 'Step 2: Setup environment for compatibility...'
echo '========================================'
docker network create app-network 2>/dev/null || echo 'Network app-network already exists'
echo 'COMPOSE_COMPATIBILITY=true' > .env
echo '========================================'
echo 'Step 3: Force cleanup old application...'
echo '========================================'
docker-compose --compatibility down 2>/dev/null || echo 'No previous compose stack'
docker images --filter 'reference=*app-backend*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'No images to remove'
echo '========================================'
echo 'Step 4: Verify files and fix compose...'
echo '========================================'
if [ ! -f docker-compose.yaml ]; then
echo 'ERROR: docker-compose.yaml not found!'
ls -la
exit 1
fi
echo 'Checking docker-compose.yaml syntax...'
docker-compose --compatibility config 2>&1 | head -20 || echo 'Config check output'
echo '========================================'
echo 'Step 5: Start infrastructure services...'
echo '========================================'
echo 'Starting PostgreSQL with resource limits...'
docker-compose --compatibility up -d db
echo 'Starting storage service...'
docker-compose --compatibility up -d storage
echo 'Waiting for PostgreSQL to be ready...'
for i in {1..30}; do
if docker-compose --compatibility exec db pg_isready -U postgres 2>/dev/null; then
echo 'PostgreSQL is ready'
break
fi
echo 'Waiting... attempt ' \$i
sleep 2
done
echo '========================================'
echo 'Step 6: Build application image...'
echo '========================================'
if [ ! -f Dockerfile ]; then
echo 'ERROR: Dockerfile not found!'
exit 1
fi
echo 'Building Docker image...'
docker build --no-cache -t app-backend:latest . 2>&1 | tail -30
echo 'Image created successfully'
docker images app-backend:latest --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.Size}}\\t{{.CreatedAt}}'
echo '========================================'
echo 'Step 7: Start application with resource limits...'
echo '========================================'
docker volume create uploads_data 2>/dev/null || echo 'Volume uploads_data already exists'
echo 'Starting main application...'
docker-compose --compatibility up -d app
echo 'Waiting for application to start...'
sleep 15
echo '========================================'
echo 'Step 8: Start monitoring stack...'
echo '========================================'
docker-compose --compatibility up -d grafana prometheus loki tempo alloy
echo '========================================'
echo 'Step 9: Verify deployment...'
echo '========================================'
echo 'All containers status:'
docker-compose --compatibility ps
echo ''
echo 'Resource limits check:'
echo 'App backend limits:'
docker inspect app-backend --format='{{.HostConfig.Memory}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}' 2>/dev/null || echo 'Cannot inspect'
echo ''
echo 'PostgreSQL limits:'
docker inspect postgres --format='{{.HostConfig.Memory}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}' 2>/dev/null || echo 'Cannot inspect'
echo ''
echo 'Container resource usage:'
docker stats --no-stream --format 'table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}' postgres app-backend storage 2>/dev/null || echo 'Stats not available'
echo '========================================'
echo 'Step 10: Health checks...'
echo '========================================'
echo 'Application health check...'
for i in {1..10}; do
if curl -s -f http://localhost:80/health 2>/dev/null; then
echo '✓ Application health check PASSED'
break
elif [ \$i -eq 10 ]; then
echo '✗ Application health check FAILED'
echo 'Last logs:'
docker-compose --compatibility logs --tail=20 app
else
echo 'Waiting for application... attempt ' \$i
sleep 5
fi
done
echo 'Checking monitoring services...'
if curl -s -f http://localhost:3000/api/health 2>/dev/null; then
echo '✓ Grafana is healthy'
else
echo '⚠ Grafana may be starting'
fi
if curl -s -f http://localhost:9090/-/healthy 2>/dev/null; then
echo '✓ Prometheus is healthy'
fi
echo '========================================'
echo 'Deployment Summary:'
echo '========================================'
echo 'Application URL: http://${params.SERVER}:80'
echo 'Grafana: http://${params.SERVER}:3000 (admin/admin)'
echo 'Prometheus: http://${params.SERVER}:9090'
echo 'Loki: http://${params.SERVER}:3100'
echo ''
echo 'To view logs: docker-compose logs -f app'
echo 'To view all containers: docker-compose ps'
echo 'To stop: docker-compose down'
echo '========================================'
"
echo "Deploying branch: ${params.BRANCH}"
echo "Copying files to server..."
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "mkdir -p /opt/deployments/${params.BRANCH}"
sshpass -p '$SSH_PASS' rsync -av --delete \\
--exclude=.git \\
--exclude=.gradle \\
--exclude=build \\
--exclude=**/build \\
--exclude=.idea \\
--exclude=out \\
. $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH}
echo '1. Остановка старого приложения...'
docker stop app-backend 2>/dev/null || echo 'Контейнер не найден'
docker rm app-backend 2>/dev/null || echo 'Контейнер не найден'
echo '2. Удаление старых образов...'
docker images --filter 'reference=*app*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'Нет образов для удаления'
echo '3. Создание сети если нужно...'
docker network create app-network 2>/dev/null || echo 'Сеть уже существует'
echo '4. Запуск инфраструктуры...'
docker-compose up -d db storage
echo '5. Ожидание PostgreSQL...'
sleep 10
echo '6. Сборка нового образа приложения...'
docker build --no-cache -t app-backend:latest .
echo '7. Запуск приложения...'
docker run -d \\
--name app-backend \\
--network app-network \\
--network-alias app \\
-p 80:8080 \\
-v uploads_data:/data/uploads:rw \\
-e POSTGRES_DB=no_copy_ \\
-e POSTGRES_USER=$DB_USER \\
-e POSTGRES_PASSWORD=$DB_PASSWORD \\
-e POSTGRES_PORT=5432 \\
-e POSTGRES_HOST=db \\
--restart unless-stopped \\
app-backend:latest
echo '8. Запуск мониторинга...'
docker-compose up -d grafana prometheus loki tempo alloy
echo '9. Проверка...'
sleep 5
echo 'Статус контейнеров:'
docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}'
echo '10. Проверка health...'
if curl -s -f http://localhost:80/health > /dev/null 2>&1; then
echo 'Приложение работает'
echo 'URL: http://${params.SERVER}:80'
else
echo 'Проверка health не удалась'
docker logs app-backend --tail=20
fi
"
"""
}
}
@@ -213,8 +124,6 @@ pipeline {
success {
echo "Deployment successful"
echo "Application URL: http://${params.SERVER}:80"
echo "Grafana: http://${params.SERVER}:3000"
echo "Prometheus: http://${params.SERVER}:9090"
}
failure {
echo "Deployment failed for branch ${params.BRANCH}"
@@ -223,4 +132,4 @@ pipeline {
echo "Deployment process finished"
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'ncp_db') THEN
CREATE USER ncp_db WITH PASSWORD 'ncpDbApp';
END IF;
END
$$;
GRANT CONNECT ON DATABASE no_copy_ TO ncp_db;
GRANT USAGE ON SCHEMA public TO ncp_db;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ncp_db;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ncp_db;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO ncp_db;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO ncp_db;
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
plugins {
kotlin("jvm") version "2.1.10"
}
group = "ru.soune"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2")
implementation("io.insert-koin:koin-core:4.1.1")
implementation("io.insert-koin:koin-core-jvm:4.1.1")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
+5
View File
@@ -0,0 +1,5 @@
package ru.soune
fun main() {
println("Hello World!")
}
+5
View File
@@ -1 +1,6 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
rootProject.name = 'no-copy'
include 'referral'
@@ -1,17 +1,49 @@
package ru.soune.nocopy.configuration;
import com.vrt.AudioFilePathProvider;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.audio.AudioLocalSearch;
import com.vrt.fileprotection.image.ImageLocalSearch;
import com.vrt.fileprotection.image.ImageUniqueCheck;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Collections;
@Configuration
@EnableAutoConfiguration
@AllArgsConstructor
public class ApplicationConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public com.vrt.
NoCopyFileService noCopyFileService(
FileProtector.FileProvider fileProvider,
FileProtector.ProcessingListener processingListener,
ImageUniqueCheck imageUniqueCheck,
ImageLocalSearch imageLocalSearch,
AudioLocalSearch audioLocalSearch,
AudioFilePathProvider audioFilePathProvider) {
return new com.vrt.NoCopyFileService(
Collections.emptyList(),
fileProvider,
processingListener,
imageUniqueCheck,
imageLocalSearch,
audioLocalSearch,
audioFilePathProvider
);
}
}
@@ -16,7 +16,10 @@ public class HandlerConfig {
LoginRequestHandler login,
FileUploadHandler upload,
FileEntityHandler file,
LogoutRequestHandler logoutHandler
LogoutRequestHandler logoutHandler,
ImageFoundRequestHandler imageFoundRequestHandler,
VerifyRegisterUserHandler verifyRegisterUser,
AuthRequestHandler authRequestHandler
) {
Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login);
@@ -24,6 +27,9 @@ public class HandlerConfig {
map.put(20004, upload);
map.put(20005, file);
map.put(20006, logoutHandler);
map.put(20007, imageFoundRequestHandler);
map.put(20008, authRequestHandler);
map.put(20009, verifyRegisterUser);
return map;
}
@@ -35,6 +35,8 @@ public class JacksonConfig {
mapper.registerModule(javaTimeModule);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
return mapper;
}
@@ -0,0 +1,22 @@
package ru.soune.nocopy.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(30000);
requestFactory.setReadTimeout(60000);
restTemplate.setRequestFactory(requestFactory);
return restTemplate;
}
}
@@ -0,0 +1,19 @@
package ru.soune.nocopy.configuration.file;
import lombok.AllArgsConstructor;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.service.file.FileProcessingOrchestrator;
@Component
@AllArgsConstructor
public class NoCopyInitializer {
private final FileProcessingOrchestrator orchestrator;
@EventListener(ApplicationReadyEvent.class)
public void initializeOnStartup() {
orchestrator.initializeProcessingQueue();
}
}
@@ -1,9 +1,15 @@
package ru.soune.nocopy.controller;
import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -14,20 +20,22 @@ import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.RegAnswer;
import ru.soune.nocopy.dto.file.ChunkUploadResponse;
import ru.soune.nocopy.dto.file.CompleteUploadResponse;
import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.dto.file.UploadProgress;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.*;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.util.FileUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -35,6 +43,7 @@ import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@@ -48,7 +57,15 @@ public class ApiController {
private final FileEntityService fileEntityService;
private final AuthTokenRepository authTokenRepository;
private final AuthService authService;
private final FileSimilarityService fileSimilarityService;
private final FileEntityRepository fileEntityRepository;
private final NoCopyFileService noCopyFileService;
private final FileUtil fileUtil;
@PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@@ -71,6 +88,12 @@ public class ApiController {
return ResponseEntity.ok().body(response);
} catch (ValidationException e) {
return createValidationErrorResponse(e.getBindingResult(), e.getMsgId());
} catch (UserNotFoundException e) {
return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.USER_NOT_FOUND.getCode(),
MessageCode.USER_NOT_FOUND.getDescription(), new HashMap<>()));
} catch (NotFoundAuthToken e) {
return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>()));
} catch (NotValidFieldException e) {
throw e;
} catch (Exception e) {
@@ -90,57 +113,82 @@ public class ApiController {
@PathVariable("version") int version,
@RequestParam(value = "upload_id", required = false) String uploadId,
@RequestParam(value = "chunk_number", required = false) Integer chunkNumber,
@RequestParam(value = "chunk", required = false) MultipartFile chunk) {
@RequestParam(value = "chunk", required = false) MultipartFile chunk,
@RequestParam(value = "findSimilar", required = false, defaultValue = "0") Integer findSimilar) {
try {
if (chunk == null || chunk.isEmpty()) {
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Chunk file null or empty", ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.build()));
return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty");
}
if (uploadId == null || uploadId.isBlank()) {
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Upload ID is required", ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.build()));
return buildErrorResponse(uploadId, chunkNumber, "Upload ID is required");
}
if (chunkNumber == null || chunkNumber < 0) {
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Valid chunk number is required", ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.build()));
return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required");
}
fileUploadService.uploadChunk(uploadId, chunkNumber, chunk);
UploadProgressResponse uploadProgressResponse = fileUploadService.uploadChunk(uploadId, chunkNumber,
chunk, findSimilar);
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.chunkSize(chunk.getSize())
.message("Chunk uploaded successfully")
.build();
return buildSuccessResponse(uploadId, chunkNumber, chunk,
fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath()));
} catch (DuplicateImageException e) {
Map<String, Object> duplicateData = new HashMap<>();
duplicateData.put("duplicateFileId", e.duplicateFileId());
duplicateData.put("userId", e.userId());
duplicateData.put("message", e.getMessage());
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(),
"Chunk uploaded successfully", responseBody));
} catch (Exception e) {
log.error("Error uploading chunk", e);
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.build();
if (uploadId != null) {
duplicateData.put("uploadId", uploadId);
}
return ResponseEntity.ok().body(new BaseResponse(
20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to upload chunk: " + e.getMessage(), responseBody));
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(),
duplicateData
));
} catch (Exception e) {
log.error("Error uploading chunk", e);
return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage());
}
}
@GetMapping("/v{version}/files/{fileId}/similar")
public ResponseEntity<BaseResponse> findSimilarFiles(
@PathVariable("version") int version,
@PathVariable String fileId,
@RequestParam(value = "auth_token",required = false) String authToken,
@RequestParam(required = false) List<String> similarityLevels,
@PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) {
SimilarityFilter filter = SimilarityFilter.builder()
.similarityLevels(similarityLevels)
.build();
Page<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken);
String messageDesc;
MessageCode success;
if (similarFiles.isEmpty()) {
messageDesc = MessageCode.FILE_NOT_FOUND.getDescription();
success = MessageCode.SUCCESS;
} else {
messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription();
success = MessageCode.SUCCESS;
}
Map<String, Object> responseData = new HashMap<>();
responseData.put("content", similarFiles.getContent());
responseData.put("page", similarFiles.getNumber());
responseData.put("size", similarFiles.getSize());
responseData.put("totalElements", similarFiles.getTotalElements());
responseData.put("totalPages", similarFiles.getTotalPages());
responseData.put("hasNext", similarFiles.hasNext());
responseData.put("hasPrevious", similarFiles.hasPrevious());
return ResponseEntity.ok()
.body(new BaseResponse(20004, success.getCode(), messageDesc, responseData));
}
@GetMapping("/v{version}/files/progress/{uploadId}")
public ResponseEntity<BaseResponse> getUploadProgress(
@PathVariable("version") int version,
@@ -164,7 +212,6 @@ public class ApiController {
return ResponseEntity.ok().body(new BaseResponse(
20004, MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), responseBody));
} catch (Exception e) {
log.error("Error getting progress for upload: {}", uploadId, e);
@@ -257,7 +304,7 @@ public class ApiController {
errorData));
}
Long userId = getUserIdFromToken(tokenHeader);
Long userId = authService.useUserAuthToken(tokenHeader);
FileEntityResponse entityResponse = fileEntityService.getById(fileId, version);
if (!entityResponse.getUserId().equals(userId)) {
@@ -332,6 +379,119 @@ public class ApiController {
}
}
@GetMapping("/protect/{fileId}")
public ResponseEntity<?> protect( @PathVariable(required = false) String fileId) {
Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileId);
if (!optionalFileEntity.isPresent()) {
return ResponseEntity.notFound().build();
}
FileEntity fileEntity = optionalFileEntity.get();
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
noCopyFileService.addFile(fileInfo);
fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING);
fileEntityRepository.save(fileEntity);
return ResponseEntity.ok().build();
}
@GetMapping("/check/{fileId}/{type}")
public ResponseEntity<?> check(@PathVariable(required = false) String fileId,
@PathVariable(required = false) String type) {
Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileId);
if (!optionalFileEntity.isPresent()) {
return ResponseEntity.notFound().build();
}
FileEntity fileEntity = optionalFileEntity.get();
Path path = type.toLowerCase().equals("image") ? Paths.get(fileEntity.getFilePath()) :
Paths.get(fileEntity.getProtectedFilePath());
File file = path.toFile();
NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file,
FileProtector.Type.valueOf(type.toUpperCase()));
return ResponseEntity.ok().body(noCopyCheckResult);
}
private ResponseEntity<BaseResponse> checkForDuplicates(String uploadId) throws IOException {
Optional<FileEntity> uploadedFile = fileEntityRepository.findByUploadSessionId(uploadId);
if (uploadedFile.isEmpty() || !uploadedFile.get().getMimeType().equals("image")) {
return null;
}
FileEntity fileEntity = uploadedFile.get();
List<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileEntity.getId());
if (hasDuplicate(similarFiles)) {
return handleDuplicate(fileEntity, similarFiles);
}
return null;
}
private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5);
}
private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileDTO> similarFiles)
throws IOException {
fileEntityService.deleteFromDisk(fileEntity);
fileEntityService.softDeleteFileWithHash(fileEntity);
Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId());
if (originalFile.isPresent()) {
Map<String, String> duplicateInfo = Map.of(
"duplicate_file_id", originalFile.get().getId(),
"owner_user_id", String.valueOf(originalFile.get().getUserId()));
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
"Failed to upload chunk, duplicate",
duplicateInfo));
}
return null;
}
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.chunkSize(chunk.getSize())
.fileId(fileId)
.message("Chunk uploaded successfully")
.build();
return ResponseEntity.ok().body(new BaseResponse(
20000,
MessageCode.SUCCESS.getCode(),
"Chunk uploaded successfully",
responseBody));
}
private ResponseEntity<BaseResponse> buildErrorResponse(String uploadId, Integer chunkNumber, String errorMessage) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId)
.chunkNumber(chunkNumber)
.build();
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.FILE_UPLOAD_ERROR.getCode(),
errorMessage,
responseBody));
}
private ResponseEntity<BaseResponse> createValidationErrorResponse(BindingResult bindingResult, Integer msgId) {
List<Map<String, String>> fieldErrors = bindingResult.getFieldErrors()
.stream()
@@ -367,23 +527,4 @@ public class ApiController {
}
return contentType;
}
private boolean isPreviewSupported(String mimeType) {
if (mimeType == null) {
return false;
}
return mimeType.startsWith("image") ||
mimeType.startsWith("text") ||
mimeType.equals("pdf") ||
mimeType.startsWith("video") ||
mimeType.startsWith("audio");
}
private Long getUserIdFromToken(String tokenHeader) {
String token = tokenHeader.replace("Bearer ", "");
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
return authToken.getUser().getId();
}
}
@@ -3,6 +3,9 @@ package ru.soune.nocopy.controller;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("check/api")
public class HealtCheckController {
@@ -11,4 +14,22 @@ public class HealtCheckController {
public HttpStatus healtCheck() {
return HttpStatus.OK;
}
@GetMapping("/api/debug/memory")
public Map<String, String> getMemoryInfo() {
Runtime runtime = Runtime.getRuntime();
long mb = 1024 * 1024;
Map<String, String> info = new HashMap<>();
info.put("maxMemory (MB)", String.valueOf(runtime.maxMemory() / mb));
info.put("totalMemory (MB)", String.valueOf(runtime.totalMemory() / mb));
info.put("freeMemory (MB)", String.valueOf(runtime.freeMemory() / mb));
info.put("usedMemory (MB)", String.valueOf((runtime.totalMemory() - runtime.freeMemory()) / mb));
info.put("availableProcessors", String.valueOf(runtime.availableProcessors()));
info.put("JAVA_TOOL_OPTIONS", System.getenv("JAVA_TOOL_OPTIONS"));
info.put("JAVA_OPTS", System.getenv("JAVA_OPTS"));
return info;
}
}
@@ -1,11 +0,0 @@
package ru.soune.nocopy.controller;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("v1/api/content")
@AllArgsConstructor
public class UserContentController {
}
@@ -2,20 +2,29 @@ package ru.soune.nocopy.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO;
import ru.soune.nocopy.dto.UserRequest;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.mapper.UserMapper;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.UserService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.user.UserService;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@@ -31,10 +40,14 @@ public class UserController {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final AuthService authService;
@GetMapping("/all")
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> allUsers = userRepository.findAll().stream()
.map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.getIsActive(),
.map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.isActive(),
u.getPhone(), u.getGenderType(),
u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType()))
.toList();
@@ -52,19 +65,19 @@ public class UserController {
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found"));
if (authToken != null) {
Optional<User> user = userRepository.findByEmail(email);
User user = userRepository.findByEmail(email);
//TODO add mapper
if (user.isPresent()) {
UserDTO userDTO = userMapper.toDTO(user.get());
if (user != null) {
UserDTO userDTO = userMapper.toDTO(user);
userDTO.setEmail(email);
userDTO.setFullName(user.get().getFullName());
userDTO.setCompany(user.get().getCompany());
userDTO.setPhone(user.get().getPhone());
userDTO.setGenderType(user.get().getGenderType());
userDTO.setBirthday(user.get().getBirthday());
userDTO.setCreatedAt(user.get().getCreatedAt());
userDTO.setSubscriptionType(user.get().getSubscriptionType());
userDTO.setActive(user.get().getIsActive());
userDTO.setFullName(user.getFullName());
userDTO.setCompany(user.getCompany());
userDTO.setPhone(user.getPhone());
userDTO.setGenderType(user.getGenderType());
userDTO.setBirthday(user.getBirthday());
userDTO.setCreatedAt(user.getCreatedAt());
userDTO.setSubscriptionType(user.getSubscriptionType());
userDTO.setActive(user.isActive());
return ResponseEntity.ok(userDTO);
}
@@ -108,4 +121,50 @@ public class UserController {
return ResponseEntity.ok(userService.updateUser(userRequest, user));
}
@PostMapping("/create-user")
public ResponseEntity<User> addUser(@RequestBody RegRequest registerRequest) {
if (userRepository.existsByEmail(registerRequest.getEmail()) ||
userRepository.existsByPhone(registerRequest.getPhone())) {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setFieldErrors(Arrays.asList(Map.of("email", registerRequest.getEmail())));
regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", registerRequest.getPhone())));
throw new NotValidFieldException("User already exists with email:" + registerRequest.getEmail() + " or phone: " +
registerRequest.getPhone(), new BaseResponse(2,
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
}
User user = new User();
user.setFullName(registerRequest.getFullName());
user.setEmail(registerRequest.getEmail());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setActive(true);
user.setEmailVerified(true);
if (registerRequest.getCompanyName() != null) {
user.setCompany(registerRequest.getCompanyName());
}
if (registerRequest.getPhone() != null) {
user.setPhone(registerRequest.getPhone());
}
User savedUser = userRepository.save(user);
AuthToken authToken = authService.generateAuthToken(savedUser);
authTokenRepository.save(authToken);
return ResponseEntity.ok().body(savedUser);
}
@DeleteMapping("/delete-user/{userId}")
public ResponseEntity<?> addUser(@PathVariable Long userId) {
userRepository.deleteById(userId);
return ResponseEntity.ok().body(Map.of("userId", userId, "deleted", "true"));
}
}
@@ -1,93 +0,0 @@
package ru.soune.nocopy.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.LoginAnswer;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.VkOAuthService;
import ru.soune.nocopy.service.auth.ExternalAuthUserService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class VkAuthController {
private final VkOAuthService vkOAuthService;
private final AuthService authService;
private final ExternalAuthUserService externalAuthUserService;
@GetMapping("/v{version}/vk/authorization/{code}/{state}/{codeVerifier}/{deviceId}")
public BaseResponse vkAuth(@PathVariable("code") String code,
@PathVariable("state") String state,
@PathVariable("codeVerifier") String codeVerifier,
@PathVariable("deviceId") String deviceId) {
try {
Map<String, Object> authMap = vkOAuthService.authTokenMap(code, state, codeVerifier, deviceId);
ObjectMapper mapper = new ObjectMapper();
String bodyJson = (String) authMap.get("body");
Map<String, Object> body = mapper.readValue(bodyJson, Map.class);
String accessToken = (String) body.get("access_token");
long vkUserId = ((Number) body.get("user_id")).longValue();
Integer expiresIn = (Integer) body.get("expires_in");
String refreshToken = (String) body.get("refresh_token");
Map<String, Object> vkUserInfo = vkOAuthService.userVkInfo(accessToken);
log.info("vkUserInfo {}", vkUserInfo);
String userInfoBodyJson = (String) vkUserInfo.get("body");
Map<String, Object> userInfoBody = mapper.readValue(userInfoBodyJson, Map.class);
Map<String, Object> user = (Map<String, Object>) userInfoBody.get("user");
String firstName = (String) user.get("first_name");
String lastName = (String) user.get("last_name");
String fullName = firstName + " " + lastName;
User userEntity = externalAuthUserService.findOrCreateUserFromVK(Long.toString(vkUserId), fullName,
null, accessToken, refreshToken, expiresIn);
AuthToken ourToken = authService.createTokenForUser(userEntity);
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setToken(ourToken.getToken());
loginAnswer.setFieldErrors(new ArrayList<>());
return new BaseResponse(20009, MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), loginAnswer);
} catch (Exception e) {
log.error("VK auth error: ", e);
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setToken(null);
List<Map<String, String>> errors = new ArrayList<>();
Map<String, String> error = new HashMap<>();
error.put("field", "vk_auth");
error.put("message", e.getMessage());
errors.add(error);
loginAnswer.setFieldErrors(errors);
return new BaseResponse(20009, MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), loginAnswer);
}
}
}
@@ -1,9 +1,11 @@
package ru.soune.nocopy.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
public class BaseRequest {
@JsonProperty("version")
Integer version;
@@ -1,11 +1,13 @@
package ru.soune.nocopy.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
public class BaseResponse {
@JsonProperty("msg_id")
private Integer msgId;
@@ -6,10 +6,15 @@ public enum MessageCode {
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
INVALID_FIELD(2, "Invalid field"),
INVALID_TOKEN(2, "Invalid token"),
TOKEN_IS_ALIVE(2, "Token is alive"),
INVALID_ACTION(2, "Invalid action"),
FILE_UPLOAD_ERROR(2, "File upload error"),
DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"),
FILE_DOWNLOAD_ERROR(2, "File download error"),
USER_NOT_VERIFIED(2, "User not verified"),
USER_NOT_FOUND(2, "User not found"),
FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
IMAGE_FOUND_ERROR(2, "Image found error"),
INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
INCOMPLETE_UPLOAD(2, "Not load all chunks"),
MSG_ID_NOT_FOUND(4, "Message id not found"),
@@ -18,8 +23,11 @@ public enum MessageCode {
AUTH_EMAIL_NOT_FOUND(4, "Email not found"),
AUTH_EMAIL_OR_TOKEN_NOT_FOUND(4, "Email or Token not found "),
AUTH_TOKEN_MISMATCH(4, "Token mismatch"),
AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
FILE_NOT_FOUND(4, "File not found"),
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match");
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"),
SEND_EMAIL_EXCEPTION(2, "Send email exception"),
SIMILAR_FILES_FOUND(0, "Similar files found");
private final Integer code;
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class TokenProcessRequest {
@JsonProperty("token")
private String token;
}
@@ -1,14 +0,0 @@
package ru.soune.nocopy.dto;
import lombok.Data;
@Data
public class UserContentDTO {
String fileName;
String originalFilename;
String fileType;
String fileExtension;
Long fileSize;
String filePath;
Integer protectionLevel;
}
@@ -1,17 +0,0 @@
package ru.soune.nocopy.dto;
import ru.soune.nocopy.entity.ContentStatus;
import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime;
public record UserContentResponse(
Long id,
String filename,
String originalFilename,
FileType fileType,
long fileSize,
String filePath,
ContentStatus status,
LocalDateTime uploadDate
) {}
@@ -20,6 +20,9 @@ public class ChunkUploadResponse {
@JsonProperty("chunk_size")
private Long chunkSize;
@JsonProperty("file_id")
private String fileId;
@JsonProperty("message")
private String message;
}
@@ -1,41 +0,0 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
public static <T> FileApiResponse<T> success(T data) {
return FileApiResponse.<T>builder()
.success(true)
.message("Operation completed successfully")
.data(data)
.build();
}
public static <T> FileApiResponse<T> success(String message, T data) {
return FileApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.build();
}
public static <T> FileApiResponse<T> error(String error) {
return FileApiResponse.<T>builder()
.success(false)
.error(error)
.build();
}
}
@@ -17,6 +17,9 @@ public class FileEntityRequest {
@JsonProperty("file_id")
private String fileId;
@JsonProperty("full_delete")
private Integer fullDelete;
@JsonProperty("upload_session_id")
private String uploadSessionId;
@@ -30,4 +30,6 @@ public class FileEntityResponse {
private String formattedSize;
private String downloadUrl;
private boolean existsOnDisk;
private Integer supportId;
private String protectStatus;
}
@@ -58,4 +58,16 @@ public class FileInfoUserResponse {
@JsonProperty("audios_violations")
private Integer audiosViolations;
@JsonProperty("protected_files_count")
private Long protectedFilesCount;
@JsonProperty("protected_audio_files_count")
private Long protectedAudioFilesCount;
@JsonProperty("protected_video_files_count")
private Long protectedVideoFilesCount;
@JsonProperty("protected_image_files_count")
private Long protectedImageFilesCount;
}
@@ -0,0 +1,68 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GoogleVisionSearchResponse {
@JsonProperty("bestGuessLabels")
private List<BestGuessLabel> bestGuessLabels;
@JsonProperty("fullMatchingImages")
private List<ImageResult> fullMatchingImages;
@JsonProperty("visuallySimilarImages")
private List<ImageResult> visuallySimilarImages;
@JsonProperty("pagesWithMatchingImages")
private List<PageResult> pagesWithMatchingImages;
@JsonProperty("partialMatchingImages")
private List<ImageResult> partialMatchingImages;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class BestGuessLabel {
@JsonProperty("label")
private String label;
@JsonProperty("languageCode")
private String languageCode;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ImageResult {
@JsonProperty("url")
private String url;
@JsonProperty("score")
private Float score;
@JsonProperty("height")
private Integer height;
@JsonProperty("width")
private Integer width;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class PageResult {
@JsonProperty("url")
private String url;
@JsonProperty("pageTitle")
private String pageTitle;
@JsonProperty("fullMatchingImages")
private List<ImageResult> fullMatchingImages;
@JsonProperty("partialMatchingImages")
private List<ImageResult> partialMatchingImages;
}
}
@@ -0,0 +1,10 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ImageSearchRequest {
@JsonProperty("file_id")
private String fileId;
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class SimilarFileDTO {
String fileId;
String originalFileName;
Long fileSize;
Integer hammingDistance;
String similarityLevel;
Long ownerId;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SimilarityFilter {
private List<String> similarityLevels;
private Integer page;
private Integer size;
}
@@ -0,0 +1,36 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class YandexSearchResponse {
@JsonProperty("images")
private List<ImageResult> images;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ImageResult {
@JsonProperty("url")
private String url;
@JsonProperty("pageUrl")
private String pageUrl;
@JsonProperty("pageTitle")
private String pageTitle;
@JsonProperty("width")
private Integer width;
@JsonProperty("height")
private Integer height;
@JsonProperty("host")
private String host;
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -13,5 +13,9 @@ import java.util.Map;
public class LoginAnswer {
private String Token;
private boolean isActive;
private boolean isVerified;
private List<Map<String, String>> fieldErrors;
}
@@ -1,5 +1,6 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@@ -10,6 +11,7 @@ import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
public class LoginRequest {
@NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -13,5 +13,13 @@ import java.util.Map;
public class RegAnswer {
private String Token;
private boolean isActive;
private boolean isVerified;
private Long userId;
private String email;
private List<Map<String, String>> fieldErrors;
}
@@ -1,10 +1,12 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
public class RegRequest {
@NotEmpty(message = "Full name is required")
private String fullName;
@@ -0,0 +1,20 @@
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerifyUserRequest {
@JsonProperty("verify_token")
private String verifyToken;
@JsonProperty("user_id")
private Long userId;
@JsonProperty("resend")
private Integer resend;
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@@ -1,11 +1,11 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.GenderType;
import ru.soune.nocopy.entity.SubscriptionType;
import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -1,15 +1,16 @@
package ru.soune.nocopy.dto;
package ru.soune.nocopy.dto.user;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size;
import lombok.Data;
import ru.soune.nocopy.entity.GenderType;
import ru.soune.nocopy.entity.user.GenderType;
import java.time.LocalDate;
@Data
@JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
public class UserRequest {
@Size(min = 2)
private String fullName;
@@ -1,49 +0,0 @@
package ru.soune.nocopy.entity;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Table(name = "image_protection")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EntityListeners(AuditingEntityListener.class)
public class ImageProtection {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long protectionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
@ToString.Exclude
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "content_id", nullable = false)
@ToString.Exclude
private UserContent content;
@Column(name = "protection_method", nullable = false, length = 50)
private String protectionMethod;
@Column(name = "protection_level", nullable = false)
private Integer protectionLevel;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@CreatedDate
@Column(name = "applied_at", nullable = false, updatable = false)
private LocalDateTime appliedAt;
@Column(name = "metadata", columnDefinition = "JSON")
private String metadata;
}
@@ -1,44 +0,0 @@
package ru.soune.nocopy.entity;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_actions")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EntityListeners(AuditingEntityListener.class)
public class UserAction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long actionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
@ToString.Exclude
private User user;
@Column(name = "action", nullable = false, length = 100)
private String action;
@Column(name = "details", columnDefinition = "TEXT")
private String details;
@Column(name = "ip_address", length = 45)
private String ipAddress;
@Column(name = "user_agent", columnDefinition = "TEXT")
private String userAgent;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
}
@@ -1,89 +0,0 @@
package ru.soune.nocopy.entity.auth;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.User;
import java.time.LocalDateTime;
@Entity
@Table(name = "external_auth_providers",
uniqueConstraints = @UniqueConstraint(
columnNames = {"provider_type", "provider_user_id"}
),
indexes = {
@Index(name = "idx_provider_type_user_id",
columnList = "provider_type, provider_user_id"),
@Index(name = "idx_user_id_provider_type",
columnList = "user_id, provider_type")
})
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class ExternalAuthProvider {
public enum ProviderType {
VK, YANDEX
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_extauth_user"))
@ToString.Exclude
private User user;
@Enumerated(EnumType.STRING)
@Column(name = "provider_type", nullable = false, length = 20)
private ProviderType providerType;
@Column(name = "provider_user_id", nullable = false, length = 255)
private String providerUserId;
@Column(name = "access_token", length = 2048)
private String accessToken;
@Column(name = "refresh_token", length = 2048)
private String refreshToken;
@Column(name = "token_expires_at")
private LocalDateTime tokenExpiresAt;
@Column(name = "email", length = 255)
private String email;
@Column(name = "phone", length = 20)
private String phoneNumber;
@CreatedDate
@Column(name = "linked_at", updatable = false)
private LocalDateTime linkedAt;
@Column(name = "last_used_at")
private LocalDateTime lastUsedAt;
@Column(name = "is_active")
@Builder.Default
private Boolean isActive = true;
@PrePersist
public void prePersist() {
if (this.linkedAt == null) {
this.linkedAt = LocalDateTime.now();
}
if (this.lastUsedAt == null) {
this.lastUsedAt = LocalDateTime.now();
}
}
@PreUpdate
public void preUpdate() {
this.lastUsedAt = LocalDateTime.now();
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.file;
import lombok.Getter;
@@ -1,11 +1,15 @@
package ru.soune.nocopy.entity.file;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.GenerationTime;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDateTime;
@Data
@@ -16,11 +20,13 @@ import java.time.LocalDateTime;
@Table(name = "file_entities")
@EntityListeners(AuditingEntityListener.class)
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "support_id", unique = true, updatable = false)
private Integer supportId;
@Column(name = "user_id", nullable = false)
private Long userId;
@@ -60,11 +66,34 @@ public class FileEntity {
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "protected_file_path")
private String protectedFilePath;
@Column(name = "protection_status")
@Enumerated(EnumType.STRING)
private ProtectionStatus protectionStatus;
@Column(name = "protected_at")
private LocalDateTime protectedAt;
@Column(name = "signature")
@JsonIgnore
private String signature;
@OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private ImageHashEntity imageHash;
@PrePersist
public void prePersist() {
if (this.status == null) {
this.status = FileStatus.ACTIVE;
}
if (this.protectionStatus == null) {
this.protectionStatus = ProtectionStatus.NOT_PROTECTED;
}
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
@@ -7,21 +7,11 @@ import java.util.List;
@Getter
public enum FileType {
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp")),
// IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")),
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")),
VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
"3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")),
AUDIO("audio", Arrays.asList("mp3", "wav", "flac")),
DOCUMENT("document", Arrays.asList(
"pdf", "txt", "rtf",
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "pps", "ppsx", "dot", "dotx", "xlt", "xltx", "pot", "potx",
"odt", "ods", "odp", "odg", "odf", "odb", "odc", "odi", "odm", "ott", "ots", "otp", "otg", "oth",
"sxw", "sxc", "sxi", "sxd", "sxg", "stc", "sti", "stw", "sxm",
"pages", "numbers", "key",
"csv", "tsv", "xml", "html", "htm", "tex", "md", "markdown",
"epub", "mobi", "azw", "azw3", "fb2",
"wps", "wpt", "et", "dps", "vsd", "vsdx",
"java", "py", "cpp", "c", "h", "js", "css", "php", "sql", "json", "yaml", "yml", "sh", "bat",
"one", "note"));
AUDIO("audio", List.of("wav"));
private final String displayName;
private final List<String> allowedExtensions;
@@ -69,6 +69,7 @@ public class FileUploadSession {
private String extension;
@Column(name = "retry_count")
@Builder.Default
private Integer retryCount = 0;
@Column(name = "completed_at")
@@ -81,6 +82,7 @@ public class FileUploadSession {
)
@MapKeyColumn(name = "chunk_number")
@Column(name = "chunk_path")
@Builder.Default
private Map<Integer, String> chunkPaths = new HashMap<>();
@PrePersist
@@ -0,0 +1,43 @@
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "image_hashes", indexes = {
@Index(name = "idx_image_hashes_hash64", columnList = "hash64_hi, hash64_lo"),
@Index(name = "idx_image_hashes_hash64_hi", columnList = "hash64_hi"),
@Index(name = "idx_image_hashes_hash64_lo", columnList = "hash64_lo")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ImageHashEntity {
@Id
@Column(name = "file_id")
private String fileId;
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "file_id")
private FileEntity file;
@Column(name = "hash64_hi")
private Long hash64Hi;
@Column(name = "hash64_lo")
private Long hash64Lo;
@Column(name = "hash_algorithm", nullable = false)
private String hashAlgorithm;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@@ -0,0 +1,9 @@
package ru.soune.nocopy.entity.file;
public enum ProtectionStatus {
NOT_PROTECTED,
PROCESSING,
PROTECTED,
FAILED,
FAILED_SAVE
}
@@ -1,9 +1,10 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime;
@@ -1,10 +1,9 @@
package ru.soune.nocopy.entity.auth;
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.User;
import java.time.LocalDateTime;
@@ -42,8 +41,4 @@ public class AuthToken {
@Column(name = "is_active")
private Boolean isActive = true;
public boolean isValid() {
return Boolean.TRUE.equals(isActive) && expiresAt.isAfter(LocalDateTime.now());
}
}
@@ -0,0 +1,35 @@
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "email_verification_tokens")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EmailVerificationToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "token", nullable = false, length = 6)
private String token;
@Column(name = "expires_at", nullable = false)
private LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(15);
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiresAt);
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.user;
public enum GenderType {
MALE, FEMALE
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.user;
import lombok.Getter;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
@@ -6,14 +6,12 @@ import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.auth.ExternalAuthProvider;
import ru.soune.nocopy.entity.file.Violation;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Entity
@Table(name = "users")
@@ -22,21 +20,23 @@ import java.util.Optional;
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long Id;
@Column(name = "full_name", nullable = false)
private String fullName;
@Size(max = 255)
@Column(name = "email", nullable = false, length = 255, unique = true)
@Size(max = 1024)
@Column(name = "email", nullable = false, length = 1024, unique = true)
private String email;
@Column(name = "email_verified")
private boolean emailVerified = false;
@Column(name = "company")
private String company;
@@ -68,7 +68,7 @@ public class User {
private LocalDateTime lastLoginAt;
@Column(name = "is_active")
private Boolean isActive = true;
private boolean isActive = false;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@@ -83,25 +83,4 @@ public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@ToString.Exclude
private List<UserAction> userActions = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@ToString.Exclude
private List<Violation> violations = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@ToString.Exclude
private List<ImageProtection> imageProtections = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@ToString.Exclude
private List<ExternalAuthProvider> externalAuthProviders = new ArrayList<>();
public Optional<ExternalAuthProvider> getProvider(ExternalAuthProvider.ProviderType providerType) {
return externalAuthProviders.stream()
.filter(p -> p.getProviderType() == providerType)
.findFirst();
}
}
private List<Violation> violations = new ArrayList<>();}
@@ -1,9 +1,10 @@
package ru.soune.nocopy.entity;
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.ContentStatus;
import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime;
@@ -0,0 +1,20 @@
package ru.soune.nocopy.exception;
public class DuplicateImageException extends RuntimeException {
private final String duplicateFileId;
private final Long userId;
public DuplicateImageException(String message, String duplicateFileId, Long userId) {
super(message);
this.duplicateFileId = duplicateFileId;
this.userId = userId;
}
public String duplicateFileId() {
return duplicateFileId;
}
public Long userId() {
return userId;
}
}
@@ -0,0 +1,37 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.TokenProcessRequest;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws NotFoundAuthToken {
TokenProcessRequest tokenProcessRequest = objectMapper.convertValue(request.getMessageBody(),
TokenProcessRequest.class);
authService.useUserAuthToken(tokenProcessRequest.getToken());
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Token processed successfully",
Map.of("token", tokenProcessRequest.getToken()));
}
}
@@ -6,13 +6,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService;
@@ -26,14 +26,16 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityService fileEntityService;
private final FileStatsService fileStatsService;
private final AuthService authService;
private final AuthTokenRepository authTokenRepository;
private final FileStatsService fileStatsService;
private final ObjectMapper objectMapper;
private final FileEntityRepository fileEntityRepository;
private final ImageHashRepository imageHashRepository;
@Override
public BaseResponse handle(BaseRequest request) {
try {
@@ -62,7 +64,8 @@ public class FileEntityHandler implements RequestHandler {
.action(action)
.availableActions(Arrays.asList(
"file_info", "file_by_session", "user_files",
"search_files", "storage_usage", "delete_file"))
"search_files", "storage_usage", "delete_file",
"user_files_info"))
.build();
return new BaseResponse(request.getMsgId(),
@@ -80,7 +83,7 @@ public class FileEntityHandler implements RequestHandler {
}
private BaseResponse handleGetFilesUserInfo(BaseRequest request, FileEntityRequest fileRequest) {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
FileInfoUserResponse userFileStats = fileStatsService.getUserFileStats(userId);
return new BaseResponse(request.getMsgId(),
@@ -91,7 +94,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId(), request.getVersion());
if (!fileInfo.getUserId().equals(userId)) {
@@ -122,7 +125,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetFileBySession(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getByUploadSessionId(fileRequest.getUploadSessionId(),
request.getVersion());
@@ -154,7 +157,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetUserFiles(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
@@ -181,7 +184,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
@@ -228,7 +231,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
long usage = fileEntityService.getUserStorageUsed(userId);
StorageUsageResponse response = StorageUsageResponse.builder()
@@ -258,7 +261,7 @@ public class FileEntityHandler implements RequestHandler {
private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
Long userId = authService.useUserAuthToken(fileRequest.getToken());
String fileId = fileRequest.getFileId();
FileEntityResponse fileInfo = fileEntityService.getById(fileId, request.getVersion());
DeleteFileResponse response;
@@ -272,15 +275,21 @@ public class FileEntityHandler implements RequestHandler {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete();
if (fileEntity.getStatus().equals(FileStatus.DELETED)) {
if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) {
fileEntityService.deleteFromDisk(fileEntity);
response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId())
.message("File deleted from disk")
.build();
} else {
fileEntityService.markAsDeleted(fileEntity);
fileEntityService.softDeleteFileWithHash(fileEntity);
//
// imageHashRepository.deleteByFileId(fileEntity.getId());
//
// fileEntityService.markAsDeleted(fileEntity);
response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId())
.message("File marked as deleted")
@@ -305,20 +314,6 @@ public class FileEntityHandler implements RequestHandler {
}
}
private Long getUserIdFromToken(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
return authToken.getUser().getId();
}
private String formatFileSize(long size) {
if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024));
@@ -10,13 +10,14 @@ import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.validator.FileUploadRequestValidator;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileUploadService;
import java.util.*;
@@ -28,6 +29,8 @@ import java.util.stream.Collectors;
public class FileUploadHandler implements RequestHandler {
private final FileUploadService fileUploadService;
private final AuthService authService;
private final AuthTokenRepository authTokenRepository;
private final FileUploadSessionRepository fileUploadSessionRepository;
@@ -86,10 +89,9 @@ public class FileUploadHandler implements RequestHandler {
MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", token));
}
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
Long userId = authService.useUserAuthToken(token);
BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest");
fileUploadRequestValidator.validate(fileRequest, bindingResult);
fileUploadRequestValidator.validate(fileRequest, bindingResult);
if (bindingResult.hasErrors()) {
Map<String, String> fieldErrors = bindingResult.getFieldErrors()
@@ -105,7 +107,7 @@ public class FileUploadHandler implements RequestHandler {
}
FileUploadSession session = fileUploadService.initUpload(
authToken.getUser().getId(),
userId,
fileRequest.getFileName(),
fileRequest.getFileType(),
fileRequest.getExtension(),
@@ -175,40 +177,13 @@ public class FileUploadHandler implements RequestHandler {
}
}
private BaseResponse handleRetryUpload(BaseRequest request, FileUploadRequest fileRequest) {
try {
fileUploadService.retryFailedUpload(fileRequest.getUploadId());
ru.soune.nocopy.dto.file.UploadProgressResponse progress =
fileUploadService.getUploadProgress(fileRequest.getUploadId());
RetryUploadResponse response = RetryUploadResponse.builder()
.uploadId(progress.getUploadId())
.message("Upload retry initiated")
.status(progress.getStatus().toString())
.build();
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), "Upload retry initiated",
response);
} catch (UploadSessionNotFoundException e) {
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
"Upload session not found", null);
} catch (FileUploadException e) {
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), e.getMessage(),
null);
} catch (Exception e) {
log.error("Error retrying upload", e);
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to retry upload", null);
}
}
private BaseResponse handleGetChunkStatus(BaseRequest request, FileUploadRequest fileRequest) {
try {
FileUploadSession session = fileUploadSessionRepository.findById(fileRequest.getUploadId())
.orElseThrow(() -> new UploadSessionNotFoundException(fileRequest.getUploadId()));
Map<String, Boolean> chunkStatus = new HashMap<>();
for (int i = 0; i < session.getTotalChunks(); i++) {
chunkStatus.put("chunk_" + i, session.getChunkPaths().containsKey(i));
}
@@ -223,7 +198,6 @@ public class FileUploadHandler implements RequestHandler {
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), response);
} catch (UploadSessionNotFoundException e) {
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
"Upload session not found", null);
@@ -5,15 +5,16 @@ import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.exception.*;
import java.util.Map;
@RestControllerAdvice
@AllArgsConstructor
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@@ -81,4 +82,12 @@ public class GlobalExceptionHandler {
"message" ,ex.getMessage()
));
}
@ExceptionHandler(DuplicateImageException.class)
@ResponseBody
public ResponseEntity<BaseResponse> handleDuplicateImage(DuplicateImageException e) {
return ResponseEntity.ok().body(new BaseResponse(
20004, MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
"Duplicate image detected", e.getMessage()));
}
}
@@ -0,0 +1,41 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService;
@Slf4j
@Component
@RequiredArgsConstructor
public class ImageFoundRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final YandexSearchService yandexSearchService;
private final GoogleVisionSearchService googleVisionSearchService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
ImageSearchRequest.class);
String fileId = imageSearchRequest.getFileId();
YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId);
//TODO uncommited when add billing
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), response);
}
}
@@ -4,10 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays;
import java.util.Map;
@@ -17,14 +20,17 @@ import java.util.Map;
public class LoginRequestHandler implements RequestHandler {
private final UserRepository userRepository;
private final AuthService authService;
private final ObjectMapper objectMapper;
@Override
public BaseResponse handle(BaseRequest request) {
LoginRequest loginRequest = objectMapper.convertValue(request.getMessageBody(), LoginRequest.class);
User user = userRepository.findByEmail(loginRequest.getEmail());
if (!userRepository.existsByEmail(loginRequest.getEmail())) {
if (user == null) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail())));
@@ -33,10 +39,24 @@ public class LoginRequestHandler implements RequestHandler {
MessageCode.AUTH_EMAIL_NOT_FOUND.getDescription(), loginAnswer));
}
if (!user.isActive() && !user.isEmailVerified()) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of(
"verified", String.valueOf(user.isEmailVerified()),
"isActive", String.valueOf(user.isActive()))));
return new BaseResponse(request.getMsgId(), MessageCode.USER_NOT_VERIFIED.getCode(),
MessageCode.USER_NOT_VERIFIED.getDescription(), loginAnswer);
}
AuthToken authToken = authService.login(loginRequest);
String token = authToken.getToken();
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setToken(authToken.getToken());
loginAnswer.setToken(token);
authService.useUserAuthToken(token);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), loginAnswer);
@@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.dto.register.LogOutAnswer;
import ru.soune.nocopy.dto.register.LogoutRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Objects;
import java.util.Optional;
@@ -31,16 +33,16 @@ public class LogoutRequestHandler implements RequestHandler {
LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class);
Optional<AuthToken> opToken = authTokenRepository.findByToken(logoutRequest.getToken());
Optional<User> user = userRepository.findByEmail(logoutRequest.getEmail());
User user = userRepository.findByEmail(logoutRequest.getEmail());
if (opToken.isEmpty() || user.isEmpty()) {
if (opToken.isEmpty() || user == null) {
throw new NotValidFieldException("User with email or token not found",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(),
new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken())));
}
if (!Objects.equals(opToken.get().getUser().getId(), user.get().getId())) {
if (!Objects.equals(opToken.get().getUser().getId(), user.getId())) {
throw new NotValidFieldException("User token mismatch with user",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(),
MessageCode.AUTH_TOKEN_MISMATCH.getDescription(),
@@ -7,12 +7,16 @@ import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.AuthService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService;
import java.util.Arrays;
import java.util.Map;
@@ -29,6 +33,8 @@ public class RegRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final EmailService emailService;
@Override
public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -45,6 +51,7 @@ public class RegRequestHandler implements RequestHandler {
}
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult);
if (bindingResult.hasErrors()) {
@@ -52,9 +59,26 @@ public class RegRequestHandler implements RequestHandler {
}
AuthToken authToken = authService.register(regRequest);
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
try {
emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken());
} catch (Exception e) {
return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(),
MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of(
"message", "not send",
"email", regRequest.getEmail()
));
}
String token = authToken.getToken();
authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(authToken.getToken());
regAnswer.setVerified(false);
regAnswer.setActive(false);
regAnswer.setUserId(authToken.getUser().getId());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer);
@@ -0,0 +1,122 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.VerifyUserRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.user.UserService;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
@Component
@Slf4j
@RequiredArgsConstructor
public class VerifyRegisterUserHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
private final EmailService emailService;
private final UserRepository userRepository;
private final UserService userService;
private final AuthTokenRepository authTokenRepository;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws MessagingException, IOException {
VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class);
Long userId = verifyUserRequest.getUserId();
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
Optional<AuthToken> userAuthTokens = authTokenRepository.findByUserId(userId);
AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get();
if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) {
return handleResend(request, user, authToken);
}
return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken());
}
private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) throws MessagingException, IOException {
EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId());
if (token != null && !token.isExpired()) {
emailService.sendVerificationEmail(user, token.getToken());
log.info("Verification token resent for user {}", user.getEmail());
return successResponse(request, authToken, "Verification code has been resent", false,
false, user.getEmail());
}
if (token != null) {
emailVerificationTokenRepository.delete(token);
}
EmailVerificationToken newToken = emailService.createEmailVerificationToken(authToken);
emailService.sendVerificationEmail(user, newToken.getToken());
log.info("New verification token generated for user {}", user.getEmail());
return successResponse(request, authToken, "New verification code has been sent",
false, false, user.getEmail());
}
private BaseResponse handleVerify(BaseRequest request, User user, AuthToken authToken, String providedToken) {
EmailVerificationToken token = emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), providedToken);
if (token == null || token.isExpired()) {
throw new NotValidFieldException("Invalid or expired token",
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
MessageCode.INVALID_TOKEN.getDescription(), Map.of("userId", user.getId())));
}
User updateUser = userService.activateAndVerifyUser(user.getId());
emailVerificationTokenRepository.delete(token);
return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(),
updateUser.isEmailVerified(), updateUser.getEmail());
}
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive,
boolean isVerified, String email) {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(authToken.getToken());
regAnswer.setVerified(isVerified);
regAnswer.setActive(isActive);
regAnswer.setEmail(email);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer);
}
}
@@ -1,19 +1,16 @@
package ru.soune.nocopy.handler.validator;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import ru.soune.nocopy.dto.RegRequest;
import ru.soune.nocopy.dto.register.RegRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
@Component
public class RegRequestValidator implements Validator {
private static final String COMPANY_REGEX = "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'()]{0,200}$";
private static final String COMPANY_REGEX = "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'()\"\"«»„‟”“”‘'’‹›❛❜❝❞〝〞〟]{1,200}$";
private static final String NAME_REGEX = "^[a-zA-Zа-яА-ЯёЁ\\s\\-'.]{2,100}$";
@@ -74,21 +71,6 @@ public class RegRequestValidator implements Validator {
errors.rejectValue("companyName", "companyName.invalid.chars",
"Company name contains invalid characters");
}
validateForbiddenWords(trimmedCompany, "companyName", errors,
Arrays.asList("admin", "root", "system", "test"));
}
private void validateForbiddenWords(String text, String fieldName,
Errors errors, List<String> forbiddenWords) {
String lowerText = text.toLowerCase();
for (String word : forbiddenWords) {
if (lowerText.contains(word)) {
errors.rejectValue(fieldName, fieldName + ".forbidden.word",
"Contains forbidden word: " + word);
break;
}
}
}
private void validateEmail(String email, Errors errors) {
@@ -156,15 +138,6 @@ public class RegRequestValidator implements Validator {
errors.rejectValue("password", "password.contains.spaces",
"Password cannot contain spaces");
}
// recomment if need complexity
// checkPasswordComplexity(password, errors);
// reccoment if need check simply standart password
// checkCommonPasswords(password, errors);
// reccoment if need check simply standart password
// checkForSequences(password, errors);
}
private void checkPasswordComplexity(String password, Errors errors) {
@@ -4,8 +4,8 @@ import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserContentUpdateRequest;
import ru.soune.nocopy.entity.UserContent;
import ru.soune.nocopy.dto.user.UserContentUpdateRequest;
import ru.soune.nocopy.entity.user.UserContent;
@Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -3,8 +3,8 @@ package ru.soune.nocopy.mapper;
import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserDTO;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.entity.user.User;
@Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -1,14 +1,24 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.auth.AuthToken;
import ru.soune.nocopy.entity.user.AuthToken;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter);
List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
Optional<AuthToken> findByToken(String token);
@Query(value = """
SELECT a.user_id FROM auth_tokens a WHERE a.token = :token
""",
nativeQuery = true)
Long findUserIdByToken(String token);
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
Optional<AuthToken> findByUserId(Long userId);
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface EmailVerificationTokenRepository extends JpaRepository<EmailVerificationToken, Long> {
Optional<EmailVerificationToken> findByToken(String token);
EmailVerificationToken findByUserIdAndToken(Long userId, String token);
List<EmailVerificationToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
EmailVerificationToken findByUserId(Long userId);
}
@@ -1,25 +0,0 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.entity.auth.ExternalAuthProvider;
import java.util.List;
import java.util.Optional;
@Repository
public interface ExternalAuthProviderRepository extends JpaRepository<ExternalAuthProvider, Long> {
Optional<ExternalAuthProvider> findByProviderTypeAndProviderUserId(
ExternalAuthProvider.ProviderType providerType,
String providerUserId
);
List<ExternalAuthProvider> findByUser(User user);
boolean existsByProviderTypeAndProviderUserId(
ExternalAuthProvider.ProviderType providerType,
String providerUserId
);
}
@@ -6,28 +6,48 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.util.List;
import java.util.Optional;
@Repository
public interface FileEntityRepository extends JpaRepository<FileEntity, String> {
List<FileEntity> findByUserId(Long userId);
Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
Optional<FileEntity> findByFilePath(String filePath);
Optional<FileEntity> findByUploadSessionId(String uploadSessionId);
List<FileEntity> findByProtectionStatus(ProtectionStatus protectionStatus);
boolean existsByFilePath(String filePath);
@Query("SELECT SUM(f.fileSize) FROM FileEntity f WHERE f.userId = :userId AND f.status = 'ACTIVE'")
Long getTotalSizeByUserId(@Param("userId") Long userId);
@Query("SELECT f FROM FileEntity f WHERE f.protectionStatus = null AND f.status = 'ACTIVE' OR " +
"f.protectionStatus = 'NOT_PROTECTED' AND f.status = 'ACTIVE'")
List<FileEntity> findAllActiveFilesAndNotProtected();
@Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.originalFileName LIKE %:keyword%")
List<FileEntity> searchByFileName(@Param("userId") Long userId, @Param("keyword") String keyword);
@Query("SELECT f FROM FileEntity f WHERE f.status = :status")
List<FileEntity> searchFileEntityByStatus(@Param("status") FileStatus status);
@Query("SELECT f FROM FileEntity f WHERE f.id = :fileId")
FileEntity findByFileId(@Param("fileId") String fileId);
@Query("SELECT f FROM FileEntity f WHERE f.signature = :signature")
FileEntity findBySignature(@Param("signature") String signature);
@Query("SELECT f.id FROM FileEntity f WHERE f.filePath = :filePath")
String findFileIdByFilePath(@Param("filePath") String filePath);
long countByUserId(Long userId);
}
@@ -0,0 +1,14 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.ImageHashEntity;
import java.util.List;
@Repository
public interface ImageHashRepository extends JpaRepository<ImageHashEntity, String> {
void deleteByFileId(String fileId);
List<ImageHashEntity> findByFileId(String fileId);
ImageHashEntity findByHash64HiAndHash64Lo(Long hash64Hi, Long hash64Lo);
}
@@ -0,0 +1,75 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.FileEntity;
import java.util.List;
@Repository
public interface ImageSimilarityRepository
extends JpaRepository<FileEntity, String> {
@Query(value = """
SELECT
f.id AS id,
f.original_file_name AS originalFileName,
f.file_size AS fileSize,
f.user_id AS userId,
h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo
FROM image_hashes ref
JOIN image_hashes h
ON ref.file_id <> h.file_id
JOIN file_entities f
ON f.id = h.file_id
WHERE ref.file_id = :fileId
""",
nativeQuery = true)
List<SimilarImageProjection> findCandidates(
@Param("fileId") String fileId
);
@Query(value = """
SELECT
f.id AS id,
f.original_file_name AS originalFileName,
f.file_size AS fileSize,
f.user_id AS userId,
h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo
FROM image_hashes ref
JOIN image_hashes h
ON ref.file_id <> h.file_id
JOIN file_entities f
ON f.id = h.file_id
WHERE ref.file_id = :fileId AND f.user_id = :userId
""",
nativeQuery = true)
List<SimilarImageProjection> findCandidatesFromUserFiles(
@Param("fileId") String fileId,
@Param("userId") Long userId
);
@Query(value = """
SELECT
h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo,
h.file_id AS fileId,
f.user_id AS userId,
f.original_file_name AS originalFileName,
f.file_size AS fileSize,
f.stored_file_name AS similarFileId
FROM image_hashes h
JOIN file_entities f ON f.id = h.file_id
WHERE h.hash64_hi = :hash64Hi
AND h.hash64_lo = :hash64Lo
""",
nativeQuery = true)
List<SimilarImageProjection> findExactDuplicates(
@Param("hash64Hi") Long hash64_hi,
@Param("hash64Lo") Long hash64_lo);
}
@@ -0,0 +1,10 @@
package ru.soune.nocopy.repository;
public interface SimilarImageProjection {
Long getHash64Hi();
Long getHash64Lo();
String getId();
Long getUserId();
String getOriginalFileName();
Long getFileSize();
}
@@ -3,7 +3,7 @@ package ru.soune.nocopy.repository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.UserContent;
import ru.soune.nocopy.entity.user.UserContent;
import java.util.List;
@@ -2,21 +2,12 @@ package ru.soune.nocopy.repository;
import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.entity.auth.ExternalAuthProvider;
import ru.soune.nocopy.entity.user.User;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
User findByEmail(String email);
boolean existsByEmail(String email);
boolean existsByPhone(@Size(min = 11, max = 14) String phone);
@Query("SELECT u FROM User u JOIN u.externalAuthProviders p " +
"WHERE p.providerType = :providerType AND p.providerUserId = :providerUserId")
Optional<User> findByExternalProvider(
@Param("providerType") ExternalAuthProvider.ProviderType providerType,
@Param("providerUserId") String providerUserId
);
}
@@ -0,0 +1,191 @@
package ru.soune.nocopy.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.dto.file.SimilarityFilter;
import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.repository.ImageSimilarityRepository;
import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.util.FileUtil;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class FileSimilarityService {
private final ImageSimilarityRepository repository;
private final ImageHashRepository hashRepository;
private final FileUtil fileUtil;
private final AuthTokenRepository authTokenRepository;
public List<SimilarFileDTO> findSimilarFiles(String fileId) {
var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found"));
Long hash64Hi = imageHashEntity.getHash64Hi();
Long hash64Lo = imageHashEntity.getHash64Lo();
List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
return candidates.stream()
.map(c -> {
Long cHi = c.getHash64Hi();
Long cLo = c.getHash64Lo();
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, cHi, cLo);
String level;
if (hamming <= 5) {
level = "DUPLICATE";
} else if (hamming <= 12) {
level = "SIMILAR";
} else {
level = "DIFFERENT";
}
return SimilarFileDTO.builder()
.fileId(c.getId())
.ownerId(c.getUserId())
.originalFileName(c.getOriginalFileName())
.fileSize(c.getFileSize())
.hammingDistance(hamming)
.similarityLevel(level)
.build();
})
.sorted((a, b) ->
Integer.compare(a.getHammingDistance(), b.getHammingDistance()))
.toList();
}
public List<SimilarFileDTO> findDuplicateByHammingDistance(String fileId, int hammingDistance,
int duplicate, int similar) {
List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
Optional<ImageHashEntity> hashOptional = hashRepository.findById(fileId);
if (hashOptional.isEmpty()) {
return Collections.emptyList();
}
var imageHashEntity = hashOptional.get();
Long hash64Hi = imageHashEntity.getHash64Hi();
Long hash64Lo = imageHashEntity.getHash64Lo();
return candidates.stream()
.map(c -> {
Long cHi = c.getHash64Hi();
Long cLo = c.getHash64Lo();
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, cHi, cLo);
return new AbstractMap.SimpleEntry<>(c, hamming);
})
.filter(entry -> entry.getValue() <= hammingDistance)
.sorted((a, b) -> Integer.compare(a.getValue(), b.getValue()))
.map(entry -> {
SimilarImageProjection similarImageProjection = entry.getKey();
int hamming = entry.getValue();
String level;
if (hamming <= duplicate) {
level = "DUPLICATE";
} else if (hamming <= similar) {
level = "SIMILAR";
} else {
level = "DIFFERENT";
}
return SimilarFileDTO.builder()
.fileId(similarImageProjection.getId())
.originalFileName(similarImageProjection.getOriginalFileName())
.fileSize(similarImageProjection.getFileSize())
.hammingDistance(hamming)
.similarityLevel(level)
.ownerId(similarImageProjection.getUserId())
.build();
})
.toList();
}
public List<SimilarImageProjection> findDuplicatedByHash(Long hash64Hi, Long hash64Lo) {
List<SimilarImageProjection> duplicates = repository.findExactDuplicates(hash64Hi,hash64Lo);
if (duplicates.isEmpty()) {
return new ArrayList<>();
}
return duplicates;
}
public Page<SimilarFileDTO> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) {
var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found"));
Long hash64Hi = imageHashEntity.getHash64Hi();
Long hash64Lo = imageHashEntity.getHash64Lo();
List<SimilarImageProjection> candidates = authToken.equals("all") ? repository.findCandidates(fileId):
repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken));
List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null)
? filter.getSimilarityLevels()
: List.of("DUPLICATE", "SIMILAR", "DIFFERENT");
List<SimilarFileDTO> allResults = candidates.stream()
.map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo))
.filter(response -> similarityLevels.contains(response.getSimilarityLevel()))
.sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance))
.collect(Collectors.toList());
int total = allResults.size();
int page = (pageable != null) ? pageable.getPageNumber() : 0;
int size = (pageable != null) ? pageable.getPageSize() : 20;
int fromIndex = Math.min(page * size, total);
int toIndex = Math.min(fromIndex + size, total);
List<SimilarFileDTO> pageContent = allResults.subList(fromIndex, toIndex);
return new PageImpl<>(pageContent, pageable, total);
}
private SimilarFileDTO createSimilarFileResponse(SimilarImageProjection similarImageProjection,
Long hash64Hi, Long hash64Lo) {
Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi();
Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo();
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo);
String level;
if (hamming <= 5) {
level = "DUPLICATE";
} else if (hamming <= 12) {
level = "SIMILAR";
} else {
level = "DIFFERENT";
}
return SimilarFileDTO.builder()
.fileId(similarImageProjection.getId())
.ownerId(similarImageProjection.getUserId())
.originalFileName(similarImageProjection.getOriginalFileName())
.fileSize(similarImageProjection.getFileSize())
.hammingDistance(hamming)
.similarityLevel(level)
.build();
}
}
@@ -0,0 +1,48 @@
package ru.soune.nocopy.service;
import com.vrt.fileprotection.image.phash.PHash;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.ImageHashRepository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Map;
@Service
@RequiredArgsConstructor
@Slf4j
public class ImageHashService {
private final ImageHashRepository repository;
public Map<String, Long> calculateHash(Path imagePath) throws IOException {
File file = imagePath.toFile();
PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file);
Long firstPart = pHash.getFirstPart();
Long secondPart = pHash.getSecondPart();
Map<String, Long> hash = Map.of(
"hi", firstPart,
"low", secondPart);
return hash;
}
public void create(FileEntity file, Map<String, Long> stringIntegerMap) {
ImageHashEntity entity = ImageHashEntity.builder()
.file(file)
.hash64Hi(stringIntegerMap.get("hi"))
.hash64Lo(stringIntegerMap.get("low"))
.hashAlgorithm("PHASH64")
.createdAt(LocalDateTime.now())
.build();
repository.save(entity);
}
}
@@ -1,72 +0,0 @@
package ru.soune.nocopy.service;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.UserContentRequest;
import ru.soune.nocopy.dto.UserContentUpdateRequest;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.entity.UserContent;
import ru.soune.nocopy.exception.ContentNotFoundException;
import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.mapper.UserContentMapper;
import ru.soune.nocopy.repository.UserContentRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
public class UserContentService {
private final UserContentRepository userContentRepository;
private final UserRepository userRepository;
private final UserContentMapper userContentMapper;
public List<UserContent> findByUser(long userId) {
return userContentRepository.findByUserId(userId);
}
@Transactional
public UserContent addUserContent(UserContentRequest userContentRequest) {
UserContent userContent = new UserContent();
Optional<User> byEmail = userRepository.findByEmail(userContentRequest.userEmail());
User user = byEmail.orElseThrow(() -> new UserNotFoundException(userContentRequest.userEmail()));
userContent.setUser(user);
userContent.setFilename(userContentRequest.fileName());
userContent.setFileExtension(userContentRequest.fileExtension());
userContent.setFilePath(userContentRequest.filePath());
userContent.setFileType(FileType.valueOf(userContentRequest.fileType()));
userContent.setOriginalFilename(userContentRequest.originalFilename());
userContent.setProtectionLevel(userContentRequest.protectionLevel());
userContent.setFileSize(userContentRequest.fileSize());
//TODO add
// @Column(name = "protection_hash", length = 64)
// private String protectionHash;
// @Column(name = "watermark_id", length = 50)
// private String watermarkId;
return userContentRepository.save(userContent);
}
@Transactional
public UserContent updateUserContent(UserContentUpdateRequest userContentRequest) {
UserContent entity = userContentRepository.findById(userContentRequest.id())
.orElseThrow(() -> new ContentNotFoundException("Content not found with: " + userContentRequest.id()));
userContentMapper.updateEntityFromDto(userContentRequest, entity);
return userContentRepository.save(entity);
}
public void deleteUserContent(long userContentId) {
userContentRepository.deleteById(userContentId);
}
}
@@ -1,69 +0,0 @@
package ru.soune.nocopy.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
public class VkOAuthService {
@Value("${auth.vk.oauth2}")
String oauthVkPath;
@Value("${auth.vk.user_info}")
String vkUserInfo;
@Value("${auth.vk.client_id}")
String clientId;
@Value("${auth.vk.redirect_uri}")
String redirectUri;
public Map<String, Object> authTokenMap(String code, String state, String codeVerifier, String deviceId) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code_verifier", codeVerifier);
params.add("redirect_uri", redirectUri);
params.add("code", code);
params.add("client_id", clientId);
params.add("device_id", deviceId);
params.add("state", state);
params.add("scope", "email,phone");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return sendRequest(params, headers,oauthVkPath);
}
public Map<String, Object> userVkInfo(String accessToken) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("access_token", accessToken);
params.add("client_id", clientId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return sendRequest(params, headers, vkUserInfo);
}
private Map<String, Object> sendRequest(MultiValueMap<String, String> params, HttpHeaders headers, String url) {
Map<String, Object> info = new HashMap<>();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST,
new HttpEntity<>(params, headers), String.class);
info.put("status", response.getStatusCodeValue());
info.put("body", response.getBody());
info.put("headers", response.getHeaders());
info.put("success", response.getStatusCode().is2xxSuccessful());
return info;
}
}
@@ -0,0 +1,37 @@
package ru.soune.nocopy.service.auth;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class CleanupTokenSessionsService {
@Autowired
private AuthTokenRepository authTokenRepository;
@Value("${file.storage.auth-token-life-hours}")
private int authTokenLife;
@Transactional
@Scheduled(fixedDelay = 30000)
public void cleanupExpiredTokens() {
LocalDateTime now = LocalDateTime.now();
List<AuthToken> expiredTokens = authTokenRepository.findByExpiresAtBefore(now);
if (!expiredTokens.isEmpty()) {
authTokenRepository.deleteAll(expiredTokens);
}
}
}
@@ -1,102 +0,0 @@
package ru.soune.nocopy.service.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.User;
import ru.soune.nocopy.entity.auth.ExternalAuthProvider;
import ru.soune.nocopy.repository.ExternalAuthProviderRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class ExternalAuthUserService {
private final UserRepository userRepository;
private final ExternalAuthProviderRepository externalAuthProviderRepository;
@Transactional
public User findOrCreateUserFromVK(String vkUserId, String fullName, String email,
String vkAccessToken, String vkRefreshToken, Integer expiresIn) {
Optional<User> existingByVkId = userRepository.findByExternalProvider(ExternalAuthProvider.ProviderType.VK,
vkUserId);
if (existingByVkId.isPresent()) {
User user = existingByVkId.get();
updateVKProvider(user, vkAccessToken, vkRefreshToken, expiresIn, email);
return user;
}
if (email != null && !email.isBlank()) {
Optional<User> existingByEmail = userRepository.findByEmail(email);
if (existingByEmail.isPresent()) {
User user = existingByEmail.get();
linkVKProvider(user, vkUserId, vkAccessToken, vkRefreshToken, expiresIn, email);
return user;
}
}
return createNewUserWithVK(vkUserId, fullName, email, vkAccessToken, vkRefreshToken, expiresIn);
}
private User createNewUserWithVK(String vkUserId, String fullName, String email,
String vkAccessToken, String vkRefreshToken, Integer expiresIn) {
User newUser = User.builder()
.fullName(fullName)
.email(email)
.password("generateRandomPassword()")
.isActive(true)
.build();
User savedUser = userRepository.save(newUser);
linkVKProvider(savedUser, vkUserId, vkAccessToken, vkRefreshToken, expiresIn, email);
return savedUser;
}
private void linkVKProvider(User user, String vkUserId, String vkAccessToken, String vkRefreshToken,
Integer expiresIn, String email) {
ExternalAuthProvider provider = ExternalAuthProvider.builder()
.user(user)
.providerType(ExternalAuthProvider.ProviderType.VK)
.providerUserId(vkUserId)
.accessToken(vkAccessToken)
.refreshToken(vkRefreshToken)
.tokenExpiresAt(LocalDateTime.now().plusSeconds(expiresIn))
.email(email)
.isActive(true)
.build();
externalAuthProviderRepository.save(provider);
user.getExternalAuthProviders().add(provider);
}
private void updateVKProvider(User user, String vkAccessToken, String vkRefreshToken,
Integer expiresIn, String email) {
Optional<ExternalAuthProvider> existingProvider = user.getProvider(ExternalAuthProvider.ProviderType.VK);
if (existingProvider.isPresent()) {
ExternalAuthProvider provider = existingProvider.get();
provider.setAccessToken(vkAccessToken);
provider.setTokenExpiresAt(LocalDateTime.now().plusSeconds(expiresIn));
provider.setEmail(email);
provider.setRefreshToken(vkRefreshToken);
provider.setLastUsedAt(LocalDateTime.now());
externalAuthProviderRepository.save(provider);
}
}
private String generateRandomPassword() {
return java.util.UUID.randomUUID().toString();
}
}
@@ -9,16 +9,23 @@ import ru.soune.nocopy.dto.file.FileResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@@ -28,7 +35,13 @@ public class FileEntityService {
private final FileEntityRepository fileEntityRepository;
@Transactional
private final ImageHashService imageHashService;
private final FileSimilarityService fileSimilarityService;
private final ImageHashRepository imageHashRepository;
@Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId());
@@ -39,6 +52,21 @@ public class FileEntityService {
throw new IOException("File not found on disk: " + filePath);
}
Map<String, Long> imageHash = Map.of();
if (session.getFileType().startsWith("image")) {
imageHash = imageHashService.calculateHash(filePath);
List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
imageHash.get("hi"), imageHash.get("low"));
if (!duplicatedByHash.isEmpty()) {
SimilarImageProjection similarImageProjection = duplicatedByHash.get(0);
throw new DuplicateImageException("Duplicate", similarImageProjection.getId(),
similarImageProjection.getUserId());
}
}
long fileSize = Files.size(filePath);
String originalName = session.getFileName();
String storedName = filePath.getFileName().toString();
@@ -57,11 +85,12 @@ public class FileEntityService {
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
log.info("FileEntity created successfully: {} (size: {} bytes)",
saved.getId(), fileSize);
if (!imageHash.isEmpty()) {
imageHashService.create(saved, imageHash);
}
return saved;
} catch (IOException e) {
log.error("Failed to create FileEntity for session {}: {}",
session.getUploadId(), e.getMessage(), e);
@@ -69,15 +98,6 @@ public class FileEntityService {
}
}
private String extractFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1).toLowerCase();
}
return "";
}
@Transactional(readOnly = true)
public FileEntityResponse getById(String fileId, int version) {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
@@ -168,24 +188,45 @@ public class FileEntityService {
}
@Transactional
public void markAsDeleted(FileEntity fileEntity) {
public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException {
fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
Path path = Paths.get(fileEntity.getProtectedFilePath());
Files.deleteIfExists(path);
fileEntity.setProtectedFilePath("");
return fileEntityRepository.save(fileEntity);
}
public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException {
if (fileEntity.getImageHash() != null) {
fileEntity.setImageHash(null);
}
fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
Path path = Paths.get(fileEntity.getProtectedFilePath());
Files.deleteIfExists(path);
fileEntity.setProtectedFilePath("");
fileEntityRepository.save(fileEntity);
}
@Transactional
public boolean deleteFromDisk(FileEntity fileEntity) throws IOException {
public void deleteFromDisk(FileEntity fileEntity) throws IOException {
Path path = Paths.get(fileEntity.getFilePath());
if (!Files.exists(path)) {
return true;
return;
}
Files.delete(path);
return true;
fileEntityRepository.delete(fileEntity);
}
@Transactional(readOnly = true)
@@ -194,6 +235,99 @@ public class FileEntityService {
return totalSize != null ? totalSize : 0L;
}
public void changeStatus(ProtectionStatus newStatus, String fileId) {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
fileEntity.setProtectionStatus(newStatus);
fileEntityRepository.save(fileEntity);
}
@Transactional
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(id)
.orElseThrow(() -> new RuntimeException("File not found: " + id));
String extension = determineFileExtension(fileExt, fileEntity);
Path protectedFilePath = prepareProtectedPath(fileEntity, extension);
if (Files.exists(protectedFilePath)) {
Files.delete(protectedFilePath);
}
Files.write(protectedFilePath, data);
fileEntity.setProtectedFilePath(protectedFilePath.toString());
fileEntity.setProtectedAt(LocalDateTime.now());
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setFileExtension(extension);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
fileEntityRepository.save(fileEntity);
}
public FileEntity findBySignature(String signature) {
return fileEntityRepository.findBySignature(signature);
}
public String findFileIdByPath(String filePath) {
return fileEntityRepository.findFileIdByFilePath(filePath);
}
public File getFileById(String id) {
try {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
File file = new File(fileEntity.getFilePath());
if (!file.exists()) {
throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath());
}
return file;
} catch (Exception e) {
log.error("Error getting file: {}", id, e);
return null;
}
}
public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException {
Path originalPath = Paths.get(fileEntity.getFilePath());
String pathStr = originalPath.toString();
pathStr = pathStr.replaceFirst("/uploads/uploads/", "/uploads/protected/");
Path protectedPath = Paths.get(pathStr);
String fileName = protectedPath.getFileName().toString();
if (extension != null) {
String nameWithoutExt = fileName;
int lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex > 0) {
nameWithoutExt = fileName.substring(0, lastDotIndex);
}
fileName = nameWithoutExt + "." + extension;
protectedPath = protectedPath.getParent().resolve(fileName);
}
Files.createDirectories(protectedPath.getParent());
return protectedPath;
}
public void updateSignature(String signature, String fileId) throws IOException {
FileEntity fileEntity = fileEntityRepository.findByFileId(fileId);
fileEntity.setSignature(signature);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectedAt(LocalDateTime.now());
fileEntity.setProtectedFilePath(prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString());
fileEntityRepository.save(fileEntity);
}
private boolean checkFileExistsOnDisk(String filePath) {
try {
return Files.exists(Paths.get(filePath));
@@ -203,6 +337,14 @@ public class FileEntityService {
}
}
private String determineFileExtension(String fileExt, FileEntity fileEntity) {
if (fileExt != null && !fileExt.trim().isEmpty()) {
return fileExt;
} else {
return fileEntity.getFileExtension();
}
}
private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
@@ -223,6 +365,8 @@ public class FileEntityService {
.formattedSize(formatFileSize(fileEntity.getFileSize()))
.downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId())
.existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString())
.build();
}
@@ -0,0 +1,53 @@
package ru.soune.nocopy.service.file;
import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.util.FileUtil;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class FileProcessingOrchestrator {
private final NoCopyFileService noCopyFileService;
private final FileEntityRepository fileRepository;
private final FileUtil fileUtil;
public void initializeProcessingQueue() {
List<FileEntity> filesToProtect = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED);
for (FileEntity fileEntity : filesToProtect) {
try {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
noCopyFileService.addFile(fileInfo);
log.info("Add to query: {}", fileEntity.getOriginalFileName());
} catch (Exception e) {
log.error("Fail add to query: {}", fileEntity.getId(), e);
}
}
}
@Scheduled(fixedDelay = 120000)
public void checkNewFilesForProtection() {
List<FileEntity> newFiles = fileRepository.findAllActiveFilesAndNotProtected();
for (FileEntity fileEntity : newFiles) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
noCopyFileService.addFile(fileInfo);
}
}
}
@@ -6,6 +6,7 @@ import ru.soune.nocopy.dto.file.FileInfoUserResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.util.List;
@@ -26,23 +27,37 @@ public class FileStatsService {
.fileCount(calculateTotalCount(files))
.filesCheck(calculateByStatus(files, FileStatus.CHECKED))
.filesViolation(calculateByStatus(files, FileStatus.VIOLATION))
.protectedFilesCount(protectedUserFiles(files))
.imagesSize(calculateMediaSize(files, FileType.IMAGE))
.imagesCount(calculateMediaCount(files, FileType.IMAGE))
.imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED))
.imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION))
.protectedImageFilesCount(protectedUserFiles(files, FileType.IMAGE))
.videosSize(calculateMediaSize(files, FileType.VIDEO))
.videosCount(calculateMediaCount(files, FileType.VIDEO))
.videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED))
.videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION))
.protectedVideoFilesCount(protectedUserFiles(files, FileType.VIDEO))
.audiosSize(calculateMediaSize(files, FileType.AUDIO))
.audiosCount(calculateMediaCount(files, FileType.AUDIO))
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO))
.build();
}
private Long protectedUserFiles(List<FileEntity> files) {
return files.stream().filter(file -> file.getProtectionStatus() == ProtectionStatus.PROTECTED).count();
}
private Long protectedUserFiles(List<FileEntity> files, FileType type) {
return files.stream().filter(file -> file.getProtectionStatus() == ProtectionStatus.PROTECTED &&
file.getMimeType().equals(type.getDisplayName())).count();
}
private Long calculateTotalSize(List<FileEntity> files) {
return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED)
@@ -10,14 +10,13 @@ public interface FileUploadService {
FileUploadSession initUpload(Long userId, String fileName,
String fileType, String extension, long fileSize);
UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile) throws IOException;
void handleExpiredSession(FileUploadSession session);
void cancelUpload(String uploadId);
UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, Integer findSimilar);
UploadProgressResponse getUploadProgress(String uploadId);
void handleExpiredSession(FileUploadSession session);
public void retryFailedUpload(String uploadId);
public void cancelUpload(String uploadId);
void completeFileProcessingAsync(FileUploadSession session);
}
@@ -0,0 +1,35 @@
package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
@Service
@RequiredArgsConstructor
public class NotExistFilesCleanupService {
@Autowired
private FileEntityRepository fileEntityRepository;
@Transactional
@Scheduled(fixedDelay = 30000)
public void cleanUpNotExistFiles() {
List<FileEntity> fileEntities = fileEntityRepository.searchFileEntityByStatus(FileStatus.DELETED);
for (FileEntity fileEntity : fileEntities) {
boolean exists = Files.exists(Paths.get(fileEntity.getFilePath()));
if (!exists) {
fileEntityRepository.delete(fileEntity);
}
}
}
}
@@ -0,0 +1,44 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.AudioFilePathProvider;
import com.vrt.fileprotection.FileProtector;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import java.io.IOException;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class AudioFilePathProviderImpl implements AudioFilePathProvider {
private final FileEntityService fileEntityService;
private final FileEntityRepository fileEntityRepository;
@Override
public @NotNull String providePath(@NotNull FileProtector.FileInfo fileInfo) {
String filePath;
Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileInfo.getId());
if (optionalFileEntity.isEmpty()) {
throw new NullPointerException();
}
FileEntity fileEntity = optionalFileEntity.get();
try {
filePath = fileEntityService.prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
return filePath;
}
}
@@ -0,0 +1,42 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.audio.AudioLocalSearch;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.util.FileUtil;
import java.io.IOException;
@Slf4j
@Service
@RequiredArgsConstructor
public class AudioLocalSearchImpl implements AudioLocalSearch {
private final FileEntityService fileEntityService;
private final FileUtil fileUtil;
@Override
public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) {
FileEntity fileEntity = fileEntityService.findBySignature(signature);
return fileUtil.createFileInfo(fileEntity);
}
@Override
public void updateSignature(@NotNull String signature, @NotNull FileProtector.FileInfo fileInfo) {
try {
fileEntityService.updateSignature(signature, fileInfo.getId());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

Some files were not shown because too many files have changed in this diff Show More