@@ -0,0 +1,44 @@
|
|||||||
|
package ru.soune.nocopy.configuration;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import ru.soune.nocopy.entity.file.FileEntity;
|
||||||
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||||
|
import ru.soune.nocopy.service.file.ImageResizeService;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class ImageMigration {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileEntityRepository fileRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ImageResizeService imageResizeService;
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void migrate() {
|
||||||
|
List<FileEntity> images = fileRepository.findByThumbnailPathIsNull();
|
||||||
|
|
||||||
|
for (FileEntity file : images) {
|
||||||
|
try {
|
||||||
|
Path path = Paths.get(file.getProtectedFilePath());
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
byte[] data = Files.readAllBytes(path);
|
||||||
|
imageResizeService.generateSizes(file, data);
|
||||||
|
fileRepository.save(file);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error migrating {}: {}", file.getId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import ru.soune.nocopy.repository.FileEntityRepository;
|
|||||||
import ru.soune.nocopy.repository.UserRepository;
|
import ru.soune.nocopy.repository.UserRepository;
|
||||||
import ru.soune.nocopy.service.file.CheckCounterService;
|
import ru.soune.nocopy.service.file.CheckCounterService;
|
||||||
import ru.soune.nocopy.service.file.FileStorageService;
|
import ru.soune.nocopy.service.file.FileStorageService;
|
||||||
|
import ru.soune.nocopy.service.file.ImageResizeService;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@@ -41,24 +42,8 @@ public class FileController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
// @GetMapping("/public/{fileId}")
|
@Autowired
|
||||||
// public ResponseEntity<Resource> getPublicFile(
|
private ImageResizeService imageResizeService;
|
||||||
// @PathVariable String fileId) throws IOException {
|
|
||||||
//
|
|
||||||
// FileEntity fileEntity = fileRepository.findById(fileId)
|
|
||||||
// .orElseThrow(() -> new RuntimeException("File not found"));
|
|
||||||
//
|
|
||||||
// Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
|
|
||||||
//
|
|
||||||
// ContentDisposition contentDisposition = ContentDisposition.inline()
|
|
||||||
// .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8)
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// return ResponseEntity.ok()
|
|
||||||
// .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
|
|
||||||
// .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
|
|
||||||
// .body(resource);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@GetMapping("/public/{fileId}")
|
@GetMapping("/public/{fileId}")
|
||||||
public ResponseEntity<Resource> getPublicFile(@PathVariable String fileId) {
|
public ResponseEntity<Resource> getPublicFile(@PathVariable String fileId) {
|
||||||
@@ -117,14 +102,16 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/protected/{fileId}")
|
@GetMapping("/protected/{fileId}/{type}")
|
||||||
public ResponseEntity<Resource> getProtectedFile(
|
public ResponseEntity<Resource> getProtectedFile(
|
||||||
@PathVariable String fileId) throws IOException {
|
@PathVariable String fileId, @PathVariable String type) throws IOException {
|
||||||
|
|
||||||
FileEntity fileEntity = fileRepository.findById(fileId)
|
FileEntity fileEntity = fileRepository.findById(fileId)
|
||||||
.orElseThrow(() -> new RuntimeException("File not found"));
|
.orElseThrow(() -> new RuntimeException("File not found"));
|
||||||
|
|
||||||
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getProtectedFilePath());
|
String path = imageResizeService.getPath(fileEntity, type);
|
||||||
|
|
||||||
|
Resource resource = fileStorageService.loadFileAsResource(path);
|
||||||
|
|
||||||
if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED ||
|
if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED ||
|
||||||
fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) {
|
fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) {
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ public class FileEntity {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String signature;
|
private String signature;
|
||||||
|
|
||||||
|
@Column(name = "thumbnail_path")
|
||||||
|
private String thumbnailPath;
|
||||||
|
|
||||||
|
@Column(name = "medium_path")
|
||||||
|
private String mediumPath;
|
||||||
|
|
||||||
@OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private ImageHashEntity imageHash;
|
private ImageHashEntity imageHash;
|
||||||
|
|||||||
@@ -61,4 +61,6 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
|
|||||||
|
|
||||||
@Query("SELECT MAX(f.supportId) FROM FileEntity f")
|
@Query("SELECT MAX(f.supportId) FROM FileEntity f")
|
||||||
Integer findMaxSupportId();
|
Integer findMaxSupportId();
|
||||||
|
|
||||||
|
List<FileEntity> findByThumbnailPathIsNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,7 +211,8 @@ public class FileSimilarityService {
|
|||||||
.supportId(similarImageProjection.getSupportId())
|
.supportId(similarImageProjection.getSupportId())
|
||||||
.uploadDate(similarImageProjection.getUploadDate())
|
.uploadDate(similarImageProjection.getUploadDate())
|
||||||
.status(similarImageProjection.getProtectionStatus())
|
.status(similarImageProjection.getProtectionStatus())
|
||||||
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
|
// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
|
||||||
|
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,13 +255,14 @@ public class FileEntityService {
|
|||||||
fileEntityRepository.save(fileEntity);
|
fileEntityRepository.save(fileEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImageResizeService imageResizeService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
|
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
|
||||||
FileEntity fileEntity = fileEntityRepository.findById(id)
|
FileEntity fileEntity = fileEntityRepository.findById(id)
|
||||||
.orElseThrow(() -> new RuntimeException("File not found: " + id));
|
.orElseThrow(() -> new RuntimeException("File not found: " + id));
|
||||||
|
|
||||||
String extension = determineFileExtension(fileExt, fileEntity);
|
String extension = determineFileExtension(fileExt, fileEntity);
|
||||||
log.info("EXTENSION: {}", extension);
|
|
||||||
Path protectedFilePath = prepareProtectedPath(fileEntity, extension);
|
Path protectedFilePath = prepareProtectedPath(fileEntity, extension);
|
||||||
|
|
||||||
if (Files.exists(protectedFilePath)) {
|
if (Files.exists(protectedFilePath)) {
|
||||||
@@ -270,6 +271,10 @@ public class FileEntityService {
|
|||||||
|
|
||||||
Files.write(protectedFilePath, data);
|
Files.write(protectedFilePath, data);
|
||||||
|
|
||||||
|
if (imageResizeService.isImage(extension)) {
|
||||||
|
imageResizeService.generateSizes(fileEntity, data);
|
||||||
|
}
|
||||||
|
|
||||||
fileEntity.setProtectedFilePath(protectedFilePath.toString());
|
fileEntity.setProtectedFilePath(protectedFilePath.toString());
|
||||||
fileEntity.setProtectedAt(LocalDateTime.now());
|
fileEntity.setProtectedAt(LocalDateTime.now());
|
||||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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.path}")
|
||||||
|
private String storagePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли файл изображением
|
||||||
|
*/
|
||||||
|
public boolean isImage(String extension) {
|
||||||
|
return extension != null && List.of("jpg", "jpeg", "png", "gif", "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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,72 +127,6 @@ public class SearchImageService {
|
|||||||
return mimeType != null && mimeType.startsWith("image");
|
return mimeType != null && mimeType.startsWith("image");
|
||||||
}
|
}
|
||||||
|
|
||||||
// private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
|
|
||||||
// if (searchApiKey == null || searchApiKey.isBlank()) {
|
|
||||||
// throw new IllegalStateException("SearchAPI key not configured");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// OkHttpClient client = engine.contains("yandex_reverse_image") ? yandexHttpClient : googleHttpClient;
|
|
||||||
//
|
|
||||||
// HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search")
|
|
||||||
// .newBuilder()
|
|
||||||
// .addQueryParameter("engine", engine)
|
|
||||||
// .addQueryParameter("api_key", searchApiKey)
|
|
||||||
// .addQueryParameter("url", imageUrl)
|
|
||||||
// .addQueryParameter("search_type", searchType)
|
|
||||||
// .addQueryParameter("t_", String.valueOf(System.currentTimeMillis()))
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// Request request = new Request.Builder()
|
|
||||||
// .url(url)
|
|
||||||
// .header("Accept", "application/json")
|
|
||||||
// .header("User-Agent", "Mozilla/5.0")
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// int maxAttempts = 2;
|
|
||||||
// IOException lastException = null;
|
|
||||||
//
|
|
||||||
// for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
// long start = System.currentTimeMillis();
|
|
||||||
//
|
|
||||||
// try (Response response = client.newCall(request).execute()) {
|
|
||||||
// long duration = System.currentTimeMillis() - start;
|
|
||||||
// log.info("Yandex response code={}, duration={}ms, attempt={}",
|
|
||||||
// response.code(), duration, attempt);
|
|
||||||
//
|
|
||||||
// if (!response.isSuccessful()) {
|
|
||||||
// throw new IOException("API error: " + response.code());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// ResponseBody body = response.body();
|
|
||||||
// if (body == null) {
|
|
||||||
// throw new IOException("Empty response body");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return body.string();
|
|
||||||
//
|
|
||||||
// } catch (IOException e) {
|
|
||||||
// lastException = e;
|
|
||||||
// log.warn("Attempt {} failed for image: {}, error: {}",
|
|
||||||
// attempt, imageUrl, e.getMessage());
|
|
||||||
//
|
|
||||||
// if (attempt == maxAttempts) {
|
|
||||||
// log.error("All {} attempts failed for image: {}", maxAttempts, imageUrl);
|
|
||||||
// throw lastException;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// Thread.sleep(500L * attempt);
|
|
||||||
// } catch (InterruptedException ie) {
|
|
||||||
// Thread.currentThread().interrupt();
|
|
||||||
// throw new IOException("Retry interrupted", ie);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// throw lastException;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
|
private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException {
|
||||||
if (searchApiKey == null || searchApiKey.isBlank()) {
|
if (searchApiKey == null || searchApiKey.isBlank()) {
|
||||||
throw new IllegalStateException("SearchAPI key not configured");
|
throw new IllegalStateException("SearchAPI key not configured");
|
||||||
|
|||||||
Reference in New Issue
Block a user