dev
Test Workflow / test (push) Successful in 3s

This commit is contained in:
vladp
2026-01-27 20:27:39 +07:00
parent cf2f73955b
commit 4a185e6f9a
12 changed files with 185 additions and 21 deletions
+4 -3
View File
@@ -88,17 +88,18 @@ services:
# FILE_CHUNK_SIZE: 1048576 # FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000 FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
POSTGRES_HOST: db POSTGRES_HOST: db
STORAGE_SERVICE_URL: http://storage:8081 STORAGE_SERVICE_URL: http://storage:8081
SPRING_PROFILES_ACTIVE: docker SPRING_PROFILES_ACTIVE: prod
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# MAIL_HOST: postfix # MAIL_HOST: postfix
# MAIL_PORT: 25 # MAIL_PORT: 25
# MAIL_USERNAME: noreply@no-copy.ru # MAIL_USERNAME: noreply@no-copy.ru
@@ -0,0 +1,25 @@
package ru.soune.nocopy.configuration.file;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
@Configuration
@ConfigurationProperties(prefix = "file.storage")
@Data
public class FileStorageConfig {
private String basePath;
private long maxFileSize;
private int chunkSize;
private int authTokenLifeHours;
@PostConstruct
public void init() throws IOException {
Files.createDirectories(Paths.get(basePath));
}
}
@@ -0,0 +1,64 @@
package ru.soune.nocopy.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileStorageService;
import java.io.IOException;
@RestController
@RequestMapping("/api/files")
@Slf4j
public class FileController {
@Autowired
private FileStorageService fileStorageService;
@Autowired
private FileEntityRepository fileRepository;
@GetMapping("/public/{fileId}")
public ResponseEntity<Resource> getPublicFile(
@PathVariable String fileId) throws IOException {
FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found"));
// if (fileEntity.getProtectionStatus() != ProtectionStatus.NOT_PROTECTED) {
// throw new RuntimeException("File is protected");
// }
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.body(resource);
}
@GetMapping("/download/{fileId}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) throws IOException {
FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found"));
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.body(resource);
}
}
@@ -2,6 +2,9 @@ package ru.soune.nocopy.dto.file;
import lombok.Builder; import lombok.Builder;
import lombok.Value; import lombok.Value;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.time.LocalDateTime;
@Value @Value
@Builder @Builder
@@ -12,4 +15,8 @@ public class SimilarFileDTO {
Integer hammingDistance; Integer hammingDistance;
String similarityLevel; String similarityLevel;
Long ownerId; Long ownerId;
Long supportId;
ProtectionStatus status;
LocalDateTime uploadDate;
String url;
} }
@@ -3,13 +3,10 @@ package ru.soune.nocopy.entity.file;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.hibernate.annotations.GenerationTime;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Data @Data
@@ -16,7 +16,10 @@ public interface ImageSimilarityRepository
f.id AS id, 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, f.user_id AS userId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus,
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
@@ -37,6 +40,9 @@ public interface ImageSimilarityRepository
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, f.user_id AS userId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus,
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
@@ -61,7 +67,10 @@ public interface ImageSimilarityRepository
f.user_id AS userId, f.user_id AS userId,
f.original_file_name AS originalFileName, f.original_file_name AS originalFileName,
f.file_size AS fileSize, f.file_size AS fileSize,
f.stored_file_name AS similarFileId f.stored_file_name AS similarFileId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus,
FROM image_hashes h FROM image_hashes h
JOIN file_entities f ON f.id = h.file_id JOIN file_entities f ON f.id = h.file_id
WHERE h.hash64_hi = :hash64Hi WHERE h.hash64_hi = :hash64Hi
@@ -1,5 +1,9 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.time.LocalDateTime;
public interface SimilarImageProjection { public interface SimilarImageProjection {
Long getHash64Hi(); Long getHash64Hi();
Long getHash64Lo(); Long getHash64Lo();
@@ -7,4 +11,7 @@ public interface SimilarImageProjection {
Long getUserId(); Long getUserId();
String getOriginalFileName(); String getOriginalFileName();
Long getFileSize(); Long getFileSize();
Long getSupportId();
LocalDateTime getUploadDate();
ProtectionStatus getProtectionStatus();
} }
@@ -1,7 +1,10 @@
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.beans.factory.annotation.Value;
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;
@@ -31,6 +34,9 @@ public class FileSimilarityService {
private final AuthTokenRepository authTokenRepository; private final AuthTokenRepository authTokenRepository;
@Value("${server.baseurl}")
private String baseUrl;
public List<SimilarFileDTO> findSimilarFiles(String fileId) { 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"));
@@ -167,11 +173,11 @@ public class FileSimilarityService {
Long hash64Hi, Long hash64Lo) { Long hash64Hi, Long hash64Lo) {
Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi();
Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo();
int hamming = PerceptualHashHelper.INSTANCE.hammingDistance(new PHash(hash64Hi, hash64Lo),
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo); new PHash(imageProjectionHash64Hi, similarImageProjectionHash64Lo));
String level; String level;
if (hamming <= 5) { if (hamming <= 6) {
level = "DUPLICATE"; level = "DUPLICATE";
} else if (hamming <= 12) { } else if (hamming <= 12) {
level = "SIMILAR"; level = "SIMILAR";
@@ -186,6 +192,10 @@ public class FileSimilarityService {
.fileSize(similarImageProjection.getFileSize()) .fileSize(similarImageProjection.getFileSize())
.hammingDistance(hamming) .hammingDistance(hamming)
.similarityLevel(level) .similarityLevel(level)
.supportId(similarImageProjection.getSupportId())
.uploadDate(similarImageProjection.getUploadDate())
.status(similarImageProjection.getProtectionStatus())
.url(baseUrl + "/api/files/public/" + similarImageProjection.getId())
.build(); .build();
} }
} }
@@ -0,0 +1,36 @@
package ru.soune.nocopy.service.file;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.configuration.file.FileStorageConfig;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
@Slf4j
public class FileStorageService {
@Autowired
private FileStorageConfig storageConfig;
public Resource loadFileAsResource(String filePath) throws IOException {
Path path = Paths.get(storageConfig.getBasePath()).resolve(filePath).normalize();
Resource resource = new UrlResource(path.toUri());
if (!resource.exists()) {
throw new FileNotFoundException("File not found: " + filePath);
}
if (!resource.isReadable()) {
throw new IOException("File is not readable: " + filePath);
}
return resource;
}
}
+2
View File
@@ -0,0 +1,2 @@
server:
baseurl: "http://localhost"
+2
View File
@@ -0,0 +1,2 @@
server:
baseurl: "http://92.242.61.23"
+14 -10
View File
@@ -1,4 +1,8 @@
spring: spring:
config:
activate:
on-profile: ${SPRING_PROFILES_ACTIVE:dev}
mail: mail:
host: smtp.gmail.com host: smtp.gmail.com
port: 587 port: 587
@@ -11,15 +15,15 @@ spring:
starttls: starttls:
enable: true enable: true
# mail: # mail:
# host: postfix # host: postfix
# port: 25 # port: 25
# username: noreply@nocopy.com # username: noreply@nocopy.com
# password: nocopy!nocopy! # password: nocopy!nocopy!
# properties: # properties:
# mail.smtp.auth: false # mail.smtp.auth: false
# mail.smtp.starttls.enable: false # mail.smtp.starttls.enable: false
# mail.debug: true # mail.debug: true
flyway: flyway:
enabled: true enabled: true
@@ -87,5 +91,5 @@ app:
email: email:
verification: verification:
standart-subject: NO COPY - Подтверждение email адреса standart-subject: NO COPY - Подтверждение email адреса
standart-address-from: NO COPY <noreply@nocopy.com> standart-address-from: vlad.popovtsev@gmail.com
user: Пользователь user: Пользователь