This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package ru.soune.nocopy.controller.demo;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import ru.soune.nocopy.dto.demo.*;
|
||||
import ru.soune.nocopy.service.demo.DemoSessionService;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/demo")
|
||||
@RequiredArgsConstructor
|
||||
public class DemoController {
|
||||
|
||||
private final DemoSessionService demoSessionService;
|
||||
private final HttpServletRequest httpServletRequest;
|
||||
|
||||
/**
|
||||
* Загрузка файла для демонстрации
|
||||
* Требуется файл в формате multipart/form-data
|
||||
*
|
||||
* @param file файл для загрузки
|
||||
* @return DemoUploadResponse с информацией о сессии
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new DemoUploadResponse(
|
||||
null, null, null, 0L,
|
||||
"FAILED", "File is empty", 0L, 0
|
||||
));
|
||||
}
|
||||
|
||||
String clientIp = getClientIp();
|
||||
log.info("Demo upload request from IP: {} for file: {}", clientIp, file.getOriginalFilename());
|
||||
|
||||
DemoUploadResponse response = demoSessionService.uploadFile(file, clientIp);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Demo upload validation failed: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(new DemoUploadResponse(
|
||||
null, null, null, 0L,
|
||||
"FAILED", e.getMessage(), 0L, 0
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("Error during demo upload", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new DemoUploadResponse(
|
||||
null, null, null, 0L,
|
||||
"FAILED", "Upload failed: " + e.getMessage(), 0L, 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о загруженном файле в демо-сессии
|
||||
*
|
||||
* @param sessionId ID демо-сессии
|
||||
* @return DemoSessionInfo с деталями файла
|
||||
*/
|
||||
@GetMapping("/file/{sessionId}")
|
||||
public ResponseEntity<?> getFileInfo(@PathVariable String sessionId) {
|
||||
try {
|
||||
String clientIp = getClientIp();
|
||||
log.info("Getting file info for session: {} from IP: {}", sessionId, clientIp);
|
||||
|
||||
DemoSessionInfo info = demoSessionService.getSessionInfo(sessionId, clientIp);
|
||||
|
||||
return ResponseEntity.ok(info);
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Error getting session info: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(new DemoSessionInfo(
|
||||
null, null, null, 0L,
|
||||
"ERROR", null, null, 0, null
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск поиска похожих файлов для демо
|
||||
* Асинхронная операция - результаты могут загружаться в реальном времени
|
||||
*
|
||||
* @param sessionId ID демо-сессии
|
||||
* @return DemoSearchResponse с результатами поиска
|
||||
*/
|
||||
@PostMapping("/search/{sessionId}")
|
||||
public ResponseEntity<?> startSearch(@PathVariable String sessionId) {
|
||||
try {
|
||||
String clientIp = getClientIp();
|
||||
log.info("Starting demo search for session: {} from IP: {}", sessionId, clientIp);
|
||||
|
||||
DemoSearchResponse response = demoSessionService.startSearch(sessionId, clientIp);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Error starting demo search: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(new DemoSearchResponse(
|
||||
sessionId, "FAILED", 0, 0.0,
|
||||
null, null, 0L, null, e.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка демо-сессии и удаление файлов
|
||||
* Должна быть вызвана пользователем или автоматически при закрытии демо
|
||||
*
|
||||
* @param sessionId ID демо-сессии
|
||||
* @return успешное подтверждение удаления
|
||||
*/
|
||||
@DeleteMapping("/cleanup/{sessionId}")
|
||||
public ResponseEntity<?> cleanup(@PathVariable String sessionId) {
|
||||
try {
|
||||
String clientIp = getClientIp();
|
||||
log.info("Demo cleanup request for session: {} from IP: {}", sessionId, clientIp);
|
||||
|
||||
demoSessionService.deleteSession(sessionId, clientIp);
|
||||
|
||||
return ResponseEntity.ok(new DemoUploadResponse(
|
||||
sessionId, null, null, 0L,
|
||||
"CLEANED", "Session cleaned successfully", 0L, 100
|
||||
));
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Error cleaning up session: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(new DemoUploadResponse(
|
||||
sessionId, null, null, 0L,
|
||||
"ERROR", e.getMessage(), 0L, 0
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check эндпоинт для демо API
|
||||
* Проверяет доступность и готовность к приёму файлов
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<?> health() {
|
||||
return ResponseEntity.ok(new DemoUploadResponse(
|
||||
null, null, null, 0L,
|
||||
"HEALTHY", "Demo service is ready", 0L, 100
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлечение IP адреса клиента из запроса
|
||||
* Учитывает различные прокси и заголовки
|
||||
*/
|
||||
private String getClientIp() {
|
||||
String xForwardedFor = httpServletRequest.getHeader("X-Forwarded-For");
|
||||
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
|
||||
return xForwardedFor.split(",")[0].trim();
|
||||
}
|
||||
|
||||
String xRealIp = httpServletRequest.getHeader("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isEmpty()) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
return httpServletRequest.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ru.soune.nocopy.dto.demo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DemoSearchResponse {
|
||||
private String sessionId;
|
||||
private String status;
|
||||
private Integer resultsCount;
|
||||
private Double matchPercentage;
|
||||
private LocalDateTime startedAt;
|
||||
private LocalDateTime completedAt;
|
||||
private Long processingTimeMs;
|
||||
private List<DemoSimilarFile> similarFiles;
|
||||
private String message;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class DemoSimilarFile {
|
||||
private String fileId;
|
||||
private String fileName;
|
||||
private Double similarityScore;
|
||||
private String source; // "web" или "database"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.soune.nocopy.dto.demo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DemoSessionInfo {
|
||||
private String sessionId;
|
||||
private String fileName;
|
||||
private String fileType;
|
||||
private Long fileSize;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime expiresAt;
|
||||
private Integer searchResultsCount;
|
||||
private String mimeType;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.soune.nocopy.dto.demo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DemoUploadRequest {
|
||||
private String fileName;
|
||||
private String fileType; // "image", "document", "audio"
|
||||
private Long fileSize;
|
||||
private String mimeType;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ru.soune.nocopy.dto.demo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DemoUploadResponse {
|
||||
private String sessionId;
|
||||
private String fileName;
|
||||
private String fileType;
|
||||
private Long fileSize;
|
||||
private String status;
|
||||
private String message;
|
||||
private Long uploadedBytes;
|
||||
private Integer progressPercentage;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package ru.soune.nocopy.entity.demo;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "demo_sessions")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DemoSession {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private String sessionId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String fileName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String fileId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String s3Path;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long fileSize;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String fileType; // "image", "document", "audio"
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private DemoSessionStatus status; // CREATED, PROCESSING, COMPLETED, FAILED
|
||||
|
||||
@Column
|
||||
private String mimeType;
|
||||
|
||||
@Column
|
||||
private Integer searchResultsCount;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime expiresAt; // 24 часа с момента создания
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
expiresAt = LocalDateTime.now().plusHours(24);
|
||||
status = DemoSessionStatus.CREATED;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.soune.nocopy.entity.demo;
|
||||
|
||||
public enum DemoSessionStatus {
|
||||
CREATED,
|
||||
PROCESSING,
|
||||
COMPLETED,
|
||||
FAILED,
|
||||
EXPIRED,
|
||||
CLEANED
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ru.soune.nocopy.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.soune.nocopy.entity.demo.DemoSession;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface DemoSessionRepository extends JpaRepository<DemoSession, String> {
|
||||
|
||||
Optional<DemoSession> findBySessionId(String sessionId);
|
||||
|
||||
List<DemoSession> findByIpAddress(String ipAddress);
|
||||
|
||||
@Query("SELECT d FROM DemoSession d WHERE d.expiresAt <= :now AND d.status != 'CLEANED'")
|
||||
List<DemoSession> findExpiredSessions(LocalDateTime now);
|
||||
|
||||
@Query("SELECT COUNT(d) FROM DemoSession d WHERE d.ipAddress = :ipAddress AND d.createdAt >= :since")
|
||||
Integer countSessionsByIpAndTime(String ipAddress, LocalDateTime since);
|
||||
|
||||
@Query("SELECT SUM(d.fileSize) FROM DemoSession d WHERE d.ipAddress = :ipAddress AND d.createdAt >= :since")
|
||||
Long getTotalFileSizeByIpAndTime(String ipAddress, LocalDateTime since);
|
||||
|
||||
List<DemoSession> findByStatus(String status);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package ru.soune.nocopy.service.demo;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.demo.DemoSession;
|
||||
import ru.soune.nocopy.entity.demo.DemoSessionStatus;
|
||||
import ru.soune.nocopy.repository.DemoSessionRepository;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DemoCleanupService {
|
||||
|
||||
private final DemoSessionRepository demoSessionRepository;
|
||||
private final CloudStorageService cloudStorageService;
|
||||
|
||||
/**
|
||||
* Запускается каждые 30 минут для удаления истекших демо-сессий
|
||||
* Это важный сервис для предотвращения засорения S3 и БД демо-данными
|
||||
*/
|
||||
@Scheduled(fixedDelay = 1800000)
|
||||
@Transactional
|
||||
public void cleanupExpiredSessions() {
|
||||
log.info("Starting cleanup of expired demo sessions...");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<DemoSession> expiredSessions = demoSessionRepository.findExpiredSessions(now);
|
||||
|
||||
if (expiredSessions.isEmpty()) {
|
||||
log.info("No expired sessions to clean");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Found {} expired sessions to clean", expiredSessions.size());
|
||||
|
||||
for (DemoSession session : expiredSessions) {
|
||||
try {
|
||||
cleanupSession(session);
|
||||
} catch (Exception e) {
|
||||
log.error("Error cleaning up session: {}", session.getSessionId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Cleanup completed. Cleaned {} sessions", expiredSessions.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Дополнительная задача: очистка FAILED сессий (старше 1 часа)
|
||||
* Запускается каждый час
|
||||
*/
|
||||
@Scheduled(fixedDelay = 3600000)
|
||||
@Transactional
|
||||
public void cleanupFailedSessions() {
|
||||
log.info("Starting cleanup of failed demo sessions...");
|
||||
|
||||
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
|
||||
List<DemoSession> failedSessions = demoSessionRepository.findByStatus(DemoSessionStatus.FAILED.name());
|
||||
|
||||
int cleanedCount = 0;
|
||||
for (DemoSession session : failedSessions) {
|
||||
if (session.getCreatedAt().isBefore(oneHourAgo)) {
|
||||
try {
|
||||
cleanupSession(session);
|
||||
cleanedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("Error cleaning failed session: {}", session.getSessionId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Failed sessions cleanup completed. Cleaned {} sessions", cleanedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ручная очистка конкретной сессии
|
||||
* Вызывается, когда пользователь явно просит удалить результаты
|
||||
*/
|
||||
@Transactional
|
||||
public void cleanupSessionManually(String sessionId) {
|
||||
log.info("Manual cleanup requested for session: {}", sessionId);
|
||||
|
||||
DemoSession session = demoSessionRepository.findBySessionId(sessionId)
|
||||
.orElseThrow(() -> new RuntimeException("Session not found: " + sessionId));
|
||||
|
||||
cleanupSession(session);
|
||||
log.info("Manual cleanup completed for session: {}", sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Внутренняя помощь функция для очистки конкретной сессии
|
||||
*/
|
||||
private void cleanupSession(DemoSession session) {
|
||||
log.debug("Cleaning up session: {}", session.getSessionId());
|
||||
|
||||
deleteFromS3(session.getS3Path());
|
||||
|
||||
session.setStatus(DemoSessionStatus.CLEANED);
|
||||
demoSessionRepository.save(session);
|
||||
|
||||
log.info("Session cleaned successfully: {}", session.getSessionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Безопасное удаление из S3 с обработкой ошибок
|
||||
*/
|
||||
private void deleteFromS3(String s3Path) {
|
||||
try {
|
||||
cloudStorageService.deleteFileFromStorageByFullPath(s3Path);
|
||||
log.debug("Deleted from S3: {}", s3Path);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to delete from S3 (will retry later): {}", s3Path, e);
|
||||
// Не бросаем исключение - сессия остаётся помечена для повторной попытки
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Статистика для мониторинга
|
||||
* Возвращает количество активных демо-сессий
|
||||
*/
|
||||
public long getActiveDemoSessionsCount() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return demoSessionRepository.findExpiredSessions(now)
|
||||
.stream()
|
||||
.filter(s -> !s.getStatus().equals(DemoSessionStatus.CLEANED))
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить размер всех демо-данных в S3
|
||||
* Полезно для мониторинга использования дискового пространства
|
||||
*/
|
||||
public long getActiveDemoDataSize() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return demoSessionRepository.findExpiredSessions(now)
|
||||
.stream()
|
||||
.filter(s -> !s.getStatus().equals(DemoSessionStatus.CLEANED))
|
||||
.mapToLong(DemoSession::getFileSize)
|
||||
.sum();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package ru.soune.nocopy.service.demo;
|
||||
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import ru.soune.nocopy.dto.demo.*;
|
||||
import ru.soune.nocopy.entity.demo.DemoSession;
|
||||
import ru.soune.nocopy.entity.demo.DemoSessionStatus;
|
||||
import ru.soune.nocopy.exception.DuplicateImageException;
|
||||
import ru.soune.nocopy.repository.DemoSessionRepository;
|
||||
import ru.soune.nocopy.repository.SimilarImageProjection;
|
||||
import ru.soune.nocopy.service.FileSimilarityService;
|
||||
import ru.soune.nocopy.service.ImageHashService;
|
||||
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
||||
import ru.soune.nocopy.util.FileUtil;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DemoSessionService {
|
||||
|
||||
private final DemoSessionRepository demoSessionRepository;
|
||||
private final CloudStorageService cloudStorageService;
|
||||
private final ImageHashService imageHashService;
|
||||
private final FileSimilarityService fileSimilarityService;
|
||||
|
||||
@Lazy
|
||||
private final NoCopyFileService noCopyFileService;
|
||||
|
||||
private final FileUtil fileUtil;
|
||||
|
||||
@Value("${demo.max-file-size:52428800}") // 50 MB default
|
||||
private Long maxFileSize;
|
||||
|
||||
@Value("${demo.max-files-per-day:10}")
|
||||
private Integer maxFilesPerDay;
|
||||
|
||||
@Value("${demo.s3-path:demo/}")
|
||||
private String demoS3Path;
|
||||
|
||||
@Value("${demo.session-ttl-hours:24}")
|
||||
private Integer sessionTtlHours;
|
||||
|
||||
@Value("${file.storage.base-path}")
|
||||
private String basePath;
|
||||
|
||||
@Transactional
|
||||
public DemoUploadResponse uploadFile(MultipartFile file, String ipAddress) throws IOException, DuplicateImageException {
|
||||
log.info("Demo upload started for IP: {}", ipAddress);
|
||||
|
||||
validateDemoUpload(file, ipAddress);
|
||||
|
||||
DemoSession session = DemoSession.builder()
|
||||
.sessionId(UUID.randomUUID().toString())
|
||||
.ipAddress(ipAddress)
|
||||
.fileName(file.getOriginalFilename())
|
||||
.fileSize(file.getSize())
|
||||
.fileType(detectFileType(file.getContentType()))
|
||||
.mimeType(file.getContentType())
|
||||
.status(DemoSessionStatus.CREATED)
|
||||
.s3Path(demoS3Path + UUID.randomUUID() + "/" + file.getOriginalFilename())
|
||||
.build();
|
||||
|
||||
String tempFilePath = null;
|
||||
try {
|
||||
tempFilePath = saveTempFile(file, session.getSessionId());
|
||||
|
||||
performProtectionCheck(tempFilePath, session.getFileType());
|
||||
|
||||
try (InputStream fileInputStream = Files.newInputStream(Paths.get(tempFilePath))) {
|
||||
cloudStorageService.saveFilesToStorage(fileInputStream, session.getS3Path());
|
||||
}
|
||||
|
||||
session.setStatus(DemoSessionStatus.COMPLETED);
|
||||
session.setFileId(generateFileId());
|
||||
log.info("File uploaded to S3 at: {}", session.getS3Path());
|
||||
|
||||
addFileToProtection(tempFilePath, session);
|
||||
|
||||
} catch (DuplicateImageException e) {
|
||||
session.setStatus(DemoSessionStatus.FAILED);
|
||||
log.warn("Duplicate detected in demo upload: {}", e.getMessage());
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
session.setStatus(DemoSessionStatus.FAILED);
|
||||
log.error("Failed to upload file to S3", e);
|
||||
throw new RuntimeException("Upload failed: " + e.getMessage());
|
||||
} finally {
|
||||
if (tempFilePath != null) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(tempFilePath));
|
||||
log.debug("Deleted temp file: {}", tempFilePath);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp file: {}", tempFilePath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
demoSessionRepository.save(session);
|
||||
|
||||
return DemoUploadResponse.builder()
|
||||
.sessionId(session.getSessionId())
|
||||
.fileName(session.getFileName())
|
||||
.fileType(session.getFileType())
|
||||
.fileSize(session.getFileSize())
|
||||
.status(session.getStatus().name())
|
||||
.message("File uploaded successfully")
|
||||
.uploadedBytes(session.getFileSize())
|
||||
.progressPercentage(100)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DemoSessionInfo getSessionInfo(String sessionId, String ipAddress) {
|
||||
log.info("Getting session info for: {}", sessionId);
|
||||
|
||||
DemoSession session = demoSessionRepository.findBySessionId(sessionId)
|
||||
.orElseThrow(() -> new RuntimeException("Demo session not found: " + sessionId));
|
||||
|
||||
// Проверка принадлежности сессии IP
|
||||
if (!session.getIpAddress().equals(ipAddress)) {
|
||||
throw new RuntimeException("Unauthorized access to session");
|
||||
}
|
||||
|
||||
return DemoSessionInfo.builder()
|
||||
.sessionId(session.getSessionId())
|
||||
.fileName(session.getFileName())
|
||||
.fileType(session.getFileType())
|
||||
.fileSize(session.getFileSize())
|
||||
.status(session.getStatus().name())
|
||||
.createdAt(session.getCreatedAt())
|
||||
.expiresAt(session.getExpiresAt())
|
||||
.searchResultsCount(session.getSearchResultsCount())
|
||||
.mimeType(session.getMimeType())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DemoSearchResponse startSearch(String sessionId, String ipAddress) {
|
||||
log.info("Starting search for session: {}", sessionId);
|
||||
|
||||
DemoSession session = demoSessionRepository.findBySessionId(sessionId)
|
||||
.orElseThrow(() -> new RuntimeException("Demo session not found: " + sessionId));
|
||||
|
||||
if (!session.getIpAddress().equals(ipAddress)) {
|
||||
throw new RuntimeException("Unauthorized access to session");
|
||||
}
|
||||
|
||||
if (!session.getStatus().equals(DemoSessionStatus.COMPLETED)) {
|
||||
throw new RuntimeException("File not ready for search");
|
||||
}
|
||||
|
||||
session.setStatus(DemoSessionStatus.PROCESSING);
|
||||
demoSessionRepository.save(session);
|
||||
|
||||
// Симулируем поиск (в реальности здесь бы был вызов поисковых систем)
|
||||
List<DemoSearchResponse.DemoSimilarFile> similarFiles = performSearch(session);
|
||||
|
||||
session.setStatus(DemoSessionStatus.COMPLETED);
|
||||
session.setSearchResultsCount(similarFiles.size());
|
||||
demoSessionRepository.save(session);
|
||||
|
||||
long startTime = session.getUpdatedAt().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
long endTime = System.currentTimeMillis();
|
||||
|
||||
return DemoSearchResponse.builder()
|
||||
.sessionId(sessionId)
|
||||
.status("COMPLETED")
|
||||
.resultsCount(similarFiles.size())
|
||||
.matchPercentage(calculateMatchPercentage(similarFiles))
|
||||
.processingTimeMs(endTime - startTime)
|
||||
.similarFiles(similarFiles)
|
||||
.message("Search completed successfully")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteSession(String sessionId, String ipAddress) {
|
||||
log.info("Deleting demo session: {}", sessionId);
|
||||
|
||||
DemoSession session = demoSessionRepository.findBySessionId(sessionId)
|
||||
.orElseThrow(() -> new RuntimeException("Demo session not found: " + sessionId));
|
||||
|
||||
if (!session.getIpAddress().equals(ipAddress)) {
|
||||
throw new RuntimeException("Unauthorized deletion of session");
|
||||
}
|
||||
|
||||
// Удаляем из S3
|
||||
try {
|
||||
cloudStorageService.deleteFileFromStorage(session.getS3Path());
|
||||
log.info("File deleted from S3: {}", session.getS3Path());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to delete file from S3", e);
|
||||
}
|
||||
|
||||
// Удаляем из БД
|
||||
session.setStatus(DemoSessionStatus.CLEANED);
|
||||
demoSessionRepository.save(session);
|
||||
|
||||
log.info("Demo session cleaned: {}", sessionId);
|
||||
}
|
||||
|
||||
private void validateDemoUpload(MultipartFile file, String ipAddress) {
|
||||
// Проверка размера файла
|
||||
if (file.getSize() > maxFileSize) {
|
||||
throw new RuntimeException(
|
||||
String.format("File too large. Max size: %d MB", maxFileSize / 1024 / 1024)
|
||||
);
|
||||
}
|
||||
|
||||
// Проверка лимита по количеству
|
||||
LocalDateTime oneDayAgo = LocalDateTime.now().minusHours(24);
|
||||
Integer fileCount = demoSessionRepository.countSessionsByIpAndTime(ipAddress, oneDayAgo);
|
||||
|
||||
if (fileCount >= maxFilesPerDay) {
|
||||
throw new RuntimeException(
|
||||
String.format("Daily limit reached. Max %d files per day", maxFilesPerDay)
|
||||
);
|
||||
}
|
||||
|
||||
// Проверка типа файла
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null ||
|
||||
(!contentType.contains("image") &&
|
||||
!contentType.contains("document") &&
|
||||
!contentType.contains("audio") &&
|
||||
!contentType.contains("pdf") &&
|
||||
!contentType.contains("msword") &&
|
||||
!contentType.contains("wordprocessingml"))) {
|
||||
throw new RuntimeException("File type not supported for demo");
|
||||
}
|
||||
|
||||
log.info("Validation passed for IP: {}", ipAddress);
|
||||
}
|
||||
|
||||
private String detectFileType(String mimeType) {
|
||||
if (mimeType == null) return "unknown";
|
||||
if (mimeType.contains("image")) return "image";
|
||||
if (mimeType.contains("audio")) return "audio";
|
||||
if (mimeType.contains("document") || mimeType.contains("pdf") || mimeType.contains("word")) {
|
||||
return "document";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private List<DemoSearchResponse.DemoSimilarFile> performSearch(DemoSession session) {
|
||||
List<DemoSearchResponse.DemoSimilarFile> results = new ArrayList<>();
|
||||
|
||||
// Для демо: генерируем несколько фейковых результатов
|
||||
String[] sampleNames = {
|
||||
"similar_image_1.jpg",
|
||||
"similar_image_2.png",
|
||||
"similar_image_3.jpg",
|
||||
"duplicate_document.pdf",
|
||||
"related_file.docx"
|
||||
};
|
||||
|
||||
double baseScore = 0.95;
|
||||
for (int i = 0; i < Math.min(3, sampleNames.length); i++) {
|
||||
results.add(DemoSearchResponse.DemoSimilarFile.builder()
|
||||
.fileId("demo_" + UUID.randomUUID())
|
||||
.fileName(sampleNames[i])
|
||||
.similarityScore(baseScore - (i * 0.05))
|
||||
.source(i % 2 == 0 ? "web" : "database")
|
||||
.build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private Double calculateMatchPercentage(List<DemoSearchResponse.DemoSimilarFile> files) {
|
||||
if (files.isEmpty()) return 0.0;
|
||||
return files.stream()
|
||||
.mapToDouble(DemoSearchResponse.DemoSimilarFile::getSimilarityScore)
|
||||
.average()
|
||||
.orElse(0.0) * 100;
|
||||
}
|
||||
|
||||
private String generateFileId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private String saveTempFile(MultipartFile file, String sessionId) throws IOException {
|
||||
Path tempDir = Paths.get(basePath).resolve("temp").resolve("demo").resolve(sessionId);
|
||||
Files.createDirectories(tempDir);
|
||||
|
||||
String fileName = file.getOriginalFilename() != null ? file.getOriginalFilename() : "file";
|
||||
Path tempFilePath = tempDir.resolve(fileName);
|
||||
|
||||
file.transferTo(tempFilePath.toFile());
|
||||
log.info("Temp file saved for demo session: {}", tempFilePath);
|
||||
|
||||
return tempFilePath.toString();
|
||||
}
|
||||
|
||||
private void performProtectionCheck(String tempFilePath, String fileType) throws IOException, DuplicateImageException {
|
||||
File file = new File(tempFilePath);
|
||||
|
||||
if ("image".equals(fileType)) {
|
||||
checkForDuplicatesSynchronously(tempFilePath);
|
||||
} else if ("audio".equals(fileType) || "document".equals(fileType)) {
|
||||
if ("audio".equals(fileType) && !isWavFile(file)) {
|
||||
throw new IOException("WAV file validation failed");
|
||||
}
|
||||
|
||||
FileProtector.Type type = "document".equals(fileType) ?
|
||||
FileProtector.Type.DOC : FileProtector.Type.valueOf(fileType.toUpperCase());
|
||||
|
||||
NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type);
|
||||
|
||||
switch (checkResult) {
|
||||
case DocumentCheckResult.Success success ->
|
||||
log.info("Document protected: {}", success.getInfo().getId());
|
||||
case AudioCheckResult.Success success ->
|
||||
log.info("Audio protected: {}", success.getInfo().getId());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForDuplicatesSynchronously(String filePath) throws IOException, DuplicateImageException {
|
||||
Path path = Paths.get(filePath);
|
||||
Map<String, Long> hash = imageHashService.calculateHash(path);
|
||||
|
||||
List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash(
|
||||
hash.get("hi"), hash.get("low"));
|
||||
|
||||
if (!duplicates.isEmpty()) {
|
||||
log.warn("Duplicate image found for demo upload, original owner: {}", duplicates.getFirst().getUserId());
|
||||
throw new DuplicateImageException("Duplicate", duplicates.getFirst().getId(),
|
||||
duplicates.getFirst().getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
private void addFileToProtection(String tempFilePath, DemoSession session) {
|
||||
try {
|
||||
FileProtector.Type type = "audio".equals(session.getFileType()) ?
|
||||
FileProtector.Type.AUDIO : "document".equals(session.getFileType()) ?
|
||||
FileProtector.Type.DOC : FileProtector.Type.IMAGE;
|
||||
|
||||
FileProtector.FileInfo fileInfo = new FileProtector.FileInfo(
|
||||
type,
|
||||
session.getSessionId(),
|
||||
"demo",
|
||||
session.getFileType()
|
||||
);
|
||||
|
||||
noCopyFileService.addFile(fileInfo);
|
||||
log.info("File added to protection: {}", session.getSessionId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to add file to protection: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWavFile(File file) {
|
||||
if (file == null || !file.exists() || file.length() < 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
log.error("Error checking WAV file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,33 @@ public class CloudStorageService {
|
||||
@Value("${yandex.cloud.bucket-cold}")
|
||||
private String bucketCold;
|
||||
|
||||
public void saveFilesToStorage(InputStream inputStream, String s3Path) throws IOException {
|
||||
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
|
||||
S3Client s3Client = S3Client.builder()
|
||||
.httpClient(ApacheHttpClient.create())
|
||||
.region(Region.of(region))
|
||||
.endpointOverride(URI.create(s3endpoint))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(credentials))
|
||||
.build();
|
||||
|
||||
try {
|
||||
byte[] bytes = inputStream.readAllBytes();
|
||||
|
||||
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(s3Path)
|
||||
.build();
|
||||
|
||||
s3Client.putObject(putObjectRequest, RequestBody.fromBytes(bytes));
|
||||
log.info("File successfully uploaded to S3: {}", s3Path);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to upload file to S3: {}", s3Path, e);
|
||||
throw e;
|
||||
} finally {
|
||||
s3Client.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void saveFilesToStorage(String mimeType, String extension, String sendToCloudFilePath) {
|
||||
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
|
||||
S3Client s3Client = S3Client.builder()
|
||||
@@ -267,4 +294,37 @@ public class CloudStorageService {
|
||||
|
||||
fileEntityRepository.delete(fileEntity);
|
||||
}
|
||||
|
||||
public void deleteFileFromStorageByFullPath(String fullS3Path) {
|
||||
if (fullS3Path == null || fullS3Path.isEmpty()) {
|
||||
log.warn("Attempted to delete file with empty path");
|
||||
return;
|
||||
}
|
||||
|
||||
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
|
||||
|
||||
S3Client s3Client = S3Client.builder()
|
||||
.httpClient(ApacheHttpClient.create())
|
||||
.region(Region.of(region))
|
||||
.endpointOverride(URI.create(s3endpoint))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(credentials))
|
||||
.build();
|
||||
|
||||
try {
|
||||
try {
|
||||
s3Client.headObject(builder -> builder.bucket(bucket).key(fullS3Path).build());
|
||||
} catch (Exception e) {
|
||||
log.warn("File does not exist in cloud storage: {}", fullS3Path);
|
||||
return;
|
||||
}
|
||||
|
||||
s3Client.deleteObject(builder -> builder.bucket(bucket).key(fullS3Path).build());
|
||||
|
||||
log.info("File successfully deleted from cloud storage: {}", fullS3Path);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to delete file from cloud storage: {}", fullS3Path, e);
|
||||
throw new RuntimeException("Failed to delete file from cloud storage: " + fullS3Path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user