package ru.soune.nocopy.service.file; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; 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.ProtectionStatus; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class FileEntityService { private final FileEntityRepository fileEntityRepository; private final UserRepository userRepository; private final ImageResizeService imageResizeService; private final CloudStorageService cloudStorageService; @Value("${server.baseurl}") private String baseUrl; @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 List getAllUserFiles(Long userId) { 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(getAllUserFiles(uId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); } } else { allFiles = getAllUserFiles(userId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } return allFiles; } public List getAllUserFiles(Long userId, List statuses) { 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.findByUserIdAndStatusIn(uId, statuses)); } } else { allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, statuses); } return allFiles; } @Transactional(readOnly = true) public FileResponse getUserFiles(Long userId, Pageable pageable, int version) { User user = userRepository.findById(userId).orElseThrow(); List users = user.getCompany() != null ? userRepository.findByCompanyId(user.getCompany().getId()) : List.of(user); List statusesForSearch = List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION); Page files = fileEntityRepository.findFiles(users.stream().map(User::getId).toList(), statusesForSearch, pageable); Long totalSize = fileEntityRepository.sumFileSize( users.stream().map(User::getId).toList(), statusesForSearch); List filesToResponse = files.stream() .map(file -> convertToResponse(file, version)) .toList(); return FileResponse.builder() .files(filesToResponse) .totalCount((int) files.getTotalElements()) .totalSize(totalSize != null ? totalSize : 0L) .formattedTotalSize(formatFileSize(totalSize != null ? totalSize : 0L)) .page(pageable.getPageNumber()) .pageSize(pageable.getPageSize()) .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.findByUserIdAndStatusIn(uId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION))); } } else { allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)); } 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(); } public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { if (fileEntity.getImageHash() != null) { fileEntity.setImageHash(null); } fileEntity.setStatus(FileStatus.REMOVED); 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(String deleteFilePath) throws IOException { Files.deleteIfExists(Paths.get(deleteFilePath)); } @Transactional(readOnly = true) public long getUserStorageUsed(Long userId) { Long totalSize = fileEntityRepository.getTotalSizeByUserId(userId); return totalSize != null ? totalSize : 0L; } public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); fileEntity.setProtectionStatus(newStatus); fileEntityRepository.save(fileEntity); } public void changeFileStatus(FileStatus newStatus, String fileId) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); fileEntity.setStatus(newStatus); fileEntityRepository.save(fileEntity); } @Transactional public FileEntity 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); Files.createDirectories(protectedFilePath.getParent()); Files.deleteIfExists(protectedFilePath); try (FileChannel channel = FileChannel.open(protectedFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { ByteBuffer buffer = ByteBuffer.wrap(data); while (buffer.hasRemaining()) { channel.write(buffer); } channel.force(true); } if (imageResizeService.isImage(extension)) { imageResizeService.generateSizes(fileEntity, data); } fileEntity.setProtectedFilePath(protectedFilePath.toString()); fileEntity.setProtectedAt(LocalDateTime.now()); fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntity.setFileExtension(extension); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); return fileEntityRepository.save(fileEntity); } public void clearTempFiles(long userId) { List fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP); for (FileEntity fileEntity : fileByUserIdAndStatus) { cloudStorageService.deleteFromStorageDisk(fileEntity); } } public FileEntity findBySignature(String signature) { return fileEntityRepository.findBySignature(signature); } public String findFileIdByPath(String filePath) { return fileEntityRepository.findFileIdByFilePath(filePath); } 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 FileEntity 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()); return fileEntityRepository.save(fileEntity); } 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)); } } // 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 final FileMonitoringRepository fileMonitoringRepository; private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { // boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); User user = userRepository.findById(fileEntity.getUserId()).get(); String fileName = fileEntity.getOriginalFileName(); String name = fileName; Optional fileMonitoringRepositoryByFileId = fileMonitoringRepository.findByFileId(fileEntity.getId()); String monitoring = fileMonitoringRepositoryByFileId.map(fileMonitoringEntity -> fileMonitoringEntity.getMonitoringType().name()) .orElseGet(MonitoringType.NONE::name); int lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex > 0) { name = fileName.substring(0, lastDotIndex); } return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) .originalFileName(name + "." + fileEntity.getFileExtension()) .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(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .ownerName(user.getFullName()) .ownerEmail(user.getEmail()) .ownerCompany(user.getCompanyName()) .monitoring(monitoring) //TODO fix after add logic for check for protect file .checksCount(0) .fileUploadDate(fileEntity.getCreatedAt()) .protectedFilePath(fileEntity.getProtectedFilePath()) .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail") .build(); } }