package ru.soune.nocopy.service.file; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.FileResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.ImageHashService; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class FileEntityService { private final FileEntityRepository fileEntityRepository; private final ImageHashService imageHashService; private final FileSimilarityService fileSimilarityService; private final UserRepository userRepository; @Transactional(noRollbackFor = DuplicateImageException.class) public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { log.info("Creating FileEntity for upload session: {}", session.getUploadId()); try { Path filePath = Paths.get(session.getFilePath()); if (!Files.exists(filePath)) { throw new IOException("File not found on disk: " + filePath); } Map imageHash = Map.of(); if (session.getFileType().startsWith("image")) { imageHash = imageHashService.calculateHash(filePath); List duplicatedByHash = fileSimilarityService.findDuplicatedByHash( imageHash.get("hi"), imageHash.get("low")); if (!duplicatedByHash.isEmpty()) { SimilarImageProjection similarImageProjection = duplicatedByHash.get(0); throw new DuplicateImageException("Duplicate", similarImageProjection.getId(), similarImageProjection.getUserId()); } } long fileSize = Files.size(filePath); String originalName = session.getFileName(); String storedName = filePath.getFileName().toString(); FileEntity fileEntity = FileEntity.builder() .userId(session.getUserId()) .originalFileName(originalName) .storedFileName(storedName) .filePath(session.getFilePath()) .fileSize(fileSize) .mimeType(session.getFileType()) .fileExtension(session.getExtension()) .checksum(checksum) .uploadSessionId(session.getUploadId()) .status(FileStatus.ACTIVE) .build(); FileEntity saved = fileEntityRepository.save(fileEntity); if (!imageHash.isEmpty()) { imageHashService.create(saved, imageHash); } return saved; } catch (IOException e) { log.error("Failed to create FileEntity for session {}: {}", session.getUploadId(), e.getMessage(), e); throw new RuntimeException("Failed to create file entity: " + e.getMessage(), e); } } @Transactional(readOnly = true) public FileEntityResponse getById(String fileId, int version) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); return convertToResponse(fileEntity, version); } @Transactional(readOnly = true) public FileEntityResponse getByUploadSessionId(String uploadSessionId, int version) { FileEntity fileEntity = fileEntityRepository.findByUploadSessionId(uploadSessionId) .orElseThrow(() -> new FileEntityNotFoundException( "Not found for upload session: " + uploadSessionId)); 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 fileEntities = fileEntityRepository.findByUserIdAndStatus( userId, FileStatus.ACTIVE); List 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) public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { User user = userRepository.findById(userId).orElseThrow(); List allFiles = new ArrayList<>(); if (user.getCompany() != null) { for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() .map(User::getId) .toList()) { allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE)); } } else { allFiles = fileEntityRepository.findByUserIdAndStatus( userId, FileStatus.ACTIVE); } int start = (page - 1) * pageSize; int end = Math.min(start + pageSize, allFiles.size()); if (start >= allFiles.size()) { return FileResponse.builder() .files(List.of()) .totalCount(allFiles.size()) .totalSize(0) .formattedTotalSize("0 B") .page(page) .pageSize(pageSize) .build(); } List pageFiles = allFiles.subList(start, end); List files = pageFiles.stream() .map(file -> convertToResponse(file, version)) .collect(Collectors.toList()); long totalSize = allFiles.stream() .mapToLong(FileEntity::getFileSize) .sum(); return FileResponse.builder() .files(files) .totalCount(allFiles.size()) .totalSize(totalSize) .formattedTotalSize(formatFileSize(totalSize)) .page(page) .pageSize(pageSize) .build(); } @Transactional public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException { fileEntity.setStatus(FileStatus.DELETED); fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); Path path = Paths.get(fileEntity.getProtectedFilePath()); Files.deleteIfExists(path); fileEntity.setProtectedFilePath(""); return fileEntityRepository.save(fileEntity); } public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { if (fileEntity.getImageHash() != null) { fileEntity.setImageHash(null); } fileEntity.setStatus(FileStatus.DELETED); fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); Path path = Paths.get(fileEntity.getProtectedFilePath()); Files.deleteIfExists(path); fileEntity.setProtectedFilePath(""); fileEntityRepository.save(fileEntity); } public void deleteFromDisk(FileEntity fileEntity) throws IOException { Path path = Paths.get(fileEntity.getFilePath()); if (!Files.exists(path)) { return; } Files.delete(path); fileEntityRepository.delete(fileEntity); } @Transactional(readOnly = true) public long getUserStorageUsed(Long userId) { Long totalSize = fileEntityRepository.getTotalSizeByUserId(userId); 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); log.info("EXTENSION: {}", extension); 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 void clearTempFiles(long userId) throws IOException { List fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP); for (FileEntity fileEntity : fileByUserIdAndStatus) { deleteFromDisk(fileEntity); } } public FileEntity findBySignature(String signature) { return fileEntityRepository.findBySignature(signature); } public String findFileIdByPath(String filePath) { return fileEntityRepository.findFileIdByFilePath(filePath); } public File getFileById(String id) { try { FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() -> new RuntimeException("File not found: " + id)); File file = new File(fileEntity.getFilePath()); if (!file.exists()) { throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath()); } return file; } catch (Exception e) { log.error("Error getting file: {}", id, e); return null; } } public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException { Path originalPath = Paths.get(fileEntity.getFilePath()); String pathStr = originalPath.toString(); pathStr = pathStr.replaceFirst("/uploads/uploads/", "/uploads/protected/"); Path protectedPath = Paths.get(pathStr); String fileName = protectedPath.getFileName().toString(); if (extension != null) { String nameWithoutExt = fileName; int lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex > 0) { nameWithoutExt = fileName.substring(0, lastDotIndex); } fileName = nameWithoutExt + "." + extension; protectedPath = protectedPath.getParent().resolve(fileName); } Files.createDirectories(protectedPath.getParent()); return protectedPath; } public void updateSignature(String signature, String fileId) throws IOException { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); fileEntity.setSignature(signature); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntity.setProtectedAt(LocalDateTime.now()); fileEntity.setProtectedFilePath(prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString()); fileEntityRepository.save(fileEntity); } private boolean checkFileExistsOnDisk(String filePath) { try { return Files.exists(Paths.get(filePath)); } catch (Exception e) { log.warn("Error checking file existence: {}", filePath, e); return false; } } private String determineFileExtension(String fileExt, FileEntity fileEntity) { if (fileExt != null) { return fileExt; } else { return fileEntity.getFileExtension(); } } private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); User user = userRepository.findById(fileEntity.getUserId()).get(); return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) .originalFileName(fileEntity.getOriginalFileName()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) .mimeType(fileEntity.getMimeType()) .fileExtension(fileEntity.getFileExtension()) .checksum(fileEntity.getChecksum()) .uploadSessionId(fileEntity.getUploadSessionId()) .status(fileEntity.getStatus()) .createdAt(fileEntity.getCreatedAt()) .updatedAt(fileEntity.getUpdatedAt()) .formattedSize(formatFileSize(fileEntity.getFileSize())) .downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId()) .existsOnDisk(existsOnDisk) .supportId(fileEntity.getSupportId()) .protectStatus(fileEntity.getProtectionStatus().toString()) .fileName(fileEntity.getOriginalFileName().replace("." + fileEntity.getFileExtension(), "") + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .ownerName(user.getFullName()) .ownerEmail(user.getEmail()) .ownerCompany(user.getCompanyName()) //TODO fix after add logic for check for protect file .checksCount(0) .fileUploadDate(fileEntity.getCreatedAt()) .protectedFilePath(fileEntity.getProtectedFilePath()) .build(); } public String formatFileSize(long size) { if (size < 1024) { return size + " B"; } else if (size < 1024 * 1024) { return String.format("%.1f KB", size / 1024.0); } else if (size < 1024 * 1024 * 1024) { return String.format("%.1f MB", size / (1024.0 * 1024.0)); } else { return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0)); } } }