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.FileListResponse; 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.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; 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.List; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class FileEntityService { private final FileEntityRepository fileEntityRepository; /** * Создает FileEntity на основе завершенной сессии загрузки */ @Transactional 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); } long fileSize = Files.size(filePath); String originalName = session.getFileName(); String extension = extractFileExtension(originalName); 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(extension) .checksum(checksum) .uploadSessionId(session.getUploadId()) .status(FileStatus.ACTIVE) .build(); FileEntity saved = fileEntityRepository.save(fileEntity); log.info("FileEntity created successfully: {} (size: {} bytes)", saved.getId(), fileSize); 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); } } /** * Извлекает расширение файла из имени */ private String extractFileExtension(String fileName) { int dotIndex = fileName.lastIndexOf('.'); if (dotIndex > 0 && dotIndex < fileName.length() - 1) { return fileName.substring(dotIndex + 1).toLowerCase(); } return ""; } /** * Получает FileEntity по ID */ @Transactional(readOnly = true) public FileEntityResponse getById(String fileId) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); return convertToResponse(fileEntity); } /** * Получает FileEntity по ID сессии загрузки */ @Transactional(readOnly = true) public FileEntityResponse getByUploadSessionId(String uploadSessionId) { FileEntity fileEntity = fileEntityRepository.findByUploadSessionId(uploadSessionId) .orElseThrow(() -> new FileEntityNotFoundException( "Not found for upload session: " + uploadSessionId)); return convertToResponse(fileEntity); } /** * Получает FileEntity по пути файла */ @Transactional(readOnly = true) public FileEntityResponse getByFilePath(String filePath) { FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath) .orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath)); return convertToResponse(fileEntity); } /** * Получает все файлы пользователя */ @Transactional(readOnly = true) public FileListResponse getAllUserFiles(Long userId) { List fileEntities = fileEntityRepository.findByUserIdAndStatus( userId, FileStatus.ACTIVE); List files = fileEntities.stream() .map(this::convertToResponse) .collect(Collectors.toList()); long totalSize = fileEntities.stream() .mapToLong(FileEntity::getFileSize) .sum(); return FileListResponse.builder() .files(files) .totalCount(files.size()) .totalSize(totalSize) .formattedTotalSize(formatFileSize(totalSize)) .page(1) .pageSize(files.size()) .build(); } /** * Получает файлы пользователя с пагинацией */ @Transactional(readOnly = true) public FileListResponse getUserFiles(Long userId, int page, int pageSize) { List allFiles = fileEntityRepository.findByUserIdAndStatus( userId, FileStatus.ACTIVE); int start = (page - 1) * pageSize; int end = Math.min(start + pageSize, allFiles.size()); if (start >= allFiles.size()) { return FileListResponse.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(this::convertToResponse) .collect(Collectors.toList()); long totalSize = allFiles.stream() .mapToLong(FileEntity::getFileSize) .sum(); return FileListResponse.builder() .files(files) .totalCount(allFiles.size()) .totalSize(totalSize) .formattedTotalSize(formatFileSize(totalSize)) .page(page) .pageSize(pageSize) .build(); } /** * Помечает файл как удаленный (мягкое удаление) */ @Transactional public void markAsDeleted(String fileId) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); fileEntity.setStatus(FileStatus.DELETED); fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntityRepository.save(fileEntity); log.info("FileEntity marked as deleted: {}", fileId); } /** * Получает общий размер файлов пользователя */ @Transactional(readOnly = true) public long getUserStorageUsed(Long userId) { Long totalSize = fileEntityRepository.getTotalSizeByUserId(userId); return totalSize != null ? totalSize : 0L; } /** * Поиск файлов пользователя по имени */ @Transactional(readOnly = true) public List searchFiles(Long userId, String query) { List files = fileEntityRepository.searchByFileName(userId, query); return files.stream() .filter(f -> f.getStatus() == FileStatus.ACTIVE) .map(this::convertToResponse) .collect(Collectors.toList()); } /** * Проверяет, существует ли файл на диске */ 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; } } /** * Конвертирует FileEntity в DTO */ private FileEntityResponse convertToResponse(FileEntity fileEntity) { boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) .originalFileName(fileEntity.getOriginalFileName()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getFilePath()) .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/files/download/" + fileEntity.getId()) .existsOnDisk(existsOnDisk) .build(); } /** * Форматирует размер файла в читаемый вид */ private 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)); } } }