2026-01-27 20:27:39 +07:00
|
|
|
package ru.soune.nocopy.controller;
|
|
|
|
|
|
2026-04-21 11:50:25 +07:00
|
|
|
import com.google.common.io.ByteStreams;
|
2026-03-24 16:16:16 +07:00
|
|
|
import com.vrt.NoCopyFileService;
|
|
|
|
|
import com.vrt.fileprotection.FileProtector;
|
2026-04-04 15:31:51 +07:00
|
|
|
import lombok.AllArgsConstructor;
|
2026-01-27 20:27:39 +07:00
|
|
|
import lombok.extern.slf4j.Slf4j;
|
2026-04-20 18:16:46 +07:00
|
|
|
import org.springframework.core.io.InputStreamResource;
|
2026-01-27 20:27:39 +07:00
|
|
|
import org.springframework.core.io.Resource;
|
2026-02-21 23:21:44 +07:00
|
|
|
import org.springframework.http.*;
|
2026-01-28 23:25:16 +07:00
|
|
|
import org.springframework.web.bind.annotation.*;
|
2026-04-21 11:50:25 +07:00
|
|
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
2026-01-28 23:25:16 +07:00
|
|
|
import ru.soune.nocopy.dto.file.CheckStatus;
|
2026-01-27 20:27:39 +07:00
|
|
|
import ru.soune.nocopy.entity.file.FileEntity;
|
2026-01-28 18:38:43 +07:00
|
|
|
import ru.soune.nocopy.entity.file.ProtectionStatus;
|
2026-01-28 23:25:16 +07:00
|
|
|
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
|
|
|
|
|
import ru.soune.nocopy.entity.user.User;
|
2026-01-27 20:27:39 +07:00
|
|
|
import ru.soune.nocopy.repository.FileEntityRepository;
|
2026-01-28 23:25:16 +07:00
|
|
|
import ru.soune.nocopy.repository.UserRepository;
|
|
|
|
|
import ru.soune.nocopy.service.file.CheckCounterService;
|
2026-04-04 15:31:51 +07:00
|
|
|
import ru.soune.nocopy.service.file.ZipService;
|
2026-04-20 18:16:46 +07:00
|
|
|
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
|
2026-03-24 16:16:16 +07:00
|
|
|
import ru.soune.nocopy.util.FileUtil;
|
2026-04-21 11:50:25 +07:00
|
|
|
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
2026-02-16 18:57:45 +07:00
|
|
|
|
2026-04-21 02:21:33 +07:00
|
|
|
import java.io.*;
|
2026-02-16 18:57:45 +07:00
|
|
|
import java.net.URLEncoder;
|
2026-02-10 12:46:45 +07:00
|
|
|
import java.nio.charset.StandardCharsets;
|
2026-01-27 20:27:39 +07:00
|
|
|
|
|
|
|
|
@RestController
|
|
|
|
|
@RequestMapping("/api/files")
|
|
|
|
|
@Slf4j
|
2026-04-04 15:31:51 +07:00
|
|
|
@AllArgsConstructor
|
2026-01-27 20:27:39 +07:00
|
|
|
public class FileController {
|
|
|
|
|
|
|
|
|
|
private FileEntityRepository fileRepository;
|
|
|
|
|
|
2026-01-28 23:25:16 +07:00
|
|
|
private CheckCounterService checkCounterService;
|
|
|
|
|
|
|
|
|
|
private UserRepository userRepository;
|
|
|
|
|
|
2026-03-24 16:16:16 +07:00
|
|
|
private NoCopyFileService noCopyFileService;
|
|
|
|
|
|
|
|
|
|
private FileUtil fileUtil;
|
|
|
|
|
|
2026-04-04 15:31:51 +07:00
|
|
|
private ZipService zipService;
|
|
|
|
|
|
2026-04-20 18:16:46 +07:00
|
|
|
private CloudStorageService cloudStorageService;
|
|
|
|
|
|
2026-01-27 20:27:39 +07:00
|
|
|
@GetMapping("/public/{fileId}")
|
2026-04-21 11:50:25 +07:00
|
|
|
public ResponseEntity<StreamingResponseBody> getPublicFile(@PathVariable String fileId) {
|
2026-02-21 23:21:44 +07:00
|
|
|
try {
|
|
|
|
|
FileEntity fileEntity = fileRepository.findById(fileId)
|
2026-04-21 11:50:25 +07:00
|
|
|
.orElseThrow(() -> new FileNotFoundException("Not found in DB: " + fileId));
|
2026-02-05 03:30:15 +07:00
|
|
|
|
2026-02-21 23:21:44 +07:00
|
|
|
MediaType mediaType = getMediaType(fileEntity);
|
2026-02-10 12:46:45 +07:00
|
|
|
|
2026-02-21 23:21:44 +07:00
|
|
|
ContentDisposition contentDisposition = ContentDisposition.inline()
|
|
|
|
|
.filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8)
|
|
|
|
|
.build();
|
|
|
|
|
|
2026-04-21 11:50:25 +07:00
|
|
|
StreamingResponseBody responseBody = outputStream -> {
|
|
|
|
|
try (InputStream inputStream = cloudStorageService.readFileFromStorage(fileEntity.getFilePath())) {
|
|
|
|
|
ByteStreams.copy(inputStream, outputStream);
|
|
|
|
|
} catch (NoSuchKeyException e) {
|
|
|
|
|
log.error("File disappeared from S3 during streaming: {}", fileId, e);
|
|
|
|
|
throw new IOException("File not found in storage", e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-21 23:21:44 +07:00
|
|
|
return ResponseEntity.ok()
|
|
|
|
|
.contentType(mediaType)
|
|
|
|
|
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
|
|
|
|
|
.header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600")
|
2026-04-21 11:50:25 +07:00
|
|
|
.body(responseBody);
|
2026-02-21 23:21:44 +07:00
|
|
|
} catch (FileNotFoundException e) {
|
2026-04-21 02:21:33 +07:00
|
|
|
log.warn("File not found in DB: {}", fileId);
|
2026-02-21 23:21:44 +07:00
|
|
|
return ResponseEntity.notFound().build();
|
2026-04-21 11:50:25 +07:00
|
|
|
} catch (NoSuchKeyException e) {
|
|
|
|
|
log.warn("File not found in S3: {}", fileId);
|
|
|
|
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
2026-04-21 02:21:33 +07:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("Error reading file: {}", fileId, e);
|
2026-02-21 23:21:44 +07:00
|
|
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private MediaType getMediaType(FileEntity fileEntity) {
|
|
|
|
|
try {
|
|
|
|
|
return MediaType.parseMediaType(fileEntity.getMimeType());
|
|
|
|
|
} catch (InvalidMediaTypeException e) {
|
|
|
|
|
String extension = fileEntity.getFileExtension().toLowerCase();
|
|
|
|
|
switch (extension) {
|
|
|
|
|
case "jpg":
|
|
|
|
|
case "jpeg":
|
|
|
|
|
return MediaType.IMAGE_JPEG;
|
|
|
|
|
case "png":
|
|
|
|
|
return MediaType.IMAGE_PNG;
|
|
|
|
|
case "pdf":
|
|
|
|
|
return MediaType.APPLICATION_PDF;
|
|
|
|
|
default:
|
|
|
|
|
return MediaType.APPLICATION_OCTET_STREAM;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 03:30:15 +07:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:27:31 +07:00
|
|
|
@GetMapping("/protected/{fileId}/{type}")
|
2026-02-05 03:30:15 +07:00
|
|
|
public ResponseEntity<Resource> getProtectedFile(
|
2026-02-26 00:27:31 +07:00
|
|
|
@PathVariable String fileId, @PathVariable String type) throws IOException {
|
2026-02-05 03:30:15 +07:00
|
|
|
|
|
|
|
|
FileEntity fileEntity = fileRepository.findById(fileId)
|
|
|
|
|
.orElseThrow(() -> new RuntimeException("File not found"));
|
2026-01-27 20:27:39 +07:00
|
|
|
|
2026-04-21 03:11:52 +07:00
|
|
|
String filePath = type.equals("thumbnail") ? fileEntity.getThumbnailPath(): fileEntity.getFilePath();
|
|
|
|
|
|
2026-04-21 19:09:27 +07:00
|
|
|
// InputStream file = cloudStorageService.readFileFromStorage(filePath);
|
2026-02-26 00:27:31 +07:00
|
|
|
|
2026-04-21 19:09:27 +07:00
|
|
|
// if (file == null || !file.exists()) {
|
|
|
|
|
// log.error("File not found in storage: {}", fileEntity.getFilePath());
|
|
|
|
|
// return ResponseEntity.notFound().build();
|
|
|
|
|
// }
|
2026-01-27 20:27:39 +07:00
|
|
|
|
2026-02-05 03:30:15 +07:00
|
|
|
if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED ||
|
|
|
|
|
fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) {
|
|
|
|
|
throw new RuntimeException("File is not protected");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:54:51 +07:00
|
|
|
String fileName = fileEntity.getOriginalFileName().replace("." +
|
|
|
|
|
fileEntity.getFileExtension(), "") + "_nocopy_protected" +
|
|
|
|
|
"." + fileEntity.getFileExtension();
|
2026-02-16 18:57:45 +07:00
|
|
|
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
|
|
|
|
.replace("+", "%20");
|
|
|
|
|
|
2026-04-21 19:09:27 +07:00
|
|
|
InputStreamResource resource = new InputStreamResource(cloudStorageService.readFileFromStorage(filePath));
|
2026-04-20 18:16:46 +07:00
|
|
|
|
2026-01-27 20:27:39 +07:00
|
|
|
return ResponseEntity.ok()
|
|
|
|
|
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
|
|
|
|
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
2026-02-16 18:57:45 +07:00
|
|
|
"inline; filename*=UTF-8''" + encodedFileName)
|
2026-01-27 20:27:39 +07:00
|
|
|
.body(resource);
|
|
|
|
|
}
|
2026-01-28 23:25:16 +07:00
|
|
|
|
|
|
|
|
@GetMapping("check-file/status/{userId}")
|
|
|
|
|
public ResponseEntity<CheckStatus> getStatus(@PathVariable Long userId) {
|
|
|
|
|
CheckStatus status = checkCounterService.getCurrentStatus(userId);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity.ok(status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("check-file/update-limit/{userId}/{limit}")
|
|
|
|
|
public ResponseEntity<ProtectedFileCheck> getStatus(@PathVariable Long userId, @PathVariable Integer limit) {
|
|
|
|
|
User user = userRepository.findById(userId).orElseThrow();
|
|
|
|
|
ProtectedFileCheck protectedFileCheck = checkCounterService.updateLimit(user, limit);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity
|
|
|
|
|
.ok()
|
|
|
|
|
.body(protectedFileCheck);
|
|
|
|
|
}
|
2026-03-24 16:16:16 +07:00
|
|
|
|
|
|
|
|
@PostMapping("protect/{fileId}/{convertTo}")
|
|
|
|
|
public ResponseEntity<FileProtector.FileInfo> convert(@PathVariable String fileId, @PathVariable String convertTo) {
|
|
|
|
|
FileEntity file = fileRepository.findByFileId(fileId);
|
|
|
|
|
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(file, convertTo);
|
|
|
|
|
|
|
|
|
|
noCopyFileService.addFile(fileInfo);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity
|
|
|
|
|
.ok()
|
|
|
|
|
.body(fileInfo);
|
|
|
|
|
}
|
2026-04-04 15:31:51 +07:00
|
|
|
|
|
|
|
|
@GetMapping("/download/user-passport/archive/{userId}")
|
|
|
|
|
public ResponseEntity<byte[]> downloadPassportZip(@PathVariable Long userId) {
|
|
|
|
|
try {
|
|
|
|
|
byte[] zipBytes = zipService.assembleZipFromSplitFiles(userId);
|
|
|
|
|
|
|
|
|
|
return ResponseEntity.ok()
|
|
|
|
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
|
|
|
|
"attachment; filename=passport_" + userId + ".zip")
|
|
|
|
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
|
|
|
|
.contentLength(zipBytes.length)
|
|
|
|
|
.body(zipBytes);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
log.error("Failed to assemble passport for user {}", userId, e);
|
|
|
|
|
return ResponseEntity.internalServerError().build();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 20:27:39 +07:00
|
|
|
}
|