Merge branch 'dev' into NCBACK-35
# Conflicts: # README.md # src/main/java/ru/soune/nocopy/controller/ApiController.java # src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java # src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java # src/main/java/ru/soune/nocopy/service/file/FileEntityService.java # src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java # src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java
This commit is contained in:
@@ -38,7 +38,6 @@ public class FileCleanupService {
|
||||
public void cleanupExpiredFiles() {
|
||||
log.info("Starting cleanup of expired temporary files");
|
||||
|
||||
cleanupOldPeriods();
|
||||
cleanupTempFilesNightly();
|
||||
|
||||
Path tempDir = Paths.get(basePath, "temp");
|
||||
@@ -91,19 +90,6 @@ public class FileCleanupService {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupOldPeriods() {
|
||||
LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
|
||||
|
||||
List<ProtectedFileCheck> checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo);
|
||||
|
||||
for (ProtectedFileCheck check : checks) {
|
||||
check.cleanupOldData();
|
||||
}
|
||||
|
||||
protectedFileCheckRepository.saveAll(checks);
|
||||
log.info("Cleaned up {} old protection usage records", checks.size());
|
||||
}
|
||||
|
||||
private void cleanupTempFilesNightly() {
|
||||
log.info("Starting nightly cleanup of temporary files");
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@ package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.monitoring.FileMonitoringEntity;
|
||||
import ru.soune.nocopy.entity.monitoring.MonitoringType;
|
||||
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.FileMonitoringRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.FileSimilarityService;
|
||||
import ru.soune.nocopy.service.ImageHashService;
|
||||
@@ -27,7 +28,7 @@ import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -37,70 +38,14 @@ public class FileEntityService {
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final ImageHashService imageHashService;
|
||||
|
||||
private final FileSimilarityService fileSimilarityService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final ImageResizeService imageResizeService;
|
||||
|
||||
private final CloudStorageService cloudStorageService;
|
||||
|
||||
@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<String, Long> imageHash = Map.of();
|
||||
|
||||
if (!session.getFileType().startsWith("video")) {
|
||||
imageHash = imageHashService.calculateHash(filePath);
|
||||
List<SimilarImageProjection> 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);
|
||||
}
|
||||
}
|
||||
@Value("${server.baseurl}")
|
||||
private String baseUrl;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FileEntityResponse getById(String fileId, int version) {
|
||||
@@ -119,6 +64,34 @@ public class FileEntityService {
|
||||
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<FileEntity> getAllUserFiles(Long userId) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
List<FileEntity> 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)));
|
||||
}
|
||||
} else {
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
@@ -128,12 +101,13 @@ public class FileEntityService {
|
||||
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
|
||||
.map(User::getId)
|
||||
.toList()) {
|
||||
allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE));
|
||||
allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)));
|
||||
}
|
||||
|
||||
} else {
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatus(
|
||||
userId, FileStatus.ACTIVE);
|
||||
allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
|
||||
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED));
|
||||
}
|
||||
|
||||
int start = (page - 1) * pageSize;
|
||||
@@ -175,13 +149,13 @@ public class FileEntityService {
|
||||
fileEntity.setImageHash(null);
|
||||
}
|
||||
|
||||
fileEntity.setStatus(FileStatus.DELETED);
|
||||
fileEntity.setStatus(FileStatus.REMOVED);
|
||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
// fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
|
||||
Path path = Paths.get(fileEntity.getProtectedFilePath());
|
||||
Files.deleteIfExists(path);
|
||||
fileEntity.setProtectedFilePath("");
|
||||
// Path path = Paths.get(fileEntity.getProtectedFilePath());
|
||||
// Files.deleteIfExists(path);
|
||||
// fileEntity.setProtectedFilePath("");
|
||||
|
||||
fileEntityRepository.save(fileEntity);
|
||||
}
|
||||
@@ -196,7 +170,7 @@ public class FileEntityService {
|
||||
return totalSize != null ? totalSize : 0L;
|
||||
}
|
||||
|
||||
public void changeStatus(ProtectionStatus newStatus, String fileId) {
|
||||
public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
fileEntity.setProtectionStatus(newStatus);
|
||||
@@ -204,6 +178,14 @@ public class FileEntityService {
|
||||
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)
|
||||
@@ -218,6 +200,10 @@ public class FileEntityService {
|
||||
|
||||
Files.write(protectedFilePath, data);
|
||||
|
||||
if (imageResizeService.isImage(extension)) {
|
||||
imageResizeService.generateSizes(fileEntity, data);
|
||||
}
|
||||
|
||||
fileEntity.setProtectedFilePath(protectedFilePath.toString());
|
||||
fileEntity.setProtectedAt(LocalDateTime.now());
|
||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -308,14 +294,29 @@ public class FileEntityService {
|
||||
}
|
||||
}
|
||||
|
||||
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<FileMonitoringEntity> 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(fileEntity.getOriginalFileName())
|
||||
.originalFileName(name + "." + fileEntity.getFileExtension())
|
||||
.storedFileName(fileEntity.getStoredFileName())
|
||||
.filePath(fileEntity.getProtectedFilePath())
|
||||
.fileSize(fileEntity.getFileSize())
|
||||
@@ -331,16 +332,16 @@ public class FileEntityService {
|
||||
.existsOnDisk(existsOnDisk)
|
||||
.supportId(fileEntity.getSupportId())
|
||||
.protectStatus(fileEntity.getProtectionStatus().toString())
|
||||
.fileName(fileEntity.getOriginalFileName().replace("." +
|
||||
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
|
||||
"." + fileEntity.getFileExtension())
|
||||
.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();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import com.vrt.NoCopyFileService;
|
||||
import com.vrt.fileprotection.FileProtector;
|
||||
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 ru.soune.nocopy.util.FileUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileProcessingOrchestrator {
|
||||
|
||||
private final NoCopyFileService noCopyFileService;
|
||||
|
||||
private final FileEntityRepository fileRepository;
|
||||
|
||||
private final FileUtil fileUtil;
|
||||
|
||||
public void initializeProcessingQueue() {
|
||||
List<FileEntity> filesToProtect = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED);
|
||||
|
||||
for (FileEntity fileEntity : filesToProtect) {
|
||||
try {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.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<FileEntity> newFiles = fileRepository.findAllActiveFilesAndNotProtected();
|
||||
|
||||
for (FileEntity fileEntity : newFiles) {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.configuration.file.FileStorageConfig;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -16,11 +15,11 @@ import java.nio.file.Paths;
|
||||
@Slf4j
|
||||
public class FileStorageService {
|
||||
|
||||
@Autowired
|
||||
private FileStorageConfig storageConfig;
|
||||
@Value("${file.storage.base-path}")
|
||||
private String basePath;
|
||||
|
||||
public Resource loadFileAsResource(String filePath) throws IOException {
|
||||
Path path = Paths.get(storageConfig.getBasePath()).resolve(filePath).normalize();
|
||||
Path path = Paths.get(basePath).resolve(filePath).normalize();
|
||||
Resource resource = new UrlResource(path.toUri());
|
||||
|
||||
if (!resource.exists()) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.io.IOException;
|
||||
|
||||
public interface FileUploadService {
|
||||
FileUploadSession initUpload(Long userId, String fileName,
|
||||
String fileType, String extension, long fileSize);
|
||||
String fileType, String extension, String convertTo, long fileSize);
|
||||
|
||||
void handleExpiredSession(FileUploadSession session);
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ImageResizeService {
|
||||
|
||||
private static final int THUMBNAIL_SIZE = 150;
|
||||
private static final int MEDIUM_SIZE = 600;
|
||||
|
||||
@Value("${file.storage.base-path}")
|
||||
private String storagePath;
|
||||
|
||||
public boolean isImage(String extension) {
|
||||
return extension != null && List.of("jpg", "jpeg", "png", "webp")
|
||||
.contains(extension.toLowerCase());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void generateSizes(FileEntity file, byte[] data) throws IOException {
|
||||
if (!isImage(file.getFileExtension())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) {
|
||||
BufferedImage original = ImageIO.read(bis);
|
||||
|
||||
if (original == null) {
|
||||
log.warn("Cannot read image: {}", file.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
String baseName = file.getId().replace(".", "_");
|
||||
|
||||
if (original.getWidth() > THUMBNAIL_SIZE) {
|
||||
String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE);
|
||||
file.setThumbnailPath(thumbPath);
|
||||
}
|
||||
|
||||
if (original.getWidth() > MEDIUM_SIZE) {
|
||||
String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE);
|
||||
file.setMediumPath(mediumPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resizeAndSave(BufferedImage original, String fileName, int targetWidth) throws IOException {
|
||||
int targetHeight = (int) ((double) original.getHeight() / original.getWidth() * targetWidth);
|
||||
|
||||
BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = resized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(original, 0, 0, targetWidth, targetHeight, null);
|
||||
g.dispose();
|
||||
|
||||
Path dirPath = Paths.get(storagePath, "resized");
|
||||
Files.createDirectories(dirPath);
|
||||
|
||||
Path filePath = dirPath.resolve(fileName + ".jpg");
|
||||
ImageIO.write(resized, "jpg", filePath.toFile());
|
||||
|
||||
return filePath.toString();
|
||||
}
|
||||
|
||||
public String getPath(FileEntity file, String size) {
|
||||
|
||||
if ("thumbnail".equals(size) && file.getThumbnailPath() != null) {
|
||||
return file.getThumbnailPath();
|
||||
}
|
||||
|
||||
if ("medium".equals(size) && file.getMediumPath() != null) {
|
||||
return file.getMediumPath();
|
||||
}
|
||||
|
||||
return file.getProtectedFilePath();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,9 @@ public class AudioLocalSearchImpl implements AudioLocalSearch {
|
||||
public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) {
|
||||
FileEntity fileEntity = fileEntityService.findBySignature(signature);
|
||||
|
||||
return fileUtil.createFileInfo(fileEntity);
|
||||
if (fileEntity == null) return null;
|
||||
|
||||
return fileUtil.createFileInfo(fileEntity, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,7 +23,7 @@ public class DocumentLocalSearchImpl implements DocumentLocalSearch {
|
||||
@Override
|
||||
public @Nullable FileProtector.FileInfo findByIds(@NotNull String ownerId, @NotNull String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId));
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null);
|
||||
log.info("fileInfo: {}", fileInfo);
|
||||
|
||||
return fileInfo;
|
||||
|
||||
@@ -2,27 +2,29 @@ package ru.soune.nocopy.service.file.impl;
|
||||
|
||||
import com.vrt.NoCopyFileService;
|
||||
import com.vrt.fileprotection.FileProtector;
|
||||
import com.vrt.fileprotection.NoCopyCheckResult;
|
||||
import com.vrt.fileprotection.audio.AudioCheckResult;
|
||||
import com.vrt.fileprotection.documents.DocumentCheckResult;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import ru.soune.nocopy.dto.file.UploadProgressResponse;
|
||||
import ru.soune.nocopy.entity.file.*;
|
||||
import ru.soune.nocopy.exception.ChunkSizeExceededException;
|
||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||
import ru.soune.nocopy.exception.FileUploadException;
|
||||
import ru.soune.nocopy.exception.UploadSessionNotFoundException;
|
||||
import ru.soune.nocopy.exception.*;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||
import ru.soune.nocopy.service.FileSimilarityService;
|
||||
import ru.soune.nocopy.service.ImageHashService;
|
||||
import ru.soune.nocopy.service.cost.CostService;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
@@ -59,9 +61,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Value("${file.storage.max-retry-attempts:3}")
|
||||
private int maxRetryAttempts;
|
||||
|
||||
@Value("${file.storage.chunk-timeout-ms:300000}") // 5 минут
|
||||
private long chunkTimeoutMs;
|
||||
|
||||
@Value("${file.storage.session-expiry-hours:24}")
|
||||
private int sessionExpiryHours;
|
||||
|
||||
@@ -79,6 +78,9 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Autowired
|
||||
private FileEntityService fileEntityService;
|
||||
|
||||
private final CostService costService;
|
||||
|
||||
@Lazy
|
||||
private final NoCopyFileService noCopyFileService;
|
||||
|
||||
private final FileUtil fileUtil;
|
||||
@@ -112,7 +114,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
@Override
|
||||
@Transactional
|
||||
public FileUploadSession initUpload(Long userId, String fileName,
|
||||
String fileType, String extension, long fileSize) {
|
||||
String fileType, String extension, String convertTo, long fileSize) {
|
||||
log.info("Initializing upload for user {}: {} ({} bytes, type: {})",
|
||||
userId, fileName, fileSize, fileType);
|
||||
|
||||
@@ -143,6 +145,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
.status(UploadStatus.INITIATED)
|
||||
.expiresAt(LocalDateTime.now().plusHours(sessionExpiryHours))
|
||||
.retryCount(0)
|
||||
.convertTo(convertTo)
|
||||
.build();
|
||||
|
||||
FileUploadSession savedSession = sessionRepository.save(session);
|
||||
@@ -252,6 +255,12 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
|
||||
cleanupSessionFiles(session);
|
||||
|
||||
if (status != FileStatus.TEMP) {
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
}
|
||||
|
||||
log.info("File processing completed for session: {}", session.getUploadId());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to complete file processing for session {}: {}",
|
||||
@@ -313,21 +322,58 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
chunkNumber, session.getChunksUploaded(), session.getTotalChunks());
|
||||
|
||||
boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());
|
||||
|
||||
//TODO CHECK
|
||||
if (isLastChunk) {
|
||||
log.info("All chunks uploaded for session {}. Starting assembly...",
|
||||
session.getUploadId());
|
||||
|
||||
String finalFilePath = assembleFileSynchronously(session);
|
||||
|
||||
if (session.getFileType().startsWith("image") && findSimilar == 0) {
|
||||
checkForDuplicatesSynchronously(finalFilePath);
|
||||
if (findSimilar == 0) {
|
||||
if (session.getFileType().startsWith("image")) {
|
||||
checkForDuplicatesSynchronously(finalFilePath);
|
||||
} else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) {
|
||||
fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(),
|
||||
session.getUserId());
|
||||
|
||||
File file = new File(finalFilePath);
|
||||
|
||||
if (session.getFileType().startsWith("audio") && !isWavFile(file)) {
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError("wav file not have RIFF header");
|
||||
|
||||
sessionRepository.save(session);
|
||||
|
||||
throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " +
|
||||
"another problems with .wav file");
|
||||
}
|
||||
|
||||
FileProtector.Type type = "document".equals(session.getFileType()) ?
|
||||
FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase());
|
||||
|
||||
NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type);
|
||||
|
||||
switch (checkResult) {
|
||||
case DocumentCheckResult.Success success -> {
|
||||
FileProtector.FileInfo info = success.getInfo();
|
||||
throw new DuplicateImageException("Duplicate", info.getId(),
|
||||
Long.valueOf(info.getOwnerId()));
|
||||
}
|
||||
case AudioCheckResult.Success success -> {
|
||||
FileProtector.FileInfo info = success.getInfo();
|
||||
throw new DuplicateImageException("Duplicate", info.getId(),
|
||||
Long.valueOf(info.getOwnerId()));
|
||||
}
|
||||
case DocumentCheckResult.Failed failed ->
|
||||
log.warn("Document check failed: {}", failed.getMessage());
|
||||
case AudioCheckResult.Failed failed ->
|
||||
log.warn("Audio check failed: {}", failed.getMessage());
|
||||
default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.COMPLETED);
|
||||
session.setFilePath(finalFilePath);
|
||||
|
||||
FileStatus status = findSimilar == 0 ? FileStatus.ACTIVE: FileStatus.TEMP;
|
||||
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;
|
||||
|
||||
if (status == FileStatus.TEMP) {
|
||||
fileEntityService.clearTempFiles(session.getUserId());
|
||||
@@ -351,7 +397,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
if (status != FileStatus.TEMP) {
|
||||
String fileType = session.getFileType();
|
||||
|
||||
tariffInfoService.writeOffTokens(session.getUserId(), protectCost(fileType));
|
||||
tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType));
|
||||
}
|
||||
} else {
|
||||
sessionRepository.save(session);
|
||||
@@ -388,21 +434,87 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private int protectCost(String fileType) {
|
||||
int cost = 0;
|
||||
|
||||
switch (fileType) {
|
||||
case "video":
|
||||
return TariffConstants.VIDEO_TOKEN_VALUE;
|
||||
case "image":
|
||||
return TariffConstants.IMAGE_TOKEN_VALUE;
|
||||
case "audio":
|
||||
return TariffConstants.AUDIO_TOKEN_VALUE;
|
||||
case "document":
|
||||
return TariffConstants.PDF_TOKEN_VALUE;
|
||||
//TODO убрать когда научимся защищать другие типы
|
||||
public boolean isWavFile(File file) {
|
||||
if (file == null || !file.exists() || file.length() < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cost;
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] header = new byte[4];
|
||||
int bytesRead = fis.read(header);
|
||||
|
||||
if (bytesRead < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return header[0] == 82 && header[1] == 73 &&
|
||||
header[2] == 70 && header[3] == 70;
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isValidWav(File file) {
|
||||
if (file == null || !file.exists() || file.length() < 44) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
byte[] header = new byte[44];
|
||||
if (fis.read(header) < 44) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[0] != 'R' || header[1] != 'I' ||
|
||||
header[2] != 'F' || header[3] != 'F') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[8] != 'W' || header[9] != 'A' ||
|
||||
header[10] != 'V' || header[11] != 'E') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[12] != 'f' || header[13] != 'm' ||
|
||||
header[14] != 't' || header[15] != ' ') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header[36] != 'd' || header[37] != 'a' ||
|
||||
header[38] != 't' || header[39] != 'a') {
|
||||
return false;
|
||||
}
|
||||
|
||||
int audioFormat = (header[20] & 0xFF) | (header[21] & 0xFF) << 8;
|
||||
if (audioFormat != 1 && audioFormat != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int numChannels = (header[22] & 0xFF) | (header[23] & 0xFF) << 8;
|
||||
if (numChannels < 1 || numChannels > 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sampleRate = (header[24] & 0xFF) | (header[25] & 0xFF) << 8 |
|
||||
(header[26] & 0xFF) << 16 | (header[27] & 0xFF) << 24;
|
||||
if (sampleRate < 8000 || sampleRate > 192000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bitsPerSample = (header[34] & 0xFF) | (header[35] & 0xFF) << 8;
|
||||
if (bitsPerSample < 8 || bitsPerSample > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String assembleFileSynchronously(FileUploadSession session) throws IOException {
|
||||
@@ -510,76 +622,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private void assembleFile(FileUploadSession session) throws IOException {
|
||||
log.info("Starting file assembly for session: {} ({})",
|
||||
session.getUploadId(), session.getFileName());
|
||||
|
||||
Path finalFilePath = null;
|
||||
String checksum;
|
||||
|
||||
try {
|
||||
finalFilePath = prepareFinalFile(session);
|
||||
log.info("Final file path: {}", finalFilePath);
|
||||
|
||||
validateAllChunksExist(session);
|
||||
|
||||
mergeChunksToFile(session, finalFilePath);
|
||||
|
||||
validateFinalFile(session, finalFilePath);
|
||||
|
||||
checksum = calculateChecksum(finalFilePath);
|
||||
log.debug("File checksum calculated: {}", checksum);
|
||||
|
||||
session.setFilePath(finalFilePath.toString());
|
||||
session.setChecksum(checksum);
|
||||
session.setStatus(UploadStatus.COMPLETED);
|
||||
session.setCompletedAt(LocalDateTime.now());
|
||||
|
||||
sessionRepository.save(session);
|
||||
log.info("Upload session updated to COMPLETED: {}", session.getUploadId());
|
||||
|
||||
try {
|
||||
FileEntity fileEntity = fileEntityService.createFromUploadSession(session, checksum);
|
||||
|
||||
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
|
||||
log.info("FileEntity successfully created for session: {}",
|
||||
session.getUploadId());
|
||||
} catch (DuplicateImageException e) {
|
||||
throw new DuplicateImageException("Duplicate", e.duplicateFileId(), e.userId());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create FileEntity for session {} (file uploaded, but metadata not saved): {}",
|
||||
session.getUploadId(), e.getMessage());
|
||||
}
|
||||
|
||||
cleanupSessionFiles(session);
|
||||
|
||||
log.info("File assembly completed successfully: {} -> {} ({} bytes)",
|
||||
session.getFileName(), finalFilePath, session.getFileSize());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("File assembly failed for session {}: {}",
|
||||
session.getUploadId(), e.getMessage(), e);
|
||||
|
||||
if (finalFilePath != null) {
|
||||
try {
|
||||
Files.deleteIfExists(finalFilePath);
|
||||
log.debug("Cleaned up partial file: {}", finalFilePath);
|
||||
} catch (IOException ioException) {
|
||||
log.warn("Failed to cleanup partial file: {}", finalFilePath, ioException);
|
||||
}
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError(e.getMessage());
|
||||
sessionRepository.save(session);
|
||||
|
||||
throw new IOException("File assembly failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path prepareFinalFile(FileUploadSession session) throws IOException {
|
||||
Path userUploadsDir = storageRoot.resolve("uploads")
|
||||
.resolve(String.valueOf(session.getUserId()))
|
||||
|
||||
@@ -44,11 +44,13 @@ public class ImageLocalSearchImpl implements ImageLocalSearch {
|
||||
@Override
|
||||
public @Nullable FileProtector.FileInfo findByPHash(@NotNull PHash pHash) {
|
||||
ImageHashEntity imageHashEntity =
|
||||
imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(),
|
||||
pHash.getSecondPart());
|
||||
imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(), pHash.getSecondPart());
|
||||
|
||||
if (imageHashEntity == null) return null;
|
||||
|
||||
FileEntity file = fileEntityRepository.findByFileId(imageHashEntity.getFileId());
|
||||
|
||||
return new FileProtector.FileInfo(FileProtector.Type.IMAGE, imageHashEntity.getFileId(),
|
||||
String.valueOf(file.getUserId()));
|
||||
String.valueOf(file.getUserId()), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,21 @@ public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingLis
|
||||
|
||||
@Override
|
||||
public void onStartProcessing(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSavingFailed(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(FileProtector.FileInfo fileInfo) {
|
||||
fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
||||
fileEntityService.changeProtectionStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
|
||||
import java.io.File;
|
||||
@@ -27,6 +31,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
private final FileUploadSessionRepository fileUploadSessionRepository;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final CloudStorageService cloudStorageService;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package ru.soune.nocopy.service.file.moderation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ru.soune.nocopy.dto.file.*;
|
||||
import ru.soune.nocopy.entity.file.*;
|
||||
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
|
||||
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||
|
||||
import ru.soune.nocopy.exception.InvalidAppealException;
|
||||
import ru.soune.nocopy.repository.FileAppealRepository;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.repository.ModerationLogRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ModerationService {
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
private final FileAppealRepository fileAppealRepository;
|
||||
|
||||
private final ModerationLogRepository moderationLogRepository;
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
|
||||
FileStatus.MODERATION,
|
||||
FileStatus.BLOCKED
|
||||
);
|
||||
|
||||
/**
|
||||
* Модерация файла администратором
|
||||
*/
|
||||
@Transactional
|
||||
public void moderateFile(ModerationActionRequest request, Long moderatorId) {
|
||||
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||
|
||||
if (!MODERATION_NEEDED_STATUSES.contains(file.getStatus())) {
|
||||
throw new IllegalStateException("File is not in moderation state. Current status: " + file.getStatus());
|
||||
}
|
||||
|
||||
ModerationLog log = ModerationLog.builder()
|
||||
.fileId(file.getId())
|
||||
.moderatorId(moderatorId)
|
||||
.oldStatus(file.getStatus())
|
||||
.newStatus(request.getNewStatus())
|
||||
.reason(request.getComment())
|
||||
.comment(request.getComment())
|
||||
.build();
|
||||
|
||||
moderationLogRepository.save(log);
|
||||
|
||||
file.setStatus(request.getNewStatus());
|
||||
|
||||
fileEntityRepository.save(file);
|
||||
|
||||
if (request.getNewStatus() == FileStatus.BLOCKED ||
|
||||
request.getNewStatus() == FileStatus.VIOLATION) {
|
||||
|
||||
fileAppealRepository.findByFileIdAndStatus(file.getId(), AppealStatus.PENDING)
|
||||
.ifPresent(appeal -> {
|
||||
appeal.setStatus(AppealStatus.REJECTED);
|
||||
appeal.setAdminComment(request.getComment());
|
||||
appeal.setResolvedAt(LocalDateTime.now());
|
||||
fileAppealRepository.save(appeal);
|
||||
});
|
||||
} else if (request.getNewStatus() == FileStatus.REMOVED) {
|
||||
try {
|
||||
fileEntityService.softDeleteFileWithHash(file);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подача апелляции пользователем
|
||||
*/
|
||||
@Transactional
|
||||
public AppealResponse submitAppeal(AppealRequest request, Long userId) {
|
||||
FileEntity file = fileEntityRepository.findById(request.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
|
||||
|
||||
if (!file.getUserId().equals(userId)) {
|
||||
throw new SecurityException("User is not the owner of this file");
|
||||
}
|
||||
|
||||
if (!canAppeal(file)) {
|
||||
throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus());
|
||||
}
|
||||
|
||||
if (hasActiveAppeal(file.getId())) {
|
||||
throw new InvalidAppealException("Active appeal already exists for this file");
|
||||
}
|
||||
|
||||
FileAppeal appeal = FileAppeal.builder()
|
||||
.fileId(file.getId())
|
||||
.userId(userId)
|
||||
.appealReason(request.getAppealReason())
|
||||
.additionalInfo(request.getAdditionalInfo())
|
||||
.status(AppealStatus.PENDING)
|
||||
.moderationBeforeStatus(file.getStatus())
|
||||
.build();
|
||||
|
||||
FileAppeal saved = fileAppealRepository.save(appeal);
|
||||
|
||||
return convertToResponse(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассмотрение апелляции администратором
|
||||
*/
|
||||
@Transactional
|
||||
public void reviewAppeal(String appealId, boolean approve, String comment, Long moderatorId) {
|
||||
FileAppeal appeal = fileAppealRepository.findById(appealId)
|
||||
.orElseThrow(() -> new RuntimeException("Appeal not found: " + appealId));
|
||||
|
||||
if (appeal.getStatus() != AppealStatus.PENDING) {
|
||||
throw new IllegalStateException("Appeal already processed");
|
||||
}
|
||||
|
||||
FileEntity file = fileEntityRepository.findById(appeal.getFileId())
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(appeal.getFileId()));
|
||||
|
||||
appeal.setStatus(approve ? AppealStatus.APPROVED : AppealStatus.REJECTED);
|
||||
appeal.setAdminComment(comment);
|
||||
appeal.setModeratorId(moderatorId);
|
||||
appeal.setResolvedAt(LocalDateTime.now());
|
||||
|
||||
if (approve) {
|
||||
file.setStatus(FileStatus.ACTIVE);
|
||||
}
|
||||
|
||||
ModerationLog log = ModerationLog.builder()
|
||||
.fileId(file.getId())
|
||||
.moderatorId(moderatorId)
|
||||
.oldStatus(file.getStatus())
|
||||
.newStatus(file.getStatus())
|
||||
.comment("Appeal review: " + comment)
|
||||
.build();
|
||||
moderationLogRepository.save(log);
|
||||
|
||||
fileEntityRepository.save(file);
|
||||
|
||||
fileAppealRepository.save(appeal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, можно ли подать апелляцию на файл
|
||||
*/
|
||||
public boolean canAppeal(FileEntity file) {
|
||||
return file.getStatus() == FileStatus.BLOCKED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка наличия активной апелляции
|
||||
*/
|
||||
public boolean hasActiveAppeal(String fileId) {
|
||||
return fileAppealRepository.existsByFileIdAndStatusIn(
|
||||
fileId,
|
||||
Arrays.asList(AppealStatus.PENDING, AppealStatus.IN_REVIEW)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о файле для модерации
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public FileModerationInfo getFileModerationInfo(String fileId) {
|
||||
FileEntity file = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
|
||||
FileAppeal activeAppeal = fileAppealRepository.findByFileIdAndStatus(
|
||||
fileId, AppealStatus.PENDING).orElse(null);
|
||||
|
||||
return FileModerationInfo.builder()
|
||||
.fileId(file.getId())
|
||||
.fileName(file.getOriginalFileName())
|
||||
.currentStatus(file.getStatus())
|
||||
.uploadDate(file.getCreatedAt())
|
||||
.hasActiveAppeal(activeAppeal != null)
|
||||
.activeAppeal(activeAppeal != null ?
|
||||
AppealInfo.builder()
|
||||
.appealId(activeAppeal.getId())
|
||||
.status(activeAppeal.getStatus().name())
|
||||
.createdAt(activeAppeal.getCreatedAt())
|
||||
.build() : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка файлов на модерации
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<FileEntity> getFilesForModeration(int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
return fileEntityRepository.findByStatusIn(
|
||||
Arrays.asList(FileStatus.MODERATION, FileStatus.BLOCKED),
|
||||
pageable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение апелляций пользователя
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<AppealResponse> getUserAppeals(Long userId, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
return fileAppealRepository.findByUserId(userId, pageable)
|
||||
.map(this::convertToResponse);
|
||||
}
|
||||
|
||||
private AppealResponse convertToResponse(FileAppeal appeal) {
|
||||
return AppealResponse.builder()
|
||||
.appealId(appeal.getId())
|
||||
.fileId(appeal.getFileId())
|
||||
.appealReason(appeal.getAppealReason())
|
||||
.additionalInfo(appeal.getAdditionalInfo())
|
||||
.status(appeal.getStatus().name())
|
||||
.adminComment(appeal.getAdminComment())
|
||||
.createdAt(appeal.getCreatedAt())
|
||||
.resolvedAt(appeal.getResolvedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user