diff --git a/build.gradle b/build.gradle index d42173a..788db2c 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,9 @@ configurations { repositories { mavenCentral() + flatDir { + dirs 'libs' + } } dependencies { @@ -45,6 +48,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mockito:mockito-core:5.3.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.0' + + implementation name: 'testlib-0.1.2' } tasks.named('test') { diff --git a/libs/testlib-0.1.2.jar b/libs/testlib-0.1.2.jar new file mode 100644 index 0000000..28fcd06 Binary files /dev/null and b/libs/testlib-0.1.2.jar differ diff --git a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java index 85ae0b8..2817654 100644 --- a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java @@ -1,17 +1,34 @@ package ru.soune.nocopy.configuration; +import com.vrt.NoCopyFileService; +import com.vrt.fileprocessor.FileProcessor; +import lombok.AllArgsConstructor; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.Collections; +import java.util.List; + @Configuration @EnableAutoConfiguration +@AllArgsConstructor public class ApplicationConfig { @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + @Scope("singleton") + public NoCopyFileService noCopyFileService(FileProcessor.FileProvider fileProvider, + FileProcessor.ProcessingListener processingListener) { + List initialQueue = Collections.emptyList(); + + return new NoCopyFileService(initialQueue, fileProvider, processingListener); + } } diff --git a/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java b/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java new file mode 100644 index 0000000..39eef84 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.configuration.file; + +import lombok.AllArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.service.file.FileProcessingOrchestrator; + +@Component +@AllArgsConstructor +public class NoCopyInitializer { + + private final FileProcessingOrchestrator orchestrator; + + @EventListener(ApplicationReadyEvent.class) + public void initializeOnStartup() { + orchestrator.initializeProcessingQueue(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index ffdd985..eb68451 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -60,11 +60,26 @@ public class FileEntity { @Column(name = "updated_at") private LocalDateTime updatedAt; + @Column(name = "protected_file_path") + private String protectedFilePath; + + @Column(name = "protection_status") + @Enumerated(EnumType.STRING) + private ProtectionStatus protectionStatus; + + @Column(name = "protected_at") + private LocalDateTime protectedAt; + @PrePersist public void prePersist() { if (this.status == null) { this.status = FileStatus.ACTIVE; } + + if (this.protectionStatus == null) { + this.protectionStatus = ProtectionStatus.NOT_PROTECTED; + } + if (this.createdAt == null) { this.createdAt = LocalDateTime.now(); } diff --git a/src/main/java/ru/soune/nocopy/entity/file/ProtectionStatus.java b/src/main/java/ru/soune/nocopy/entity/file/ProtectionStatus.java new file mode 100644 index 0000000..4ffa30c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/ProtectionStatus.java @@ -0,0 +1,8 @@ +package ru.soune.nocopy.entity.file; + +public enum ProtectionStatus { + NOT_PROTECTED, + PROCESSING, + PROTECTED, + FAILED +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 3ee41cb..d2788d8 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; +import ru.soune.nocopy.entity.file.ProtectionStatus; import java.util.List; import java.util.Optional; @@ -21,6 +22,8 @@ public interface FileEntityRepository extends JpaRepository Optional findByUploadSessionId(String uploadSessionId); + List findByProtectionStatus(ProtectionStatus protectionStatus); + boolean existsByFilePath(String filePath); @Query("SELECT SUM(f.fileSize) FROM FileEntity f WHERE f.userId = :userId AND f.status = 'ACTIVE'") diff --git a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java new file mode 100644 index 0000000..6fe56cf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java @@ -0,0 +1,73 @@ +package ru.soune.nocopy.service.file; + +import com.vrt.NoCopyFileService; +import com.vrt.fileprocessor.FileProcessor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.file.ProtectionStatus; +import ru.soune.nocopy.repository.FileEntityRepository; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FileProcessingOrchestrator { + private final NoCopyFileService noCopyFileService; + + private final FileEntityRepository fileRepository; + + public void initializeProcessingQueue() { + List filesToProtect = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED); + + for (FileEntity fileEntity : filesToProtect) { + try { + FileProcessor.FileInfo fileInfo = createFileInfo(fileEntity); + + noCopyFileService.addFile(fileInfo); + + log.info("Add to query: {}", fileEntity.getOriginalFileName()); + } catch (Exception e) { + log.error("Fail add to query: {}", fileEntity.getId(), e); + } + } + } + + @Scheduled(fixedDelay = 120000) + public void checkNewFilesForProtection() { + List newFiles = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED); + + for (FileEntity fileEntity : newFiles) { + FileProcessor.FileInfo fileInfo = createFileInfo(fileEntity); + noCopyFileService.addFile(fileInfo); + + fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); + fileRepository.save(fileEntity); + } + } + + private FileProcessor.FileInfo createFileInfo(FileEntity fileEntity) { + FileProcessor.Type type = determineFileType(fileEntity.getMimeType()); + + return new FileProcessor.FileInfo(type, fileEntity.getId()); + } + + private FileProcessor.Type determineFileType(String mimeType) { + if (mimeType == null) { + return FileProcessor.Type.IMAGE; + } + + if (mimeType.startsWith("image")) { + return FileProcessor.Type.IMAGE; + } else if (mimeType.startsWith("video")) { + return FileProcessor.Type.VIDEO; + } else if (mimeType.startsWith("audio")) { + return FileProcessor.Type.AUDIO; + } else { + return FileProcessor.Type.IMAGE; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/NoCopyProcessingListener.java b/src/main/java/ru/soune/nocopy/service/file/NoCopyProcessingListener.java new file mode 100644 index 0000000..244d26f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/NoCopyProcessingListener.java @@ -0,0 +1,26 @@ +package ru.soune.nocopy.service.file; + +import com.vrt.fileprocessor.FileProcessor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +//Менять статус защиты +@Slf4j +@Component +public class NoCopyProcessingListener implements FileProcessor.ProcessingListener { + + @Override + public void onStartProcessing(FileProcessor.FileInfo fileInfo) { + } + + @Override + public void onProcessingFailed(FileProcessor.FileInfo fileInfo) { + } + + @Override + public void onSavingFailed(FileProcessor.FileInfo fileInfo) { + } + + @Override + public void onFinish(FileProcessor.FileInfo fileInfo) { + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/ProtectionFileProvider.java b/src/main/java/ru/soune/nocopy/service/file/ProtectionFileProvider.java new file mode 100644 index 0000000..33aea6c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/ProtectionFileProvider.java @@ -0,0 +1,149 @@ +package ru.soune.nocopy.service.file; + +import com.vrt.fileprocessor.FileProcessor; +import kotlin.Result; +import kotlin.Unit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.file.ProtectionStatus; +import ru.soune.nocopy.repository.FileEntityRepository; + +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; +//Сохранение и запись защищенных файлов +@Slf4j +@Component +@RequiredArgsConstructor +public class ProtectionFileProvider implements FileProcessor.FileProvider { + + private final FileEntityRepository fileRepository; + +// @Override + public Result writeAudioFile(String id, byte[] data, String fileExt) { + try { + writeProtectedFile(id, data, fileExt); + return new Result<>(); + } catch (Exception e) { + log.error("Failed to create protected file: {}", id, e); + return new Result<>(); + } + } + + // @Override + public Result writeImageFile(String id, byte[] data, String fileExt) { + try { + writeProtectedFile(id, data, fileExt); + return new Result<>(); + } catch (Exception e) { + log.error("Failed to create protected file: {}", id, e); + return new Result<>(); + } + } + + // @Override + public Result writeVideoFile(String id, byte[] data, String fileExt) { + try { + writeProtectedFile(id, data, fileExt); + return new Result<>(); + } catch (Exception e) { + log.error("Failed to create protected file: {}", id, e); + return new Result<>(); + } + } + + @Override + public File getImageFile(String id) { + return getFileById(id); + } + + @Override + public File getVideoFile(String id) { + return getFileById(id); + } + + @Override + public File getAudioFile(String id) { + return getFileById(id); + } + + + private void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { + FileEntity fileEntity = fileRepository.findById(id) + .orElseThrow(() -> new RuntimeException("File not found: " + id)); + + String extension = determineFileExtension(fileExt, fileEntity); + Path protectedFilePath = prepareProtectedPath(fileEntity, extension); + + if (Files.exists(protectedFilePath)) { + Files.delete(protectedFilePath); + } + + Files.write(protectedFilePath, data); + + fileEntity.setProtectedFilePath(protectedFilePath.toString()); + fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); + fileEntity.setProtectedAt(LocalDateTime.now()); + fileEntity.setUpdatedAt(LocalDateTime.now()); + fileEntity.setFileExtension(extension); + + fileRepository.save(fileEntity); + } + + + private File getFileById(String id) { + try { + FileEntity fileEntity = fileRepository.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; + } + } + + private String determineFileExtension(String fileExt, FileEntity fileEntity) { + if (fileExt != null && !fileExt.trim().isEmpty()) { + return fileExt; + } else { + return fileEntity.getFileExtension(); + } + } + + private Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException { + Path originalPath = Paths.get(fileEntity.getFilePath()); + + String pathStr = originalPath.toString(); + pathStr = pathStr.replaceFirst("/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; + } +}