NCBACK-33 #16

Merged
backdev merged 13 commits from NCBACK-33 into dev 2026-01-27 09:11:31 +08:00
47 changed files with 1458 additions and 431 deletions
Showing only changes of commit 816a984891 - Show all commits
+9
View File
@@ -21,6 +21,9 @@ configurations {
repositories { repositories {
mavenCentral() mavenCentral()
flatDir {
dirs 'libs'
}
} }
dependencies { dependencies {
@@ -32,6 +35,9 @@ dependencies {
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: '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'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0'
@@ -52,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') {
+3 -2
View File
@@ -77,11 +77,12 @@ 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: 1048576
+10 -2
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 {
@@ -48,7 +48,15 @@ pipeline {
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' rsync -av --delete \\
--exclude=.git \\
--exclude=.gradle \\
--exclude=build \\
--exclude=**/build \\
--exclude=.idea \\
--exclude=out \\
. $SSH_USER@$SERVER:/opt/deployments/${params.BRANCH}/
sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER " sshpass -p '$SSH_PASS' ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER "
cd /opt/deployments/${params.BRANCH} cd /opt/deployments/${params.BRANCH}
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
);
} }
}
@@ -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;
@@ -21,6 +24,7 @@ import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.dto.register.RegAnswer; 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.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.*;
@@ -29,7 +33,9 @@ import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.register.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;
@@ -57,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) {
@@ -103,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) 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");
@@ -115,39 +126,54 @@ 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());
} }
} }
@GetMapping("/v{version}/files/{fileId}/similar") @GetMapping("/v{version}/files/{fileId}/similar/{authToken}")
public ResponseEntity<BaseResponse> findSimilarFiles( public ResponseEntity<BaseResponse> findSimilarFiles(
@PathVariable("version") int version, @PathVariable("version") int version,
@PathVariable String fileId, @PathVariable String fileId,
@PathVariable(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<>();
@@ -193,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));
} }
} }
@@ -352,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);
@@ -360,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);
@@ -369,11 +435,11 @@ 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);
@@ -396,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();
@@ -459,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;
}
} }
@@ -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;
} }
@@ -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;
@@ -1,51 +0,0 @@
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.entity.user.UserContent;
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;
}
@@ -0,0 +1,9 @@
package ru.soune.nocopy.entity.file;
public enum ProtectionStatus {
NOT_PROTECTED,
PROCESSING,
PROTECTED,
FAILED,
FAILED_SAVE
}
@@ -84,10 +84,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<Violation> violations = new ArrayList<>(); private List<Violation> violations = new ArrayList<>();}
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
@ToString.Exclude
private List<ImageProtection> imageProtections = new ArrayList<>();
}
@@ -274,7 +274,7 @@ public class FileEntityHandler implements RequestHandler {
FileEntity fileEntity = fileEntityRepository.findById(fileId) FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId)); .orElseThrow(() -> new FileEntityNotFoundException(fileId));
if (fileEntity.getStatus().equals(FileStatus.DELETED)) { if (fileEntity.getStatus().equals(FileStatus.DELETED) || fileRequest.getFullDelete() == 1) {
fileEntityService.deleteFromDisk(fileEntity); fileEntityService.deleteFromDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
@@ -7,9 +7,12 @@ 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.GoogleVisionSearchResponse;
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.search.YandexSearchService; import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.GoogleVisionSearchService;
import ru.soune.nocopy.service.YandexSearchService;
@Slf4j @Slf4j
@Component @Component
@@ -20,6 +23,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(),
@@ -29,6 +34,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);
} }
@@ -138,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) {
@@ -1,6 +1,7 @@
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.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
@@ -13,6 +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> findByUserIdAndIsActive(Long userId, boolean isActive);
Optional<AuthToken> findByUserId(Long userId); Optional<AuthToken> 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 similarFileId, f.id AS id,
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();
@@ -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);
}
} }
@@ -0,0 +1,205 @@
package ru.soune.nocopy.service;
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,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;
}
} }
@@ -9,6 +9,7 @@ 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;
@@ -16,6 +17,7 @@ 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;
@@ -47,7 +49,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);
@@ -110,6 +112,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(
@@ -167,7 +202,8 @@ public class FileEntityService {
Files.delete(path); Files.delete(path);
markAsDeleted(fileEntity); // markAsDeleted(fileEntity);
fileEntityRepository.delete(fileEntity);
return true; return true;
} }
@@ -178,6 +214,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 +316,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 +345,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).getId(), 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,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;
}
}
}
@@ -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));