45 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 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 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
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 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 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 20e1ab321d NCBACK-25 work version for protect file
Test Workflow / test (push) Successful in 3s
2026-01-13 13:28:09 +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
95 changed files with 2275 additions and 730 deletions
+9
View File
@@ -1,10 +1,19 @@
# Postgres
POSTGRES_DB=no_copy_ POSTGRES_DB=no_copy_
POSTGRES_USER=ncp_db POSTGRES_USER=ncp_db
POSTGRES_PASSWORD=ncpDbApp POSTGRES_PASSWORD=ncpDbApp
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
# Redis
REDIS_HOST=redis REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
# Backend server
SERVER_PORT=8080 SERVER_PORT=8080
# Postfix
MAIL_HOST_PROD=postfix-production
MAIL_PORT_PROD=25
MAIL_USERNAME_PROD=noreply@nocopy.com
SMTP_PASSWORD_PROD=nocopy!nocopy!
+10
View File
@@ -21,6 +21,9 @@ configurations {
repositories { repositories {
mavenCentral() mavenCentral()
flatDir {
dirs 'libs'
}
} }
dependencies { dependencies {
@@ -30,6 +33,10 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto:6.5.3' implementation 'org.springframework.security:spring-security-crypto:6.5.3'
implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'commons-validator:commons-validator:1.7' 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 'org.flywaydb:flyway-core:9.22.0'
@@ -51,6 +58,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.3.1' testImplementation 'org.mockito:mockito-core:5.3.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation name: 'testlib-fat-0.2.1-all'
} }
tasks.named('test') { tasks.named('test') {
+39 -3
View File
@@ -17,6 +17,29 @@ services:
command: tail -f /dev/null command: tail -f /dev/null
restart: unless-stopped 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: db:
image: postgres:17.7 image: postgres:17.7
restart: always restart: always
@@ -30,6 +53,8 @@ services:
memory: 1G memory: 1G
environment: environment:
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_SHARED_BUFFERS: 512MB POSTGRES_SHARED_BUFFERS: 512MB
@@ -52,15 +77,19 @@ services:
resources: resources:
limits: limits:
cpus: '1.5' cpus: '1.5'
memory: 1G memory: 3G
reservations: reservations:
cpus: '0.5' cpus: '0.5'
memory: 512M memory: 2G
environment: 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 FILE_STORAGE_PATH: /data/uploads
MAX_FILE_SIZE: 10737418240 MAX_FILE_SIZE: 10737418240
# FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000 FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
@@ -70,6 +99,13 @@ services:
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" 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: depends_on:
- db - db
ports: ports:
@@ -238,7 +274,7 @@ volumes:
loki_rules: loki_rules:
uploads_data: uploads_data:
prometheus_data: prometheus_data:
# postfix-data:
networks: networks:
app-network: app-network:
external: true external: true
+58 -50
View File
@@ -17,6 +17,7 @@ pipeline {
stages { stages {
stage('Git pull') { stage('Git pull') {
steps { steps {
cleanWs()
script { script {
checkout([ checkout([
$class: 'GitSCM', $class: 'GitSCM',
@@ -30,7 +31,6 @@ pipeline {
} }
} }
stage('Deploy with docker-compose') { stage('Deploy with docker-compose') {
steps { steps {
script { script {
@@ -44,68 +44,76 @@ pipeline {
string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD') string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD')
]) { ]) {
sh """ sh """
echo "Deploying branch: ${params.BRANCH}" echo "Deploying branch: ${params.BRANCH}"
echo "Copying files to server..." 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' 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 " sshpass -p '$SSH_PASS' rsync -av --delete \\
cd /opt/deployments/${params.BRANCH} --exclude=.git \\
--exclude=.gradle \\
--exclude=build \\
--exclude=**/build \\
--exclude=.idea \\
--exclude=out \\
. $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/
echo '1. Остановка старого приложения...' sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "
docker stop app-backend 2>/dev/null || echo 'Контейнер не найден' cd /opt/deployments/${params.BRANCH}
docker rm app-backend 2>/dev/null || echo 'Контейнер не найден'
echo '2. Удаление старых образов...' echo '1. Остановка старого приложения...'
docker images --filter 'reference=*app*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'Нет образов для удаления' docker stop app-backend 2>/dev/null || echo 'Контейнер не найден'
docker rm app-backend 2>/dev/null || echo 'Контейнер не найден'
echo '3. Создание сети если нужно...' echo '2. Удаление старых образов...'
docker network create app-network 2>/dev/null || echo 'Сеть уже существует' docker images --filter 'reference=*app*' -q | xargs -r docker rmi -f 2>/dev/null || echo 'Нет образов для удаления'
echo '4. Запуск инфраструктуры...' echo '3. Создание сети если нужно...'
docker-compose up -d db storage docker network create app-network 2>/dev/null || echo 'Сеть уже существует'
echo '5. Ожидание PostgreSQL...' echo '4. Запуск инфраструктуры...'
sleep 10 docker-compose up -d db storage
echo '6. Сборка нового образа приложения...' echo '5. Ожидание PostgreSQL...'
docker build --no-cache -t app-backend:latest . sleep 10
echo '7. Запуск приложения...' echo '6. Сборка нового образа приложения...'
docker run -d \\ docker build --no-cache -t app-backend:latest .
--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. Запуск мониторинга...' echo '7. Запуск приложения...'
docker-compose up -d grafana prometheus loki tempo alloy 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 '9. Проверка...' echo '8. Запуск мониторинга...'
sleep 5 docker-compose up -d grafana prometheus loki tempo alloy
echo 'Статус контейнеров:' echo '9. Проверка...'
docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}' sleep 5
echo '10. Проверка health...' echo 'Статус контейнеров:'
if curl -s -f http://localhost:80/health > /dev/null 2>&1; then docker ps --format 'table {{.Names}}\\t{{.Image}}\\t{{.Status}}'
echo 'Приложение работает'
echo 'URL: http://${params.SERVER}:80' echo '10. Проверка health...'
else if curl -s -f http://localhost:80/health > /dev/null 2>&1; then
echo 'Проверка health не удалась' echo 'Приложение работает'
docker logs app-backend --tail=20 echo 'URL: http://${params.SERVER}:80'
fi else
" echo 'Проверка health не удалась'
""" docker logs app-backend --tail=20
fi
"
"""
} }
} }
} }
Binary file not shown.
-2
View File
@@ -16,8 +16,6 @@ dependencies {
implementation("io.insert-koin:koin-core:4.1.1") implementation("io.insert-koin:koin-core:4.1.1")
implementation("io.insert-koin:koin-core-jvm:4.1.1") implementation("io.insert-koin:koin-core-jvm:4.1.1")
testImplementation(kotlin("test"))
} }
tasks.test { tasks.test {
@@ -1,17 +1,49 @@
package ru.soune.nocopy.configuration; 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.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Collections;
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@AllArgsConstructor
public class ApplicationConfig { public class ApplicationConfig {
@Bean @Bean
PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); 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
);
}
} }
@@ -18,6 +18,7 @@ public class HandlerConfig {
FileEntityHandler file, FileEntityHandler file,
LogoutRequestHandler logoutHandler, LogoutRequestHandler logoutHandler,
ImageFoundRequestHandler imageFoundRequestHandler, ImageFoundRequestHandler imageFoundRequestHandler,
VerifyRegisterUserHandler verifyRegisterUser,
AuthRequestHandler authRequestHandler AuthRequestHandler authRequestHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
@@ -28,6 +29,7 @@ public class HandlerConfig {
map.put(20006, logoutHandler); map.put(20006, logoutHandler);
map.put(20007, imageFoundRequestHandler); map.put(20007, imageFoundRequestHandler);
map.put(20008, authRequestHandler); map.put(20008, authRequestHandler);
map.put(20009, verifyRegisterUser);
return map; return map;
} }
@@ -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,5 +1,8 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -17,23 +20,22 @@ import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.RegAnswer;
import ru.soune.nocopy.dto.file.*; 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.FileEntity;
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.file.FileStatus; 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.entity.file.UploadStatus;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.*; import ru.soune.nocopy.handler.*;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.util.FileUtil;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -61,6 +63,10 @@ public class ApiController {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final NoCopyFileService noCopyFileService;
private final FileUtil fileUtil;
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@PathVariable("version") int version) { @PathVariable("version") int version) {
@@ -82,6 +88,9 @@ public class ApiController {
return ResponseEntity.ok().body(response); return ResponseEntity.ok().body(response);
} catch (ValidationException e) { } catch (ValidationException e) {
return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); 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) { } catch (NotFoundAuthToken e) {
return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>())); MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>()));
@@ -104,7 +113,8 @@ public class ApiController {
@PathVariable("version") int version, @PathVariable("version") int version,
@RequestParam(value = "upload_id", required = false) String uploadId, @RequestParam(value = "upload_id", required = false) String uploadId,
@RequestParam(value = "chunk_number", required = false) Integer chunkNumber, @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 { try {
if (chunk == null || chunk.isEmpty()) { if (chunk == null || chunk.isEmpty()) {
return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty");
@@ -116,13 +126,27 @@ public class ApiController {
return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required"); return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required");
} }
fileUploadService.uploadChunk(uploadId, chunkNumber, chunk); UploadProgressResponse uploadProgressResponse = fileUploadService.uploadChunk(uploadId, chunkNumber,
chunk, findSimilar);
return buildSuccessResponse(uploadId, chunkNumber, chunk); return buildSuccessResponse(uploadId, chunkNumber, chunk,
fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath()));
} catch (DuplicateImageException e) { } catch (DuplicateImageException e) {
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.DUPLICATE_FILE_UPLOAD.getCode(), Map<String, Object> duplicateData = new HashMap<>();
duplicateData.put("duplicateFileId", e.duplicateFileId());
duplicateData.put("userId", e.userId());
duplicateData.put("message", e.getMessage());
if (uploadId != null) {
duplicateData.put("uploadId", uploadId);
}
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(),
Map.of("duplicateOwnerId", e.userId(), "duplicateFileId", e.duplicateFileId()))); duplicateData
));
} catch (Exception e) { } catch (Exception e) {
log.error("Error uploading chunk", e); log.error("Error uploading chunk", e);
return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage());
@@ -133,22 +157,23 @@ public class ApiController {
public ResponseEntity<BaseResponse> findSimilarFiles( public ResponseEntity<BaseResponse> findSimilarFiles(
@PathVariable("version") int version, @PathVariable("version") int version,
@PathVariable String fileId, @PathVariable String fileId,
@RequestParam(value = "auth_token",required = false) String authToken,
@RequestParam(required = false) List<String> similarityLevels, @RequestParam(required = false) List<String> similarityLevels,
@PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) {
SimilarityFilter filter = SimilarityFilter.builder() SimilarityFilter filter = SimilarityFilter.builder()
.similarityLevels(similarityLevels) .similarityLevels(similarityLevels)
.build(); .build();
Page<SimilarFileResponse> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable); Page<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken);
String messageDesc; String messageDesc;
MessageCode success; MessageCode success;
if (similarFiles.isEmpty()) { if (similarFiles.isEmpty()) {
messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); messageDesc = MessageCode.FILE_NOT_FOUND.getDescription();
success = MessageCode.FILE_NOT_FOUND; success = MessageCode.SUCCESS;
} else { } else {
messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription();
success = MessageCode.SIMILAR_FILES_FOUND; success = MessageCode.SUCCESS;
} }
Map<String, Object> responseData = new HashMap<>(); Map<String, Object> responseData = new HashMap<>();
@@ -194,7 +219,8 @@ public class ApiController {
.uploadId(uploadId) .uploadId(uploadId)
.build(); .build();
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), return ResponseEntity.ok().body(new BaseResponse(
20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get upload progress: " + e.getMessage(), responseBody)); "Failed to get upload progress: " + e.getMessage(), responseBody));
} }
} }
@@ -353,6 +379,45 @@ 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 { private ResponseEntity<BaseResponse> checkForDuplicates(String uploadId) throws IOException {
Optional<FileEntity> uploadedFile = fileEntityRepository.findByUploadSessionId(uploadId); Optional<FileEntity> uploadedFile = fileEntityRepository.findByUploadSessionId(uploadId);
@@ -361,7 +426,7 @@ public class ApiController {
} }
FileEntity fileEntity = uploadedFile.get(); FileEntity fileEntity = uploadedFile.get();
List<SimilarFileResponse> similarFiles = fileSimilarityService.findSimilarFiles(fileEntity.getId()); List<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileEntity.getId());
if (hasDuplicate(similarFiles)) { if (hasDuplicate(similarFiles)) {
return handleDuplicate(fileEntity, similarFiles); return handleDuplicate(fileEntity, similarFiles);
@@ -370,15 +435,15 @@ public class ApiController {
return null; return null;
} }
private boolean hasDuplicate(List<SimilarFileResponse> similarFiles) { private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5); return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5);
} }
private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileResponse> similarFiles) private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileDTO> similarFiles)
throws IOException { throws IOException {
fileEntityService.deleteFromDisk(fileEntity); fileEntityService.deleteFromDisk(fileEntity);
fileEntityService.markAsDeleted(fileEntity); fileEntityService.softDeleteFileWithHash(fileEntity);
Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId()); Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId());
@@ -397,11 +462,13 @@ public class ApiController {
return null; return null;
} }
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk) { private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder() ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId) .uploadId(uploadId)
.chunkNumber(chunkNumber) .chunkNumber(chunkNumber)
.chunkSize(chunk.getSize()) .chunkSize(chunk.getSize())
.fileId(fileId)
.message("Chunk uploaded successfully") .message("Chunk uploaded successfully")
.build(); .build();
@@ -460,16 +527,4 @@ public class ApiController {
} }
return contentType; 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");
}
} }
@@ -3,6 +3,9 @@ package ru.soune.nocopy.controller;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController @RestController
@RequestMapping("check/api") @RequestMapping("check/api")
public class HealtCheckController { public class HealtCheckController {
@@ -11,4 +14,22 @@ public class HealtCheckController {
public HttpStatus healtCheck() { public HttpStatus healtCheck() {
return HttpStatus.OK; 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 lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.UserRequest; import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.entity.User; 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.InvalidUserEmail;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.mapper.UserMapper; import ru.soune.nocopy.mapper.UserMapper;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; 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.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@RestController @RestController
@@ -31,10 +40,14 @@ public class UserController {
private final UserMapper userMapper; private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final AuthService authService;
@GetMapping("/all") @GetMapping("/all")
public ResponseEntity<List<UserDTO>> getAllUsers() { public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> allUsers = userRepository.findAll().stream() 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.getPhone(), u.getGenderType(),
u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType()))
.toList(); .toList();
@@ -52,19 +65,19 @@ public class UserController {
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found"));
if (authToken != null) { if (authToken != null) {
Optional<User> user = userRepository.findByEmail(email); User user = userRepository.findByEmail(email);
//TODO add mapper //TODO add mapper
if (user.isPresent()) { if (user != null) {
UserDTO userDTO = userMapper.toDTO(user.get()); UserDTO userDTO = userMapper.toDTO(user);
userDTO.setEmail(email); userDTO.setEmail(email);
userDTO.setFullName(user.get().getFullName()); userDTO.setFullName(user.getFullName());
userDTO.setCompany(user.get().getCompany()); userDTO.setCompany(user.getCompany());
userDTO.setPhone(user.get().getPhone()); userDTO.setPhone(user.getPhone());
userDTO.setGenderType(user.get().getGenderType()); userDTO.setGenderType(user.getGenderType());
userDTO.setBirthday(user.get().getBirthday()); userDTO.setBirthday(user.getBirthday());
userDTO.setCreatedAt(user.get().getCreatedAt()); userDTO.setCreatedAt(user.getCreatedAt());
userDTO.setSubscriptionType(user.get().getSubscriptionType()); userDTO.setSubscriptionType(user.getSubscriptionType());
userDTO.setActive(user.get().getIsActive()); userDTO.setActive(user.isActive());
return ResponseEntity.ok(userDTO); return ResponseEntity.ok(userDTO);
} }
@@ -108,4 +121,50 @@ public class UserController {
return ResponseEntity.ok(userService.updateUser(userRequest, user)); 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,10 +0,0 @@
package ru.soune.nocopy.dto;
import java.time.LocalDateTime;
public record AuthResponse (
boolean success,
String message,
String token,
LocalDateTime expiresAt
) {}
@@ -6,10 +6,13 @@ public enum MessageCode {
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
INVALID_FIELD(2, "Invalid field"), INVALID_FIELD(2, "Invalid field"),
INVALID_TOKEN(2, "Invalid token"), INVALID_TOKEN(2, "Invalid token"),
TOKEN_IS_ALIVE(2, "Token is alive"),
INVALID_ACTION(2, "Invalid action"), INVALID_ACTION(2, "Invalid action"),
FILE_UPLOAD_ERROR(2, "File upload error"), FILE_UPLOAD_ERROR(2, "File upload error"),
DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"),
FILE_DOWNLOAD_ERROR(2, "File download error"), 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"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
IMAGE_FOUND_ERROR(2, "Image found error"), IMAGE_FOUND_ERROR(2, "Image found error"),
INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
@@ -23,6 +26,7 @@ public enum MessageCode {
AUTH_TOKEN_NOT_FOUND(4, "Token not found"), AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
FILE_NOT_FOUND(4, "File 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"); SIMILAR_FILES_FOUND(0, "Similar files found");
private final Integer code; private final Integer code;
@@ -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") @JsonProperty("chunk_size")
private Long chunkSize; private Long chunkSize;
@JsonProperty("file_id")
private String fileId;
@JsonProperty("message") @JsonProperty("message")
private String message; private String message;
} }
@@ -17,6 +17,9 @@ public class FileEntityRequest {
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@JsonProperty("full_delete")
private Integer fullDelete;
@JsonProperty("upload_session_id") @JsonProperty("upload_session_id")
private String uploadSessionId; private String uploadSessionId;
@@ -31,4 +31,5 @@ public class FileEntityResponse {
private String downloadUrl; private String downloadUrl;
private boolean existsOnDisk; private boolean existsOnDisk;
private Integer supportId; private Integer supportId;
private String protectStatus;
} }
@@ -58,4 +58,16 @@ public class FileInfoUserResponse {
@JsonProperty("audios_violations") @JsonProperty("audios_violations")
private Integer audiosViolations; 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;
}
}
@@ -5,10 +5,11 @@ import lombok.Value;
@Value @Value
@Builder @Builder
public class SimilarFileResponse { public class SimilarFileDTO {
String fileId; String fileId;
String originalFileName; String originalFileName;
Long fileSize; Long fileSize;
Integer hammingDistance; Integer hammingDistance;
String similarityLevel; String similarityLevel;
Long ownerId;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -13,5 +13,9 @@ import java.util.Map;
public class LoginAnswer { public class LoginAnswer {
private String Token; private String Token;
private boolean isActive;
private boolean isVerified;
private List<Map<String, String>> fieldErrors; private List<Map<String, String>> fieldErrors;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; 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.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -13,5 +13,13 @@ import java.util.Map;
public class RegAnswer { public class RegAnswer {
private String Token; private String Token;
private boolean isActive;
private boolean isVerified;
private Long userId;
private String email;
private List<Map<String, String>> fieldErrors; private List<Map<String, String>> fieldErrors;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
@@ -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.Email;
import jakarta.validation.constraints.NotBlank; 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.Email;
import jakarta.validation.constraints.NotBlank; 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.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.SubscriptionType; import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,11 +1,11 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.user;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import ru.soune.nocopy.entity.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import java.time.LocalDate; import java.time.LocalDate;
@@ -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,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.file;
import lombok.Getter; import lombok.Getter;
@@ -66,6 +66,20 @@ public class FileEntity {
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; 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) @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
private ImageHashEntity imageHash; private ImageHashEntity imageHash;
@@ -75,9 +89,19 @@ public class FileEntity {
if (this.status == null) { if (this.status == null) {
this.status = FileStatus.ACTIVE; this.status = FileStatus.ACTIVE;
} }
if (this.protectionStatus == null) {
this.protectionStatus = ProtectionStatus.NOT_PROTECTED;
}
if (this.createdAt == null) { if (this.createdAt == null) {
this.createdAt = LocalDateTime.now(); this.createdAt = LocalDateTime.now();
} }
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
} }
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
} }
@@ -7,10 +7,11 @@ import java.util.List;
@Getter @Getter
public enum FileType { public enum FileType {
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")), // 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", VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
"3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")), "3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")),
AUDIO("audio", Arrays.asList("mp3", "wav", "flac")); AUDIO("audio", List.of("wav"));
private final String displayName; private final String displayName;
private final List<String> allowedExtensions; private final List<String> allowedExtensions;
@@ -9,7 +9,11 @@ import lombok.NoArgsConstructor;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Entity @Entity
@Table(name = "image_hashes") @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 @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@@ -26,10 +30,10 @@ public class ImageHashEntity {
private FileEntity file; private FileEntity file;
@Column(name = "hash64_hi") @Column(name = "hash64_hi")
private Integer hash64Hi; private Long hash64Hi;
@Column(name = "hash64_lo") @Column(name = "hash64_lo")
private Integer hash64Lo; private Long hash64Lo;
@Column(name = "hash_algorithm", nullable = false) @Column(name = "hash_algorithm", nullable = false)
private String hashAlgorithm; private String hashAlgorithm;
@@ -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 jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@@ -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 { public enum GenderType {
MALE, FEMALE MALE, FEMALE
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import lombok.Getter; 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 com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
@@ -6,6 +6,7 @@ import jakarta.validation.constraints.Size;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.Violation;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -33,6 +34,9 @@ public class User {
@Column(name = "email", nullable = false, length = 1024, unique = true) @Column(name = "email", nullable = false, length = 1024, unique = true)
private String email; private String email;
@Column(name = "email_verified")
private boolean emailVerified = false;
@Column(name = "company") @Column(name = "company")
private String company; private String company;
@@ -64,7 +68,7 @@ public class User {
private LocalDateTime lastLoginAt; private LocalDateTime lastLoginAt;
@Column(name = "is_active") @Column(name = "is_active")
private Boolean isActive = true; private boolean isActive = false;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@@ -79,15 +83,4 @@ public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<UserAction> userActions = new ArrayList<>(); private List<Violation> violations = 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<>();
}
@@ -1,9 +1,10 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.ContentStatus;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -9,7 +9,7 @@ import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.TokenProcessRequest; import ru.soune.nocopy.dto.TokenProcessRequest;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import java.util.Map; import java.util.Map;
@@ -11,7 +11,8 @@ import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.auth.AuthService; 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.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService; import ru.soune.nocopy.service.file.FileStatsService;
@@ -33,6 +34,8 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final ImageHashRepository imageHashRepository;
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
try { try {
@@ -272,15 +275,21 @@ public class FileEntityHandler implements RequestHandler {
FileEntity fileEntity = fileEntityRepository.findById(fileId) FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(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); fileEntityService.deleteFromDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
.message("File deleted from disk") .message("File deleted from disk")
.build(); .build();
} else { } else {
fileEntityService.markAsDeleted(fileEntity); fileEntityService.softDeleteFileWithHash(fileEntity);
//
// imageHashRepository.deleteByFileId(fileEntity.getId());
//
// fileEntityService.markAsDeleted(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
.message("File marked as deleted") .message("File marked as deleted")
@@ -10,14 +10,14 @@ import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.validator.FileUploadRequestValidator; import ru.soune.nocopy.handler.validator.FileUploadRequestValidator;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import java.util.*; import java.util.*;
@@ -3,14 +3,14 @@ package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.service.YandexSearchService; import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService;
@Slf4j @Slf4j
@Component @Component
@@ -21,6 +21,8 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final YandexSearchService yandexSearchService; private final YandexSearchService yandexSearchService;
private final GoogleVisionSearchService googleVisionSearchService;
@Override @Override
public BaseResponse handle(BaseRequest request) throws Exception { public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -30,6 +32,9 @@ public class ImageFoundRequestHandler implements RequestHandler {
YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId); YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId);
//TODO uncommited when add billing
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), response); MessageCode.SUCCESS.getDescription(), response);
} }
@@ -4,10 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.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.exception.NotValidFieldException;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -25,8 +28,9 @@ public class LoginRequestHandler implements RequestHandler {
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
LoginRequest loginRequest = objectMapper.convertValue(request.getMessageBody(), LoginRequest.class); 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 loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail()))); loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail())));
@@ -35,6 +39,17 @@ public class LoginRequestHandler implements RequestHandler {
MessageCode.AUTH_EMAIL_NOT_FOUND.getDescription(), loginAnswer)); 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); AuthToken authToken = authService.login(loginRequest);
String token = authToken.getToken(); String token = authToken.getToken();
@@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.dto.register.LogOutAnswer;
import ru.soune.nocopy.entity.User; 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.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -31,16 +33,16 @@ public class LogoutRequestHandler implements RequestHandler {
LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class); LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class);
Optional<AuthToken> opToken = authTokenRepository.findByToken(logoutRequest.getToken()); 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", throw new NotValidFieldException("User with email or token not found",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(), new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(),
new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken()))); 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", throw new NotValidFieldException("User token mismatch with user",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(), new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(),
MessageCode.AUTH_TOKEN_MISMATCH.getDescription(), MessageCode.AUTH_TOKEN_MISMATCH.getDescription(),
@@ -7,12 +7,16 @@ import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.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.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator; import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -29,6 +33,8 @@ public class RegRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EmailService emailService;
@Override @Override
public BaseResponse handle(BaseRequest request) throws ValidationException { public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -45,6 +51,7 @@ public class RegRequestHandler implements RequestHandler {
} }
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult); regRequestValidator.validate(regRequest, bindingResult);
if (bindingResult.hasErrors()) { if (bindingResult.hasErrors()) {
@@ -52,11 +59,26 @@ public class RegRequestHandler implements RequestHandler {
} }
AuthToken authToken = authService.register(regRequest); 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(); String token = authToken.getToken();
authService.useUserAuthToken(token); authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer(); RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(token); regAnswer.setVerified(false);
regAnswer.setActive(false);
regAnswer.setUserId(authToken.getUser().getId());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); 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,14 +1,11 @@
package ru.soune.nocopy.handler.validator; package ru.soune.nocopy.handler.validator;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.EmailValidator; import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; 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.*; import java.util.*;
@Component @Component
@@ -141,15 +138,6 @@ public class RegRequestValidator implements Validator {
errors.rejectValue("password", "password.contains.spaces", errors.rejectValue("password", "password.contains.spaces",
"Password cannot contain 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) { private void checkPasswordComplexity(String password, Errors errors) {
@@ -4,8 +4,8 @@ import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserContentUpdateRequest; import ru.soune.nocopy.dto.user.UserContentUpdateRequest;
import ru.soune.nocopy.entity.UserContent; import ru.soune.nocopy.entity.user.UserContent;
@Mapper(componentModel = "spring", @Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -3,8 +3,8 @@ package ru.soune.nocopy.mapper;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
@Mapper(componentModel = "spring", @Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -1,8 +1,9 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -13,4 +14,11 @@ public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore); List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt); Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
Optional<AuthToken> findByToken(String token); 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);
}
@@ -6,14 +6,15 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
public interface FileEntityRepository extends JpaRepository<FileEntity, String> { public interface FileEntityRepository extends JpaRepository<FileEntity, String> {
List<FileEntity> findByUserId(Long userId); List<FileEntity> findByUserId(Long userId);
Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash); Optional<FileEntity> findByUserIdAndChecksum(Long userId, String imageHash);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
@@ -22,16 +23,31 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
Optional<FileEntity> findByUploadSessionId(String uploadSessionId); Optional<FileEntity> findByUploadSessionId(String uploadSessionId);
List<FileEntity> findByProtectionStatus(ProtectionStatus protectionStatus);
boolean existsByFilePath(String filePath); boolean existsByFilePath(String filePath);
@Query("SELECT SUM(f.fileSize) FROM FileEntity f WHERE f.userId = :userId AND f.status = 'ACTIVE'") @Query("SELECT SUM(f.fileSize) FROM FileEntity f WHERE f.userId = :userId AND f.status = 'ACTIVE'")
Long getTotalSizeByUserId(@Param("userId") Long userId); 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%") @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); List<FileEntity> searchByFileName(@Param("userId") Long userId, @Param("keyword") String keyword);
@Query("SELECT f FROM FileEntity f WHERE f.status = :status") @Query("SELECT f FROM FileEntity f WHERE f.status = :status")
List<FileEntity> searchFileEntityByStatus(@Param("status") FileStatus 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); long countByUserId(Long userId);
} }
@@ -4,7 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import java.util.List;
@Repository @Repository
public interface ImageHashRepository extends JpaRepository<ImageHashEntity, String> { public interface ImageHashRepository extends JpaRepository<ImageHashEntity, String> {
void deleteByFileId(String fileId); void deleteByFileId(String fileId);
List<ImageHashEntity> findByFileId(String fileId);
ImageHashEntity findByHash64HiAndHash64Lo(Long hash64Hi, Long hash64Lo);
} }
@@ -11,13 +11,12 @@ import java.util.List;
@Repository @Repository
public interface ImageSimilarityRepository public interface ImageSimilarityRepository
extends JpaRepository<FileEntity, String> { extends JpaRepository<FileEntity, String> {
@Query(value = """ @Query(value = """
SELECT
SELECT f.id AS id,
f.id AS similarFileId,
f.original_file_name AS originalFileName, f.original_file_name AS originalFileName,
f.file_size AS fileSize, f.file_size AS fileSize,
f.user_id AS userId,
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo h.hash64_lo AS hash64Lo
FROM image_hashes ref FROM image_hashes ref
@@ -32,6 +31,28 @@ public interface ImageSimilarityRepository
@Param("fileId") String fileId @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 = """ @Query(value = """
SELECT SELECT
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
@@ -48,7 +69,7 @@ public interface ImageSimilarityRepository
""", """,
nativeQuery = true) nativeQuery = true)
List<SimilarImageProjection> findExactDuplicates( List<SimilarImageProjection> findExactDuplicates(
@Param("hash64Hi") Integer hash64_hi, @Param("hash64Hi") Long hash64_hi,
@Param("hash64Lo") Integer hash64_lo); @Param("hash64Lo") Long hash64_lo);
} }
@@ -1,8 +1,8 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
public interface SimilarImageProjection { public interface SimilarImageProjection {
Integer getHash64Hi(); Long getHash64Hi();
Integer getHash64Lo(); Long getHash64Lo();
String getId(); String getId();
Long getUserId(); Long getUserId();
String getOriginalFileName(); String getOriginalFileName();
@@ -3,7 +3,7 @@ package ru.soune.nocopy.repository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.UserContent; import ru.soune.nocopy.entity.user.UserContent;
import java.util.List; import java.util.List;
@@ -2,12 +2,12 @@ package ru.soune.nocopy.repository;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
import java.util.Optional; import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); User findByEmail(String email);
boolean existsByEmail(String email); boolean existsByEmail(String email);
boolean existsByPhone(@Size(min = 11, max = 14) String phone); boolean existsByPhone(@Size(min = 11, max = 14) String phone);
} }
@@ -1,44 +1,51 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileResponse; import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.dto.file.SimilarityFilter; 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.ImageHashRepository;
import ru.soune.nocopy.repository.ImageSimilarityRepository; import ru.soune.nocopy.repository.ImageSimilarityRepository;
import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.util.FileUtil;
import java.util.ArrayList; import java.util.*;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class FileSimilarityService { public class FileSimilarityService {
private final ImageSimilarityRepository repository; private final ImageSimilarityRepository repository;
private final ImageHashRepository hashRepository; private final ImageHashRepository hashRepository;
public List<SimilarFileResponse> findSimilarFiles(String fileId) { private final FileUtil fileUtil;
private final AuthTokenRepository authTokenRepository;
public List<SimilarFileDTO> findSimilarFiles(String fileId) {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found")); .orElseThrow(() -> new RuntimeException("Hash not found"));
Integer hash64Hi = imageHashEntity.getHash64Hi(); Long hash64Hi = imageHashEntity.getHash64Hi();
Integer hash64Lo = imageHashEntity.getHash64Lo(); Long hash64Lo = imageHashEntity.getHash64Lo();
List<SimilarImageProjection> candidates = List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
repository.findCandidates(fileId);
return candidates.stream() return candidates.stream()
.map(c -> { .map(c -> {
Integer cHi = c.getHash64Hi(); Long cHi = c.getHash64Hi();
Integer cLo = c.getHash64Lo(); Long cLo = c.getHash64Lo();
int hamming = hamming64(hash64Hi, hash64Lo, cHi, cLo); int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, cHi, cLo);
String level; String level;
if (hamming <= 5) { if (hamming <= 5) {
@@ -49,8 +56,9 @@ public class FileSimilarityService {
level = "DIFFERENT"; level = "DIFFERENT";
} }
return SimilarFileResponse.builder() return SimilarFileDTO.builder()
.fileId(c.getId()) .fileId(c.getId())
.ownerId(c.getUserId())
.originalFileName(c.getOriginalFileName()) .originalFileName(c.getOriginalFileName())
.fileSize(c.getFileSize()) .fileSize(c.getFileSize())
.hammingDistance(hamming) .hammingDistance(hamming)
@@ -62,7 +70,58 @@ public class FileSimilarityService {
.toList(); .toList();
} }
public List<SimilarImageProjection> findDuplicatedByHash(Integer hash64Hi, Integer hash64Lo) { 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); List<SimilarImageProjection> duplicates = repository.findExactDuplicates(hash64Hi,hash64Lo);
if (duplicates.isEmpty()) { if (duplicates.isEmpty()) {
@@ -72,23 +131,24 @@ public class FileSimilarityService {
return duplicates; return duplicates;
} }
public Page<SimilarFileResponse> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable) { public Page<SimilarFileDTO> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found")); .orElseThrow(() -> new RuntimeException("Hash not found"));
Integer hash64Hi = imageHashEntity.getHash64Hi(); Long hash64Hi = imageHashEntity.getHash64Hi();
Integer hash64Lo = imageHashEntity.getHash64Lo(); Long hash64Lo = imageHashEntity.getHash64Lo();
List<SimilarImageProjection> candidates = repository.findCandidates(fileId); List<SimilarImageProjection> candidates = authToken.equals("all") ? repository.findCandidates(fileId):
repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken));
List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null) List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null)
? filter.getSimilarityLevels() ? filter.getSimilarityLevels()
: List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); : List.of("DUPLICATE", "SIMILAR", "DIFFERENT");
List<SimilarFileResponse> allResults = candidates.stream() List<SimilarFileDTO> allResults = candidates.stream()
.map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo))
.filter(response -> similarityLevels.contains(response.getSimilarityLevel())) .filter(response -> similarityLevels.contains(response.getSimilarityLevel()))
.sorted(Comparator.comparingInt(SimilarFileResponse::getHammingDistance)) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance))
.collect(Collectors.toList()); .collect(Collectors.toList());
int total = allResults.size(); int total = allResults.size();
@@ -98,17 +158,17 @@ public class FileSimilarityService {
int fromIndex = Math.min(page * size, total); int fromIndex = Math.min(page * size, total);
int toIndex = Math.min(fromIndex + size, total); int toIndex = Math.min(fromIndex + size, total);
List<SimilarFileResponse> pageContent = allResults.subList(fromIndex, toIndex); List<SimilarFileDTO> pageContent = allResults.subList(fromIndex, toIndex);
return new PageImpl<>(pageContent, pageable, total); return new PageImpl<>(pageContent, pageable, total);
} }
private SimilarFileResponse createSimilarFileResponse(SimilarImageProjection similarImageProjection, private SimilarFileDTO createSimilarFileResponse(SimilarImageProjection similarImageProjection,
Integer hash64Hi, Integer hash64Lo) { Long hash64Hi, Long hash64Lo) {
Integer imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi();
Integer similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo();
int hamming = hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo); int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo);
String level; String level;
if (hamming <= 5) { if (hamming <= 5) {
@@ -119,17 +179,13 @@ public class FileSimilarityService {
level = "DIFFERENT"; level = "DIFFERENT";
} }
return SimilarFileResponse.builder() return SimilarFileDTO.builder()
.fileId(similarImageProjection.getId()) .fileId(similarImageProjection.getId())
.ownerId(similarImageProjection.getUserId())
.originalFileName(similarImageProjection.getOriginalFileName()) .originalFileName(similarImageProjection.getOriginalFileName())
.fileSize(similarImageProjection.getFileSize()) .fileSize(similarImageProjection.getFileSize())
.hammingDistance(hamming) .hammingDistance(hamming)
.similarityLevel(level) .similarityLevel(level)
.build(); .build();
} }
private int hamming64(int aHi, int aLo, int bHi, int bLo) {
return Integer.bitCount(aHi ^ bHi)
+ Integer.bitCount(aLo ^ bLo);
}
} }
@@ -1,5 +1,7 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service;
import com.vrt.fileprotection.image.phash.PHash;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -7,15 +9,10 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.ImageHashRepository;
import javax.imageio.ImageIO; import java.io.File;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
@Service @Service
@@ -25,15 +22,19 @@ public class ImageHashService {
private final ImageHashRepository repository; private final ImageHashRepository repository;
public Map<String, Integer> calculateHash(Path imagePath) throws IOException { public Map<String, Long> calculateHash(Path imagePath) throws IOException {
long hash64 = computePhash64(imagePath); 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 Map.of( return hash;
"hi", high32(hash64),
"low", low32(hash64));
} }
public void create(FileEntity file, Map<String, Integer> stringIntegerMap) { public void create(FileEntity file, Map<String, Long> stringIntegerMap) {
ImageHashEntity entity = ImageHashEntity.builder() ImageHashEntity entity = ImageHashEntity.builder()
.file(file) .file(file)
.hash64Hi(stringIntegerMap.get("hi")) .hash64Hi(stringIntegerMap.get("hi"))
@@ -44,111 +45,4 @@ public class ImageHashService {
repository.save(entity); repository.save(entity);
} }
private long computePhash64(Path path) throws IOException {
try (InputStream is = Files.newInputStream(path)) {
BufferedImage image = ImageIO.read(is);
if (image == null) throw new IOException("Cannot read image file: " + path);
BufferedImage gray = toGrayScale(image);
BufferedImage resized = resizeImage(gray, 32, 32);
double[][] dct = applyDCT(resized);
double[][] topLeft = extractTopLeft8x8(dct);
double median = calculateMedian(topLeft, true);
return create64BitHashLong(topLeft, median);
}
}
private BufferedImage toGrayScale(BufferedImage image) {
BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = gray.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return gray;
}
private BufferedImage resizeImage(BufferedImage image, int width, int height) {
BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(image, 0, 0, width, height, null);
g.dispose();
return resized;
}
private double[][] applyDCT(BufferedImage image) {
int w = image.getWidth();
int h = image.getHeight();
double[][] pixels = new double[h][w];
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int gray = image.getRGB(x, y) & 0xFF;
pixels[y][x] = gray;
}
}
double[][] dct = new double[h][w];
for (int u = 0; u < h; u++) {
double au = u == 0 ? 1.0 / Math.sqrt(h) : Math.sqrt(2.0 / h);
for (int v = 0; v < w; v++) {
double av = v == 0 ? 1.0 / Math.sqrt(w) : Math.sqrt(2.0 / w);
double sum = 0.0;
for (int x = 0; x < h; x++) {
for (int y = 0; y < w; y++) {
sum += pixels[x][y] *
Math.cos((2 * x + 1) * u * Math.PI / (2.0 * h)) *
Math.cos((2 * y + 1) * v * Math.PI / (2.0 * w));
}
}
dct[u][v] = au * av * sum;
}
}
return dct;
}
private double[][] extractTopLeft8x8(double[][] dct) {
double[][] block = new double[8][8];
for (int i = 0; i < 8; i++) System.arraycopy(dct[i], 0, block[i], 0, 8);
return block;
}
private double calculateMedian(double[][] block, boolean excludeDC) {
double[] vals = new double[63];
int idx = 0;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (i == 0 && j == 0 && excludeDC) continue;
vals[idx++] = block[i][j];
}
}
Arrays.sort(vals);
return (vals.length % 2 == 0) ?
(vals[vals.length / 2 - 1] + vals[vals.length / 2]) / 2.0 :
vals[vals.length / 2];
}
private long create64BitHashLong(double[][] block, double median) {
long hash = 0L;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (i == 0 && j == 0) continue;
hash <<= 1;
if (block[i][j] > median) hash |= 1;
}
}
return hash;
}
private int high32(long hash64) {
return (int) (hash64 >>> 32);
}
private int low32(long hash64) {
return (int) hash64;
}
} }
@@ -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);
}
}
@@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -9,13 +9,16 @@ import ru.soune.nocopy.dto.file.FileResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession; 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.DuplicateImageException;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -36,6 +39,8 @@ public class FileEntityService {
private final FileSimilarityService fileSimilarityService; private final FileSimilarityService fileSimilarityService;
private final ImageHashRepository imageHashRepository;
@Transactional(noRollbackFor = DuplicateImageException.class) @Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId()); log.info("Creating FileEntity for upload session: {}", session.getUploadId());
@@ -47,7 +52,7 @@ public class FileEntityService {
throw new IOException("File not found on disk: " + filePath); throw new IOException("File not found on disk: " + filePath);
} }
Map<String, Integer> imageHash = Map.of(); Map<String, Long> imageHash = Map.of();
if (session.getFileType().startsWith("image")) { if (session.getFileType().startsWith("image")) {
imageHash = imageHashService.calculateHash(filePath); imageHash = imageHashService.calculateHash(filePath);
@@ -57,7 +62,7 @@ public class FileEntityService {
if (!duplicatedByHash.isEmpty()) { if (!duplicatedByHash.isEmpty()) {
SimilarImageProjection similarImageProjection = duplicatedByHash.get(0); SimilarImageProjection similarImageProjection = duplicatedByHash.get(0);
throw new DuplicateImageException("Duplicate", similarImageProjection.getFileId(), throw new DuplicateImageException("Duplicate", similarImageProjection.getId(),
similarImageProjection.getUserId()); similarImageProjection.getUserId());
} }
} }
@@ -110,6 +115,39 @@ public class FileEntityService {
return convertToResponse(fileEntity, version); return convertToResponse(fileEntity, version);
} }
@Transactional(readOnly = true)
public FileEntityResponse getByFilePath(String filePath, int version) {
FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath)
.orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath));
return convertToResponse(fileEntity, version);
}
@Transactional(readOnly = true)
public FileResponse getAllUserFiles(Long userId, int version) {
List<FileEntity> fileEntities = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
List<FileEntityResponse> files = fileEntities.stream()
.map(file -> convertToResponse(file, version))
.collect(Collectors.toList());
long totalSize = fileEntities.stream()
.mapToLong(FileEntity::getFileSize)
.sum();
return FileResponse.builder()
.files(files)
.totalCount(files.size())
.totalSize(totalSize)
.formattedTotalSize(formatFileSize(totalSize))
.page(1)
.pageSize(files.size())
.build();
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
List<FileEntity> allFiles = fileEntityRepository.findByUserIdAndStatus( List<FileEntity> allFiles = fileEntityRepository.findByUserIdAndStatus(
@@ -150,26 +188,45 @@ public class FileEntityService {
} }
@Transactional @Transactional
public void markAsDeleted(FileEntity fileEntity) { public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException {
fileEntity.setStatus(FileStatus.DELETED); fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now()); 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); fileEntityRepository.save(fileEntity);
} }
@Transactional @Transactional
public boolean deleteFromDisk(FileEntity fileEntity) throws IOException { public void deleteFromDisk(FileEntity fileEntity) throws IOException {
Path path = Paths.get(fileEntity.getFilePath()); Path path = Paths.get(fileEntity.getFilePath());
if (!Files.exists(path)) { if (!Files.exists(path)) {
return true; return;
} }
Files.delete(path); Files.delete(path);
markAsDeleted(fileEntity); fileEntityRepository.delete(fileEntity);
return true;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -178,6 +235,99 @@ public class FileEntityService {
return totalSize != null ? totalSize : 0L; 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) { private boolean checkFileExistsOnDisk(String filePath) {
try { try {
return Files.exists(Paths.get(filePath)); return Files.exists(Paths.get(filePath));
@@ -187,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) { private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
@@ -208,6 +366,7 @@ public class FileEntityService {
.downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId()) .downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId())
.existsOnDisk(existsOnDisk) .existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId()) .supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString())
.build(); .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.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import java.util.List; import java.util.List;
@@ -26,23 +27,37 @@ public class FileStatsService {
.fileCount(calculateTotalCount(files)) .fileCount(calculateTotalCount(files))
.filesCheck(calculateByStatus(files, FileStatus.CHECKED)) .filesCheck(calculateByStatus(files, FileStatus.CHECKED))
.filesViolation(calculateByStatus(files, FileStatus.VIOLATION)) .filesViolation(calculateByStatus(files, FileStatus.VIOLATION))
.protectedFilesCount(protectedUserFiles(files))
.imagesSize(calculateMediaSize(files, FileType.IMAGE)) .imagesSize(calculateMediaSize(files, FileType.IMAGE))
.imagesCount(calculateMediaCount(files, FileType.IMAGE)) .imagesCount(calculateMediaCount(files, FileType.IMAGE))
.imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED)) .imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED))
.imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION)) .imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION))
.protectedImageFilesCount(protectedUserFiles(files, FileType.IMAGE))
.videosSize(calculateMediaSize(files, FileType.VIDEO)) .videosSize(calculateMediaSize(files, FileType.VIDEO))
.videosCount(calculateMediaCount(files, FileType.VIDEO)) .videosCount(calculateMediaCount(files, FileType.VIDEO))
.videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED)) .videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED))
.videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION)) .videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION))
.protectedVideoFilesCount(protectedUserFiles(files, FileType.VIDEO))
.audiosSize(calculateMediaSize(files, FileType.AUDIO)) .audiosSize(calculateMediaSize(files, FileType.AUDIO))
.audiosCount(calculateMediaCount(files, FileType.AUDIO)) .audiosCount(calculateMediaCount(files, FileType.AUDIO))
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED)) .audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION)) .audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO))
.build(); .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) { private Long calculateTotalSize(List<FileEntity> files) {
return files.stream() return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED) .filter(file -> file.getStatus() != FileStatus.DELETED)
@@ -10,12 +10,13 @@ public interface FileUploadService {
FileUploadSession initUpload(Long userId, String fileName, FileUploadSession initUpload(Long userId, String fileName,
String fileType, String extension, long fileSize); String fileType, String extension, long fileSize);
UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, void handleExpiredSession(FileUploadSession session);
MultipartFile chunkFile) throws IOException;
void cancelUpload(String uploadId);
UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, Integer findSimilar);
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
void handleExpiredSession(FileUploadSession session); void completeFileProcessingAsync(FileUploadSession session);
public void cancelUpload(String uploadId);
} }
@@ -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);
}
}
}
@@ -1,5 +1,7 @@
package ru.soune.nocopy.service.file; package ru.soune.nocopy.service.file.impl;
import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -23,6 +25,9 @@ import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.util.FileUtil;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
@@ -73,6 +78,10 @@ public class FileUploadServiceImpl implements FileUploadService {
@Autowired @Autowired
private FileEntityService fileEntityService; private FileEntityService fileEntityService;
private final NoCopyFileService noCopyFileService;
private final FileUtil fileUtil;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -138,10 +147,38 @@ public class FileUploadServiceImpl implements FileUploadService {
return savedSession; return savedSession;
} }
@Override
@Transactional
public void handleExpiredSession(FileUploadSession session) {
session.setStatus(UploadStatus.FAILED);
session.setLastError("Upload session expired");
sessionRepository.save(session);
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
}
@Override
@Transactional
public void cancelUpload(String uploadId) {
FileUploadSession session = sessionRepository.findById(uploadId)
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
throw new FileUploadException("Cannot cancel completed or cancelled upload");
}
session.setStatus(UploadStatus.CANCELLED);
sessionRepository.save(session);
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
log.info("Upload cancelled: {}", uploadId);
}
@Override @Override
@Transactional @Transactional
public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile) { MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId) FileUploadSession session = sessionRepository.findById(uploadId)
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId)); .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
@@ -167,52 +204,7 @@ public class FileUploadServiceImpl implements FileUploadService {
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
} }
return processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
}
@Override
@Transactional
public void handleExpiredSession(FileUploadSession session) {
session.setStatus(UploadStatus.FAILED);
session.setLastError("Upload session expired");
sessionRepository.save(session);
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
}
@Async("fileUploadTaskExecutor")
@Transactional
public void assembleFileAsync(FileUploadSession session) throws DuplicateImageException {
try {
assembleFile(session);
log.info("File assembly completed successfully for session: {}",
session.getUploadId());
} catch (DuplicateImageException e) {
throw new DuplicateImageException("DUBL", e.duplicateFileId(), e.userId());
} catch (Exception e) {
log.error("Failed to assemble file for session {}: {}",
session.getUploadId(), e.getMessage(), e);
handleAssemblyFailure(session, e);
}
}
@Override
@Transactional
public void cancelUpload(String uploadId) {
FileUploadSession session = sessionRepository.findById(uploadId)
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
throw new FileUploadException("Cannot cancel completed or cancelled upload");
}
session.setStatus(UploadStatus.CANCELLED);
sessionRepository.save(session);
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
log.info("Upload cancelled: {}", uploadId);
} }
@Override @Override
@@ -223,6 +215,47 @@ public class FileUploadServiceImpl implements FileUploadService {
return UploadProgressResponse.fromSession(session); return UploadProgressResponse.fromSession(session);
} }
@Async("fileUploadTaskExecutor")
@Transactional
@Override
public void completeFileProcessingAsync(FileUploadSession session) {
try {
Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath);
FileEntity fileEntity = FileEntity.builder()
.userId(session.getUserId())
.originalFileName(session.getFileName())
.storedFileName(filePath.getFileName().toString())
.filePath(session.getFilePath())
.fileSize(session.getFileSize())
.mimeType(session.getFileType())
.fileExtension(session.getExtension())
.checksum(checksum)
.uploadSessionId(session.getUploadId())
.status(FileStatus.ACTIVE)
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
if (session.getFileType().equals("image")) {
Map<String, Long> hash = imageHashService.calculateHash(filePath);
imageHashService.create(saved, hash);
}
cleanupSessionFiles(session);
noCopyFileService.addFile(fileUtil.createFileInfo(fileEntity));
log.info("File processing completed for session: {}", session.getUploadId());
} catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e);
}
}
private void validateSession(FileUploadSession session) { private void validateSession(FileUploadSession session) {
UploadStatus status = session.getStatus(); UploadStatus status = session.getStatus();
@@ -252,47 +285,13 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
@Async("fileUploadTaskExecutor") private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
@Transactional Integer findSimilar) {
public void completeFileProcessingAsync(FileUploadSession session) {
try {
Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath);
FileEntity fileEntity = FileEntity.builder()
.userId(session.getUserId())
.originalFileName(session.getFileName())
.storedFileName(filePath.getFileName().toString())
.filePath(session.getFilePath())
.fileSize(session.getFileSize())
.mimeType(session.getFileType())
.fileExtension(session.getExtension())
.checksum(checksum)
.uploadSessionId(session.getUploadId())
.status(FileStatus.ACTIVE)
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
Map<String, Integer> hash = imageHashService.calculateHash(filePath);
imageHashService.create(saved, hash);
cleanupSessionFiles(session);
log.info("File processing completed for session: {}", session.getUploadId());
} catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e);
}
}
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile) {
String chunkPath = null; String chunkPath = null;
try { try {
if (session.getChunkPaths().containsKey(chunkNumber)) { if (session.getChunkPaths().containsKey(chunkNumber)) {
return handleExistingChunk(session, chunkNumber, chunkFile); return handleExistingChunk(session, chunkNumber, chunkFile, findSimilar);
} }
chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile); chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile);
@@ -313,7 +312,7 @@ public class FileUploadServiceImpl implements FileUploadService {
String finalFilePath = assembleFileSynchronously(session); String finalFilePath = assembleFileSynchronously(session);
if (session.getFileType().startsWith("image")) { if (session.getFileType().startsWith("image") && findSimilar == 0) {
checkForDuplicatesSynchronously(finalFilePath); checkForDuplicatesSynchronously(finalFilePath);
} }
@@ -368,21 +367,23 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
private void checkForDuplicatesSynchronously(String filePath) private void checkForDuplicatesSynchronously(String filePath)
throws IOException { throws IOException , DuplicateImageException {
Path path = Paths.get(filePath); Path path = Paths.get(filePath);
Map<String, Integer> hash = imageHashService.calculateHash(path); Map<String, Long> hash = imageHashService.calculateHash(path);
List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash( List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash(
hash.get("hi"), hash.get("low")); hash.get("hi"), hash.get("low"));
if (!duplicates.isEmpty()) { if (!duplicates.isEmpty()) {
throw new DuplicateImageException("Duplicate", duplicates.get(0).getFileId(), throw new DuplicateImageException("Duplicate", duplicates.getFirst().getId(),
duplicates.get(0).getUserId()); duplicates.getFirst().getUserId());
} }
} }
private UploadProgressResponse handleExistingChunk(FileUploadSession session, private UploadProgressResponse handleExistingChunk(FileUploadSession session,
Integer chunkNumber, Integer chunkNumber,
MultipartFile chunkFile) throws IOException { MultipartFile chunkFile,
Integer findSimilar) throws IOException {
String existingPath = session.getChunkPaths().get(chunkNumber); String existingPath = session.getChunkPaths().get(chunkNumber);
Path chunkPath = Paths.get(existingPath); Path chunkPath = Paths.get(existingPath);
@@ -391,7 +392,7 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setChunksUploaded(session.getChunksUploaded() - 1); session.setChunksUploaded(session.getChunksUploaded() - 1);
sessionRepository.save(session); sessionRepository.save(session);
return processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
} }
long existingSize = Files.size(chunkPath); long existingSize = Files.size(chunkPath);
@@ -402,7 +403,7 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setChunksUploaded(session.getChunksUploaded() - 1); session.setChunksUploaded(session.getChunksUploaded() - 1);
sessionRepository.save(session); sessionRepository.save(session);
return processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
} }
return UploadProgressResponse.fromSession(session); return UploadProgressResponse.fromSession(session);
@@ -482,11 +483,17 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setChecksum(checksum); session.setChecksum(checksum);
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setCompletedAt(LocalDateTime.now()); session.setCompletedAt(LocalDateTime.now());
sessionRepository.save(session); sessionRepository.save(session);
log.info("Upload session updated to COMPLETED: {}", session.getUploadId()); log.info("Upload session updated to COMPLETED: {}", session.getUploadId());
try { try {
fileEntityService.createFromUploadSession(session, checksum); FileEntity fileEntity = fileEntityService.createFromUploadSession(session, checksum);
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
noCopyFileService.addFile(fileInfo);
log.info("FileEntity successfully created for session: {}", log.info("FileEntity successfully created for session: {}",
session.getUploadId()); session.getUploadId());
} catch (DuplicateImageException e) { } catch (DuplicateImageException e) {
@@ -0,0 +1,54 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.image.ImageLocalSearch;
import com.vrt.fileprotection.image.phash.PHash;
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.dto.file.SimilarFileDTO;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.util.FileUtil;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class ImageLocalSearchImpl implements ImageLocalSearch {
private final FileSimilarityService fileSimilarityService;
private final ImageHashRepository imageHashRepository;
private final FileUtil fileUtil;
private final FileEntityRepository fileEntityRepository;
@Override
public @NotNull List<Result> find(@NotNull FileProtector.FileInfo fileInfo) {
List<SimilarFileDTO> duplicateByHammingDistance =
fileSimilarityService.findDuplicateByHammingDistance(fileInfo.getId(), 10,
5, 10);
return duplicateByHammingDistance.stream()
.map(fileUtil::convertToResult)
.toList();
}
@Override
public @Nullable FileProtector.FileInfo findByPHash(@NotNull PHash pHash) {
ImageHashEntity imageHashEntity =
imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(),
pHash.getSecondPart());
FileEntity file = fileEntityRepository.findByFileId(imageHashEntity.getFileId());
return new FileProtector.FileInfo(FileProtector.Type.IMAGE, imageHashEntity.getFileId(),
String.valueOf(file.getUserId()));
}
}
@@ -0,0 +1,34 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.OperationResult;
import com.vrt.fileprotection.image.ImageUniqueCheck;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.service.FileSimilarityService;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class ImageUniqueCheckImpl implements ImageUniqueCheck {
private final FileSimilarityService fileSimilarityService;
@Override
public @NotNull OperationResult check(@NotNull FileProtector.FileInfo fileInfo) {
String fileId = fileInfo.getId();
List<SimilarFileDTO> duplicateByHammingDistance =
fileSimilarityService.findDuplicateByHammingDistance(fileId, 3, 3, 4);
if (duplicateByHammingDistance == null || duplicateByHammingDistance.isEmpty()) {
return OperationResult.Companion.success();
} else {
return OperationResult.Companion.failure("Duplicate file");
}
}
}
@@ -0,0 +1,36 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.service.file.FileEntityService;
@Slf4j
@Component
@AllArgsConstructor
public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingListener {
private final FileEntityService fileEntityService;
@Override
public void onStartProcessing(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
}
@Override
public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId());
}
@Override
public void onSavingFailed(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
}
@Override
public void onFinish(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
}
}
@@ -0,0 +1,108 @@
package ru.soune.nocopy.service.file.impl;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.OperationResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.service.file.FileEntityService;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@Component
@RequiredArgsConstructor
public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
private static final Path SIGNATURE_FILE_PATH = Paths.get("/data/uploads/signature");
private final FileEntityService fileEntityService;
@Nullable
@Override
public File getImageFile(@NotNull String id) {
return fileEntityService.getFileById(id);
}
@Nullable
@Override
public File getVideoFile(@NotNull String id) {
return fileEntityService.getFileById(id);
}
@Nullable
@Override
public File getAudioFile(@NotNull String id) {
return fileEntityService.getFileById(id);
}
@Override
public @Nullable File getSignature() {
File signatureFile = SIGNATURE_FILE_PATH.toFile();
if (signatureFile.exists() && signatureFile.isFile() && signatureFile.length() > 0) {
return signatureFile;
}
return null;
}
@Override
public @NotNull OperationResult writeSignature(@NotNull byte[] bytes) {
try {
Path directory = SIGNATURE_FILE_PATH.getParent();
if (directory != null && !Files.exists(directory)) {
Files.createDirectories(directory);
}
Files.write(SIGNATURE_FILE_PATH, bytes);
return OperationResult.Companion.success();
} catch (IOException e) {
return OperationResult.Companion.failure("Not saved signature file");
}
}
@NotNull
@Override
public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try {
fileEntityService.writeProtectedFile(id, data, fileExt);
return OperationResult.Companion.success();
} catch (Exception e) {
log.error("Failed to create protected file: {}", id, e);
return OperationResult.Companion.failure("Failed to create protected file");
}
}
@NotNull
@Override
public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try {
fileEntityService.writeProtectedFile(id, data, fileExt);
return OperationResult.Companion.success();
} catch (Exception e) {
log.error("Failed to create protected file: {}", id, e);
return OperationResult.Companion.failure("Failed to create protected file");
}
}
@NotNull
@Override
public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try {
fileEntityService.writeProtectedFile(id, data, fileExt);
return OperationResult.Companion.success();
} catch (Exception e) {
log.error("Failed to create protected file: {}", id, e);
return OperationResult.Companion.failure("Failed to create protected file");
}
}
}
@@ -0,0 +1,94 @@
package ru.soune.nocopy.service.mail;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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.repository.EmailVerificationTokenRepository;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {
private final JavaMailSender mailSender;
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
@Value("${app.email.verification.standart-subject}")
private String standartSubject;
@Value("${app.email.verification.standart-address-from}")
private String standartAdressFrom;
@Value("${app.email.verification.user}")
private String user;
public void sendVerificationEmail(User user, String code) throws MessagingException, IOException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(standartAdressFrom);
helper.setTo(user.getEmail());
helper.setSubject(standartSubject);
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("Email sent to: {}", user.getEmail());
}
@Transactional
public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) {
EmailVerificationToken emailToken = new EmailVerificationToken();
emailToken.setUser(authToken.getUser());
emailToken.setToken(generateVerificationCode());
emailToken.setCreatedAt(LocalDateTime.now());
return emailVerificationTokenRepository.save(emailToken);
}
private String generateVerificationCode() {
SecureRandom random = new SecureRandom();
int code = 100000 + random.nextInt(900000);
return String.valueOf(code);
}
private String loadAndProcessTemplate(String username, String code) throws IOException {
ClassPathResource resource = new ClassPathResource("templates/email-verification.html");
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String safeUsername = escapeHtml(username != null ? username : user);
String safeCode = escapeHtml(code);
return template
.replace("{{username}}", safeUsername)
.replace("{{code}}", safeCode);
}
private String escapeHtml(String text) {
if (text == null) return "";
return text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
}
@@ -1,12 +1,16 @@
package ru.soune.nocopy.service.auth; package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
@@ -14,10 +18,7 @@ import ru.soune.nocopy.repository.UserRepository;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.*;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -29,6 +30,8 @@ public class AuthService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final MessageSource messageSource;
private final SecureRandom secureRandom = new SecureRandom(); private final SecureRandom secureRandom = new SecureRandom();
@Transactional @Transactional
@@ -48,33 +51,7 @@ public class AuthService {
User savedUser = userRepository.save(user); User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken(); AuthToken authToken = genereateAuthToken(savedUser);
authToken.setToken(generateAuthToken());
authToken.setUser(savedUser);
return authTokenRepository.save(authToken);
}
@Transactional
public AuthToken login(LoginRequest request) {
Optional<User> userOpt = userRepository.findByEmail(request.getEmail());
User user = userOpt.get();
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getDescription(), loginAnswer));
}
user.setLastLoginAt(LocalDateTime.now());
User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken();
authToken.setToken(generateAuthToken());
authToken.setUser(savedUser);
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
@@ -99,12 +76,48 @@ public class AuthService {
return authToken.getUser().getId(); return authToken.getUser().getId();
} }
@Transactional
public AuthToken login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail());
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getDescription(), loginAnswer));
}
user.setLastLoginAt(LocalDateTime.now());
User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken();
authToken.setToken(generateAuthToken());
authToken.setUser(savedUser);
return authTokenRepository.save(authToken);
}
@Transactional @Transactional
public void logout(String token) { public void logout(String token) {
authTokenRepository.findByToken(token) authTokenRepository.findByToken(token)
.ifPresent(authTokenRepository::delete); .ifPresent(authTokenRepository::delete);
} }
public AuthToken generateAuthToken(User user) {
return authTokenRepository.save(genereateAuthToken(user));
}
private AuthToken genereateAuthToken(User user) {
AuthToken authToken = new AuthToken();
authToken.setToken(generateAuthToken());
authToken.setUser(user);
return authToken;
}
private String generateAuthToken() { private String generateAuthToken() {
byte[] bytes = new byte[32]; byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes); secureRandom.nextBytes(bytes);
@@ -0,0 +1,31 @@
package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CleanUpVerifyTokens {
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
@Transactional
@Scheduled(fixedDelay = 30000)
public void cleanupExpiredTokens() {
LocalDateTime now = LocalDateTime.now();
List<EmailVerificationToken> expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now);
if (!expiredTokens.isEmpty()) {
emailVerificationTokenRepository.deleteAll(expiredTokens);
}
}
}
@@ -0,0 +1,205 @@
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.vision.v1.*;
import com.google.protobuf.ByteString;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.GoogleVisionSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class GoogleVisionSearchService {
private final FileEntityRepository fileEntityRepository;
private final ObjectMapper objectMapper;
private ImageAnnotatorClient visionClient;
@PostConstruct
public void init() throws IOException {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
initVisionClient();
}
private void initVisionClient() throws IOException {
try (InputStream credentialsStream = new ClassPathResource("config/google-service-account.json").getInputStream()) {
GoogleCredentials credentials = GoogleCredentials.fromStream(credentialsStream);
ImageAnnotatorSettings settings = ImageAnnotatorSettings.newBuilder()
.setCredentialsProvider(() -> credentials)
.build();
this.visionClient = ImageAnnotatorClient.create(settings);
}
}
public GoogleVisionSearchResponse searchByFileEntity(String fileId) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> {
throw new NotValidFieldException("File not found", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId", fileId)));
});
if (!isImageFile(fileEntity)) {
throw new NotValidFieldException("File not image", new BaseResponse(20007,
MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(),
Map.of("file_type", fileEntity.getMimeType())));
}
byte[] fileBytes;
try {
fileBytes = readFileFromDisk(fileEntity);
} catch (IOException e) {
throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId", fileId,
"filePath", fileEntity.getFilePath())));
}
return callGoogleVisionApi(fileBytes);
}
private boolean isImageFile(FileEntity fileEntity) {
String mimeType = fileEntity.getMimeType();
return mimeType != null && mimeType.startsWith("image");
}
private byte[] readFileFromDisk(FileEntity fileEntity) throws IOException {
Path filePath = Path.of(fileEntity.getFilePath());
if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
throw new IOException("Cannot read file: " + fileEntity.getFilePath());
}
return Files.readAllBytes(filePath);
}
private GoogleVisionSearchResponse callGoogleVisionApi(byte[] imageBytes) throws IOException {
ByteString imgBytes = ByteString.copyFrom(imageBytes);
Image image = Image.newBuilder()
.setContent(imgBytes)
.build();
Feature feature = Feature.newBuilder()
.setType(Feature.Type.WEB_DETECTION)
.setMaxResults(20)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(feature)
.setImage(image)
.build();
BatchAnnotateImagesResponse response = visionClient.batchAnnotateImages(List.of(request));
if (response.getResponsesCount() == 0) {
throw new IOException("Empty response from Google Vision API");
}
AnnotateImageResponse singleResponse = response.getResponses(0);
if (singleResponse.hasError()) {
throw new IOException("Google Vision API error: " + singleResponse.getError().getMessage());
}
return convertToResponse(singleResponse.getWebDetection());
}
private GoogleVisionSearchResponse convertToResponse(WebDetection webDetection) {
GoogleVisionSearchResponse response = new GoogleVisionSearchResponse();
if (webDetection.getBestGuessLabelsCount() > 0) {
response.setBestGuessLabels(
webDetection.getBestGuessLabelsList().stream()
.map(label -> {
GoogleVisionSearchResponse.BestGuessLabel dtoLabel =
new GoogleVisionSearchResponse.BestGuessLabel();
dtoLabel.setLabel(label.getLabel());
dtoLabel.setLanguageCode(label.getLanguageCode());
return dtoLabel;
})
.toList()
);
}
if (webDetection.getFullMatchingImagesCount() > 0) {
response.setFullMatchingImages(
webDetection.getFullMatchingImagesList().stream()
.map(img -> {
GoogleVisionSearchResponse.ImageResult dtoImg =
new GoogleVisionSearchResponse.ImageResult();
dtoImg.setUrl(img.getUrl());
dtoImg.setScore(img.getScore());
return dtoImg;
})
.toList()
);
}
if (webDetection.getVisuallySimilarImagesCount() > 0) {
response.setVisuallySimilarImages(
webDetection.getVisuallySimilarImagesList().stream()
.map(img -> {
GoogleVisionSearchResponse.ImageResult dtoImg =
new GoogleVisionSearchResponse.ImageResult();
dtoImg.setUrl(img.getUrl());
dtoImg.setScore(img.getScore());
return dtoImg;
})
.toList()
);
}
if (webDetection.getPagesWithMatchingImagesCount() > 0) {
response.setPagesWithMatchingImages(
webDetection.getPagesWithMatchingImagesList().stream()
.map(page -> {
GoogleVisionSearchResponse.PageResult dtoPage =
new GoogleVisionSearchResponse.PageResult();
dtoPage.setUrl(page.getUrl());
dtoPage.setPageTitle(page.getPageTitle());
return dtoPage;
})
.toList()
);
}
if (webDetection.getPartialMatchingImagesCount() > 0) {
response.setPartialMatchingImages(
webDetection.getPartialMatchingImagesList().stream()
.map(img -> {
GoogleVisionSearchResponse.ImageResult dtoImg =
new GoogleVisionSearchResponse.ImageResult();
dtoImg.setUrl(img.getUrl());
dtoImg.setScore(img.getScore());
return dtoImg;
})
.toList()
);
}
return response;
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,7 +12,6 @@ import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
@@ -1,18 +1,20 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service.user;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.UserRequest; import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.InvalidUserEmail;
import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.NotValidationPasswordException;
import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import java.util.Optional;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class UserService { public class UserService {
@@ -44,6 +46,16 @@ public class UserService {
return mapToDTO(updatedUser); return mapToDTO(updatedUser);
} }
@Transactional
public User activateAndVerifyUser(long userId) {
Optional<User> users = userRepository.findById(userId);
User user = users.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
user.setActive(true);
user.setEmailVerified(true);
return userRepository.save(user);
}
private void updateUserFromRequest(User user, UserRequest request) { private void updateUserFromRequest(User user, UserRequest request) {
if (request.getFullName() != null) { if (request.getFullName() != null) {
user.setFullName(request.getFullName()); user.setFullName(request.getFullName());
@@ -0,0 +1,48 @@
package ru.soune.nocopy.util;
import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.image.ImageLocalSearch;
import com.vrt.fileprotection.image.ImageScore;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.entity.file.FileEntity;
@Service
public class FileUtil {
public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) {
FileProtector.FileInfo fileInfo =
new FileProtector.FileInfo(FileProtector.Type.IMAGE, response.getFileId(),
String.valueOf(response.getOwnerId()));
ImageScore imageScore = new ImageScore(ImageScore.Rate.valueOf(response.getSimilarityLevel()),
response.getHammingDistance());
return new ImageLocalSearch.Result(fileInfo, imageScore);
}
public FileProtector.FileInfo createFileInfo(FileEntity fileEntity) {
FileProtector.Type type = determineFileType(fileEntity.getMimeType());
return new FileProtector.FileInfo(type, fileEntity.getId(), String.valueOf(fileEntity.getUserId()));
}
public int hamming64(long aHi, long aLo, long bHi, long bLo) {
return Long.bitCount(aHi ^ bHi)
+ Long.bitCount(aLo ^ bLo);
}
private FileProtector.Type determineFileType(String mimeType) {
if (mimeType == null) {
return FileProtector.Type.IMAGE;
}
if (mimeType.startsWith("image")) {
return FileProtector.Type.IMAGE;
} else if (mimeType.startsWith("video")) {
return FileProtector.Type.VIDEO;
} else if (mimeType.startsWith("audio")) {
return FileProtector.Type.AUDIO;
} else {
return FileProtector.Type.IMAGE;
}
}
}
+29
View File
@@ -1,4 +1,26 @@
spring: spring:
mail:
host: smtp.gmail.com
port: 587
username: vlad.popovtsev@gmail.com
password: gocjwvrqrzlaatsr
properties:
mail:
smtp:
auth: true
starttls:
enable: true
# mail:
# host: postfix
# port: 25
# username: noreply@nocopy.com
# password: nocopy!nocopy!
# properties:
# mail.smtp.auth: false
# mail.smtp.starttls.enable: false
# mail.debug: true
flyway: flyway:
enabled: true enabled: true
baseline-on-migrate: true baseline-on-migrate: true
@@ -60,3 +82,10 @@ yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
app:
email:
verification:
standart-subject: NO COPY - Подтверждение email адреса
standart-address-from: NO COPY <noreply@nocopy.com>
user: Пользователь
@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "ncp-1-483610",
"private_key_id": "94ea762d9a56a3362155ab33f8959bfec5ae8b42",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCwjY9sYQfeqHwr\nqKgr8sw3Qfao/vzTa9wnmbZvrM6UJ3ukMkj08w50rP0udKxTRWnH6rpSQnD696GS\nY0/qrjEO1LeMdPJ+R0v8B+Z3PlpuQtfmxXALF4QbGQxeWg9NK7o4JM0kANkV80me\nxo6T7WnywQm1SfBIPZjwLBhkn7pfcdTDJFiZkH073yB6/tPsgd6TRQDmlvGsrGmq\nCf8E/DhxaFIHUpoUEGpxeuFPmvAicEWdURCtCXeiAxRQv+Gy0mP7uGGY6C3SeYfJ\n5PNLopgnN2ATCeIp1EjJnH28sAEvE0e/K/9z4ZThgh1mB1CaLkaQweq3YQWoiyGF\nfcoPpJ8LAgMBAAECggEABluSppx35BS9I/VW2P5NTFAbycok4JgpvWNTpoHajos9\ncJQ+/FMkWh9AnsOk0PrW7FQPkZbC6sawEs1wu0q6iYYvdedgNBvtKI5ARlsAdqgB\njlXdywh1wWQNhfhIRMcmVY89s+Yz6w5vwD/2Mm2COzOoXdbjyDYvo7ZyymzWEUnA\nwaxg7aF5cGC+iqf+l2Ym8XdY2ZavvyZ8RwgKfQGfcPuhMogg+H6au4yvXPBNxv0u\nxDCbOm2ezA4cLyKyFqi6S5KiGmk2LnCOoLrkIalmTQhnptFZofGs4ezuOvhZ6ZTi\nQVDayHzbBygmFzJn1wzw83IQ7QgMmWYoEjC+JcRlgQKBgQDg6U3QPxhdvX1dCzBj\nef6yUrIeJhiILvKnAzi6MUhaWP0mU6kcIIn8DS78ojML+leKh2R7JQFFrt87kHjK\npcaTC7wkgNCa6e3d+U95EIVUvPVrvQ5O4zIM7evTiPEeZhN9Dhuh0o3eeYxfIqZO\nsN0y8F/xPWdGW5CcdL7dcTOUkwKBgQDI9Qzr7poRaHM8jzS0M4Q4cVsYdC2P6KgI\nWD9Qbbl1rVi14YLt1FSd4w7U4YJ/kaWOrdtKklqAvM3oq5J0oEQekpixsxFvXdW+\ngJTzOoOq2lgCThBagwVKOFkAH/CwPkhFPwuVBgfVNiajbrcGF6i5VWzIHCXT+IDe\naSw64dqOqQKBgDp9EYpNTjXaeEaBCWVlLVIMZVunxotrwhiiotbwyAMOz05vRTQW\nVivg9c4nFCVSRf+1c/D+T5Vig5UG3hK9B6Xn0Fah1R3kJcKq+frey/2cYipRcO4c\n8UAhg0lwfFvOadUEnTT4/4HSlCmNZjhikDOWBS1ELZ5DY5j8V0JZFPPXAoGAXi0B\nLjw2dbwGbTYLk/ukljMBZvdjNtLolGiO22lghbaEIVCa5Ewij4+OLtO0LYabGL9/\nSnZF9ZkFwmlNjFxjMBSxfG2X2SIXflyR8V7Vv6btob7lyRUn0H2RsA5H5MB7bAA8\ntE0MNK5Y8zR6j19dEeXnwev3ClymQBT3xmx72WkCgYAEhx+piCTvD+2SFBweM3J4\ng6LefBXFWHSlftQF1Er5mI+5kr4i3M5d6zh8lKcLpygSS1q5/03CMUxEiLTZMLmG\nVX/+UgEWzdEe1Dr/7YPYcOSPiBpTRyPiq6KQC/KtPPWHouGyaPO1SAAZhlk97dQs\n9mosf4Re69Re8U6MxxGw9Q==\n-----END PRIVATE KEY-----\n",
"client_email": "image-search-service@ncp-1-483610.iam.gserviceaccount.com",
"client_id": "105492884344006453429",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/image-search-service%40ncp-1-483610.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
@@ -1,2 +0,0 @@
CREATE INDEX idx_image_hash_prefix
ON image_hashes ((hash_value >> 48));
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Подтверждение email</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 20px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 800;
margin-bottom: 10px;
letter-spacing: 1px;
}
.content {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
margin-bottom: 20px;
}
.username {
color: #667eea;
font-weight: bold;
}
.code-container {
background: #f8f9fa;
border: 2px dashed #667eea;
border-radius: 10px;
padding: 25px;
text-align: center;
margin: 30px 0;
}
.verification-code {
font-size: 42px;
font-weight: 800;
letter-spacing: 8px;
color: #333;
margin: 0;
}
.info-text {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 8px;
margin: 25px 0;
font-size: 14px;
}
.footer {
text-align: center;
padding: 25px;
color: #666;
font-size: 12px;
border-top: 1px solid #eee;
background: #f9f9f9;
}
.highlight {
color: #764ba2;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">NO COPY</div>
<h1>Подтверждение email адреса</h1>
</div>
<div class="content">
<p class="greeting">
Здравствуйте, <span class="username">{{username}}</span>!
</p>
<p>Спасибо за регистрацию на платформе NO COPY.
Для завершения регистрации, пожалуйста, введите код подтверждения:</p>
<div class="code-container">
<p class="verification-code">{{code}}</p>
<p class="info-text">Код подтверждения</p>
</div>
<p class="info-text">
Код действителен в течение <span class="highlight">15 минут</span>
</p>
<div class="warning">
<strong>Важно:</strong> Если вы не регистрировались на NO COPY,
просто проигнорируйте это письмо.
</div>
</div>
<div class="footer">
<p>© 2024 NO COPY. Все права защищены.</p>
<p>Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.</p>
<p>Если у вас возникли вопросы, обратитесь в службу поддержки.</p>
</div>
</div>
</body>
</html>