From f0d4c43a1fe2c6caed363eb677c2999b3b0cdeab Mon Sep 17 00:00:00 2001 From: Rostislav Raykov Date: Fri, 25 Oct 2024 22:45:00 +0300 Subject: [PATCH] Added a client side check for the file size and now deletes the temp decrypted file when downloading --- .../controller/FileViewController.java | 8 +- .../quickdrop/service/FileService.java | 88 +++++++++++++------ src/main/resources/application.properties | 4 +- src/main/resources/static/js/upload.js | 13 +++ src/main/resources/templates/upload.html | 14 +++ 5 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java b/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java index 74173e2..955409c 100644 --- a/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java +++ b/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java @@ -1,9 +1,10 @@ package org.rostislav.quickdrop.controller; +import java.util.List; + import jakarta.servlet.http.HttpServletRequest; import org.rostislav.quickdrop.model.FileEntity; import org.rostislav.quickdrop.service.FileService; -import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -12,8 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; - -import java.util.List; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import static org.rostislav.quickdrop.util.FileUtils.populateModelAttributes; @@ -66,7 +66,7 @@ public class FileViewController { } @GetMapping("/download/{id}") - public ResponseEntity downloadFile(@PathVariable Long id, HttpServletRequest request) { + public ResponseEntity downloadFile(@PathVariable Long id, HttpServletRequest request) { FileEntity fileEntity = fileService.getFile(id); if (fileEntity.passwordHash != null) { diff --git a/src/main/java/org/rostislav/quickdrop/service/FileService.java b/src/main/java/org/rostislav/quickdrop/service/FileService.java index b21acd5..aa771cb 100644 --- a/src/main/java/org/rostislav/quickdrop/service/FileService.java +++ b/src/main/java/org/rostislav/quickdrop/service/FileService.java @@ -1,5 +1,16 @@ package org.rostislav.quickdrop.service; +import java.io.File; +import java.io.FileInputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + import org.rostislav.quickdrop.model.FileEntity; import org.rostislav.quickdrop.model.FileUploadRequest; import org.rostislav.quickdrop.repository.FileRepository; @@ -14,16 +25,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import static org.rostislav.quickdrop.util.DataValidator.validateObjects; import static org.rostislav.quickdrop.util.FileEncryptionUtils.decryptFile; @@ -42,6 +44,41 @@ public class FileService { this.passwordEncoder = passwordEncoder; } + private static StreamingResponseBody getStreamingResponseBody(Path outputFile, FileEntity fileEntity) { + return outputStream -> { + try (FileInputStream inputStream = new FileInputStream(outputFile.toFile())) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } finally { + if (fileEntity.passwordHash != null) { + try { + Files.delete(outputFile); + logger.info("Decrypted file deleted: {}", outputFile); + } catch ( + Exception e) { + logger.error("Error deleting decrypted file: {}", e.getMessage()); + } + } + } + }; + } + + private boolean saveUnencryptedFile(MultipartFile file, Path path) { + try { + Files.createFile(path); + Files.write(path, file.getBytes()); + logger.info("File saved: {}", path); + } catch (Exception e) { + logger.error("Error saving file: {}", e.getMessage()); + return false; + } + return true; + } + public FileEntity saveFile(MultipartFile file, FileUploadRequest fileUploadRequest) { if (!validateObjects(file, fileUploadRequest)) { return null; @@ -73,20 +110,12 @@ public class FileService { fileEntity.passwordHash = passwordEncoder.encode(fileUploadRequest.password); } - logger.info("FileEntity saved: {}", fileEntity); + logger.info("FileEntity inserted into database: {}", fileEntity); return fileRepository.save(fileEntity); } - private boolean saveUnencryptedFile(MultipartFile file, Path path) { - try { - Files.createFile(path); - Files.write(path, file.getBytes()); - logger.info("File saved: {}", path); - } catch (Exception e) { - logger.error("Error saving file: {}", e.getMessage()); - return false; - } - return true; + public List getFiles() { + return fileRepository.findAll(); } public boolean saveEncryptedFile(Path savePath, MultipartFile file, FileUploadRequest fileUploadRequest) { @@ -102,7 +131,9 @@ public class FileService { try { Path encryptedFile = Files.createFile(savePath); + logger.info("Encrypting file: {}", encryptedFile); encryptFile(tempFile.toFile(), encryptedFile.toFile(), fileUploadRequest.password); + logger.info("Encrypted file saved: {}", encryptedFile); } catch (Exception e) { logger.error("Error encrypting file: {}", e.getMessage()); return false; @@ -110,6 +141,7 @@ public class FileService { try { Files.delete(tempFile); + logger.info("Temp file deleted: {}", tempFile); } catch (Exception e) { logger.error("Error deleting temp file: {}", e.getMessage()); return false; @@ -118,13 +150,10 @@ public class FileService { return true; } - public List getFiles() { - return fileRepository.findAll(); - } - - public ResponseEntity downloadFile(Long id, String password) { + public ResponseEntity downloadFile(Long id, String password) { FileEntity fileEntity = fileRepository.findById(id).orElse(null); if (fileEntity == null) { + logger.info("File not found: {}", id); return ResponseEntity.notFound().build(); } @@ -133,7 +162,9 @@ public class FileService { if (fileEntity.passwordHash != null) { try { outputFile = File.createTempFile("Decrypted", "tmp").toPath(); + logger.info("Decrypting file: {}", pathOfFile); decryptFile(pathOfFile.toFile(), outputFile.toFile(), password); + logger.info("File decrypted: {}", outputFile); } catch (Exception e) { logger.error("Error decrypting file: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); @@ -142,13 +173,16 @@ public class FileService { outputFile = pathOfFile; } + StreamingResponseBody responseBody = getStreamingResponseBody(outputFile, fileEntity); + try { Resource resource = new UrlResource(outputFile.toUri()); + logger.info("Sending file: {}", fileEntity); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(fileEntity.name, StandardCharsets.UTF_8) + "\"") .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream") .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(resource.contentLength())) - .body(resource); + .body(responseBody); } catch (Exception e) { logger.error("Error reading file: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f834977..ebb6105 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,8 +8,8 @@ spring.jpa.hibernate.ddl-auto=update spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.cache=false -spring.servlet.multipart.max-file-size=1024MB -spring.servlet.multipart.max-request-size=1024MB +spring.servlet.multipart.max-file-size=1025MB +spring.servlet.multipart.max-request-size=1025MB server.tomcat.connection-timeout=60000 file.save.path=files file.max.age=30 diff --git a/src/main/resources/static/js/upload.js b/src/main/resources/static/js/upload.js index c202e4c..59dbe58 100644 --- a/src/main/resources/static/js/upload.js +++ b/src/main/resources/static/js/upload.js @@ -67,4 +67,17 @@ function isPasswordProtected() { const passwordField = document.getElementById("password"); return passwordField && passwordField.value.trim() !== ""; +} + +function validateFileSize() { + const file = document.getElementById('file').files[0]; + const maxSize = 1024 * 1024 * 1024; // 1GB + const fileSizeAlert = document.getElementById('fileSizeAlert'); + + if (file.size > maxSize) { + fileSizeAlert.style.display = 'block'; + document.getElementById('file').value = ''; + } else { + fileSizeAlert.style.display = 'none'; + } } \ No newline at end of file diff --git a/src/main/resources/templates/upload.html b/src/main/resources/templates/upload.html index e6a8395..ab07dc5 100644 --- a/src/main/resources/templates/upload.html +++ b/src/main/resources/templates/upload.html @@ -67,9 +67,23 @@ name="file" required type="file" + onchange="validateFileSize()" /> + + +