This commit is contained in:
@@ -114,7 +114,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");
|
||||||
@@ -126,9 +127,11 @@ 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) {
|
||||||
Map<String, Object> duplicateData = new HashMap<>();
|
Map<String, Object> duplicateData = new HashMap<>();
|
||||||
duplicateData.put("duplicateFileId", e.duplicateFileId());
|
duplicateData.put("duplicateFileId", e.duplicateFileId());
|
||||||
@@ -151,16 +154,17 @@ public class ApiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable);
|
Page<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken);
|
||||||
|
|
||||||
String messageDesc;
|
String messageDesc;
|
||||||
MessageCode success;
|
MessageCode success;
|
||||||
@@ -459,11 +463,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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,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())
|
||||||
|
|||||||
@@ -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.AuthToken;
|
import ru.soune.nocopy.entity.AuthToken;
|
||||||
|
|
||||||
@@ -13,4 +14,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,5 +46,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
|
|||||||
@Query("SELECT f FROM FileEntity f WHERE f.signature = :signature")
|
@Query("SELECT f FROM FileEntity f WHERE f.signature = :signature")
|
||||||
FileEntity findBySignature(@Param("signature") String 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,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,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import ru.soune.nocopy.dto.file.SimilarFileDTO;
|
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.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;
|
||||||
@@ -28,6 +29,8 @@ public class FileSimilarityService {
|
|||||||
|
|
||||||
private final FileUtil fileUtil;
|
private final FileUtil fileUtil;
|
||||||
|
|
||||||
|
private final AuthTokenRepository authTokenRepository;
|
||||||
|
|
||||||
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"));
|
||||||
@@ -128,14 +131,15 @@ public class FileSimilarityService {
|
|||||||
return duplicates;
|
return duplicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<SimilarFileDTO> 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"));
|
||||||
|
|
||||||
Long hash64Hi = imageHashEntity.getHash64Hi();
|
Long hash64Hi = imageHashEntity.getHash64Hi();
|
||||||
Long 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()
|
||||||
|
|||||||
@@ -202,7 +202,8 @@ public class FileEntityService {
|
|||||||
|
|
||||||
Files.delete(path);
|
Files.delete(path);
|
||||||
|
|
||||||
markAsDeleted(fileEntity);
|
// markAsDeleted(fileEntity);
|
||||||
|
fileEntityRepository.delete(fileEntity);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -248,6 +249,10 @@ public class FileEntityService {
|
|||||||
return fileEntityRepository.findBySignature(signature);
|
return fileEntityRepository.findBySignature(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String findFileIdByPath(String filePath) {
|
||||||
|
return fileEntityRepository.findFileIdByFilePath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
public File getFileById(String id) {
|
public File getFileById(String id) {
|
||||||
try {
|
try {
|
||||||
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
|
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public interface FileUploadService {
|
|||||||
|
|
||||||
void cancelUpload(String uploadId);
|
void cancelUpload(String uploadId);
|
||||||
|
|
||||||
void uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile);
|
UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, Integer findSimilar);
|
||||||
|
|
||||||
UploadProgressResponse getUploadProgress(String uploadId);
|
UploadProgressResponse getUploadProgress(String uploadId);
|
||||||
|
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ public class FileUploadServiceImpl implements FileUploadService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void 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));
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
|||||||
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
|
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
processChunk(session, chunkNumber, chunkFile);
|
return processChunk(session, chunkNumber, chunkFile, findSimilar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -285,12 +285,13 @@ public class FileUploadServiceImpl implements FileUploadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile) {
|
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
|
||||||
|
Integer findSimilar) {
|
||||||
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);
|
||||||
@@ -311,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +382,8 @@ public class FileUploadServiceImpl implements FileUploadService {
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -390,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);
|
||||||
@@ -401,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user