diff --git a/src/main/java/org/rostislav/quickdrop/controller/FileRestController.java b/src/main/java/org/rostislav/quickdrop/controller/FileRestController.java index 1309a0b..252900a 100644 --- a/src/main/java/org/rostislav/quickdrop/controller/FileRestController.java +++ b/src/main/java/org/rostislav/quickdrop/controller/FileRestController.java @@ -41,6 +41,7 @@ public class FileRestController { @RequestParam("fileName") String fileName, @RequestParam("chunkNumber") int chunkNumber, @RequestParam("totalChunks") int totalChunks, + @RequestParam(value = "fileSize", required = false) Long fileSize, @RequestParam(value = "description", required = false) String description, @RequestParam(value = "keepIndefinitely", defaultValue = "false") Boolean keepIndefinitely, @RequestParam(value = "password", required = false) String password, @@ -53,7 +54,7 @@ public class FileRestController { try { logger.info("Submitting chunk {} of {} for file: {}", chunkNumber, totalChunks, fileName); - FileUploadRequest fileUploadRequest = new FileUploadRequest(description, keepIndefinitely, password, hidden, fileName, totalChunks); + FileUploadRequest fileUploadRequest = new FileUploadRequest(description, keepIndefinitely, password, hidden, fileName, totalChunks, fileSize); FileEntity fileEntity = asyncFileMergeService.submitChunk(fileUploadRequest, file, chunkNumber); return ResponseEntity.ok(fileEntity); } catch (IOException e) { diff --git a/src/main/java/org/rostislav/quickdrop/model/FileUploadRequest.java b/src/main/java/org/rostislav/quickdrop/model/FileUploadRequest.java index 0134a2f..45f6177 100644 --- a/src/main/java/org/rostislav/quickdrop/model/FileUploadRequest.java +++ b/src/main/java/org/rostislav/quickdrop/model/FileUploadRequest.java @@ -3,6 +3,7 @@ package org.rostislav.quickdrop.model; public class FileUploadRequest { public String fileName; public int totalChunks; + public Long fileSize; public String description; public boolean keepIndefinitely; public String password; @@ -11,12 +12,13 @@ public class FileUploadRequest { public FileUploadRequest() { } - public FileUploadRequest(String description, boolean keepIndefinitely, String password, boolean hidden, String fileName, int totalChunks) { + public FileUploadRequest(String description, boolean keepIndefinitely, String password, boolean hidden, String fileName, int totalChunks, Long fileSize) { this.description = description; this.keepIndefinitely = keepIndefinitely; this.password = password; this.hidden = hidden; this.fileName = fileName; this.totalChunks = totalChunks; + this.fileSize = fileSize; } } diff --git a/src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java b/src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java index 88d8623..f56f1f9 100644 --- a/src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java +++ b/src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java @@ -61,6 +61,22 @@ public class FileEncryptionService { } } + public InputStream getDecryptedInputStream(File inputFile, String password) throws Exception { + FileInputStream fis = new FileInputStream(inputFile); + byte[] salt = new byte[16]; + byte[] iv = new byte[16]; + + fis.read(salt); + fis.read(iv); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + SecretKey secretKey = generateKeyFromPassword(password, salt); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); + CipherInputStream cipherInputStream = new CipherInputStream(fis, cipher); + return cipherInputStream; + } + public OutputStream getEncryptedOutputStream(File finalFile, String password) throws Exception { FileOutputStream fos = new FileOutputStream(finalFile, true); byte[] salt = generateRandomBytes(); diff --git a/src/main/java/org/rostislav/quickdrop/service/FileService.java b/src/main/java/org/rostislav/quickdrop/service/FileService.java index f6e1291..36ecb8a 100644 --- a/src/main/java/org/rostislav/quickdrop/service/FileService.java +++ b/src/main/java/org/rostislav/quickdrop/service/FileService.java @@ -15,8 +15,6 @@ import org.rostislav.quickdrop.repository.ShareTokenRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,10 +22,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -64,26 +59,14 @@ public class FileService { this.shareTokenRepository = shareTokenRepository; } - private static StreamingResponseBody getStreamingResponseBody(Path outputFile, FileEntity fileEntity) { + private static StreamingResponseBody getStreamingResponseBody(InputStream inputStream) { return outputStream -> { - try (FileInputStream inputStream = new FileInputStream(outputFile.toFile())) { - byte[] buffer = new byte[8192]; - 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()); - } - } + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); } + outputStream.flush(); }; } @@ -112,7 +95,7 @@ public class FileService { logger.info("Saving file: {}", file.getName()); - FileEntity fileEntity = populateFileEntity(file, fileUploadRequest, uuid); + FileEntity fileEntity = populateFileEntity(fileUploadRequest, uuid); logger.info("FileEntity inserted into database: {}", fileEntity); return fileRepository.save(fileEntity); @@ -126,12 +109,12 @@ public class FileService { return request.password != null && !request.password.isBlank() && applicationSettingsService.isEncryptionEnabled(); } - private FileEntity populateFileEntity(File file, FileUploadRequest request, String uuid) { + private FileEntity populateFileEntity(FileUploadRequest request, String uuid) { FileEntity fileEntity = new FileEntity(); fileEntity.name = request.fileName; fileEntity.uuid = uuid; fileEntity.description = request.description; - fileEntity.size = file.length(); + fileEntity.size = request.fileSize; fileEntity.keepIndefinitely = request.keepIndefinitely; fileEntity.hidden = request.hidden; fileEntity.encrypted = shouldEncrypt(request); @@ -194,13 +177,26 @@ public class FileService { Path filePath = Path.of(applicationSettingsService.getFileStoragePath(), fileEntity.uuid); String password = getFilePasswordFromSessionToken(request); - Path decryptedFilePath = decryptFileIfNeeded(fileEntity, filePath, password); - if (decryptedFilePath == null) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + + InputStream inputStream; + if (fileEntity.encrypted) { + try { + inputStream = fileEncryptionService.getDecryptedInputStream(filePath.toFile(), password); + } catch (Exception e) { + logger.error("Error decrypting file: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } else { + try { + inputStream = new FileInputStream(filePath.toFile()); + } catch (FileNotFoundException e) { + logger.error("File not found: {}", filePath); + return ResponseEntity.notFound().build(); + } } try { - return createFileDownloadResponse(decryptedFilePath, fileEntity, request); + return createFileDownloadResponse(inputStream, fileEntity, request); } catch (Exception e) { logger.error("Error preparing file download response: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); @@ -386,7 +382,7 @@ public class FileService { Path decryptedFilePath = encryptedFilePath.resolveSibling(file.uuid + "-decrypted"); // Decrypt the file if necessary - if (file.passwordHash != null && !Files.exists(decryptedFilePath)) { + if (file.encrypted && !Files.exists(decryptedFilePath)) { try { String password = sessionService.getPasswordForFileSessionToken(sessionToken).getPassword(); fileEncryptionService.decryptFile(encryptedFilePath.toFile(), decryptedFilePath.toFile(), password); @@ -409,32 +405,15 @@ public class FileService { return shareTokenRepository.getShareTokenEntityByToken(token); } - private Path decryptFileIfNeeded(FileEntity fileEntity, Path filePath, String password) { - if (!fileEntity.encrypted) { - return filePath; - } - try { - Path tempFile = File.createTempFile("Decrypted", "tmp").toPath(); - logger.info("Decrypting file: {}", filePath); - fileEncryptionService.decryptFile(filePath.toFile(), tempFile.toFile(), password); - logger.info("File decrypted: {}", tempFile); - return tempFile; - } catch (Exception e) { - logger.error("Error decrypting file: {}", e.getMessage()); - return null; - } - } - - private ResponseEntity createFileDownloadResponse(Path filePath, FileEntity fileEntity, HttpServletRequest request) throws IOException { - StreamingResponseBody responseBody = getStreamingResponseBody(filePath, fileEntity); - Resource resource = new UrlResource(filePath.toUri()); + private ResponseEntity createFileDownloadResponse(InputStream inputStream, FileEntity fileEntity, HttpServletRequest request) throws IOException { + StreamingResponseBody responseBody = getStreamingResponseBody(inputStream); logger.info("Sending file: {}", fileEntity); logDownload(fileEntity, request); 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())) + .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileEntity.size)) .header("X-Accel-Buffering", "no") .body(responseBody); } diff --git a/src/main/resources/static/js/upload.js b/src/main/resources/static/js/upload.js index 9a24f2e..9418aa4 100644 --- a/src/main/resources/static/js/upload.js +++ b/src/main/resources/static/js/upload.js @@ -76,7 +76,7 @@ function startChunkUpload() { const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); - const formData = buildChunkFormData(chunk, currentChunk, file.name, totalChunks); + const formData = buildChunkFormData(chunk, currentChunk, file.name, totalChunks, file.size); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/file/upload-chunk", true); @@ -140,7 +140,7 @@ function startChunkUpload() { uploadNextChunk(); } -function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks) { +function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks, fileSize) { const uploadForm = document.getElementById("uploadForm"); const formData = new FormData(); @@ -149,6 +149,7 @@ function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks) { formData.append("fileName", fileName); formData.append("chunkNumber", chunkNumber); formData.append("totalChunks", totalChunks); + formData.append("fileSize", fileSize); // Keep Indefinitely + hidden const keepIndefinitelyCheckbox = document.getElementById("keepIndefinitely");