Much optimized download process

This commit is contained in:
Rostislav Raykov
2025-03-29 16:29:01 +02:00
parent a87c145a0b
commit 821b10ea94
5 changed files with 55 additions and 56 deletions

View File

@@ -41,6 +41,7 @@ public class FileRestController {
@RequestParam("fileName") String fileName, @RequestParam("fileName") String fileName,
@RequestParam("chunkNumber") int chunkNumber, @RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks, @RequestParam("totalChunks") int totalChunks,
@RequestParam(value = "fileSize", required = false) Long fileSize,
@RequestParam(value = "description", required = false) String description, @RequestParam(value = "description", required = false) String description,
@RequestParam(value = "keepIndefinitely", defaultValue = "false") Boolean keepIndefinitely, @RequestParam(value = "keepIndefinitely", defaultValue = "false") Boolean keepIndefinitely,
@RequestParam(value = "password", required = false) String password, @RequestParam(value = "password", required = false) String password,
@@ -53,7 +54,7 @@ public class FileRestController {
try { try {
logger.info("Submitting chunk {} of {} for file: {}", chunkNumber, totalChunks, fileName); 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); FileEntity fileEntity = asyncFileMergeService.submitChunk(fileUploadRequest, file, chunkNumber);
return ResponseEntity.ok(fileEntity); return ResponseEntity.ok(fileEntity);
} catch (IOException e) { } catch (IOException e) {

View File

@@ -3,6 +3,7 @@ package org.rostislav.quickdrop.model;
public class FileUploadRequest { public class FileUploadRequest {
public String fileName; public String fileName;
public int totalChunks; public int totalChunks;
public Long fileSize;
public String description; public String description;
public boolean keepIndefinitely; public boolean keepIndefinitely;
public String password; public String password;
@@ -11,12 +12,13 @@ public class FileUploadRequest {
public 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.description = description;
this.keepIndefinitely = keepIndefinitely; this.keepIndefinitely = keepIndefinitely;
this.password = password; this.password = password;
this.hidden = hidden; this.hidden = hidden;
this.fileName = fileName; this.fileName = fileName;
this.totalChunks = totalChunks; this.totalChunks = totalChunks;
this.fileSize = fileSize;
} }
} }

View File

@@ -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 { public OutputStream getEncryptedOutputStream(File finalFile, String password) throws Exception {
FileOutputStream fos = new FileOutputStream(finalFile, true); FileOutputStream fos = new FileOutputStream(finalFile, true);
byte[] salt = generateRandomBytes(); byte[] salt = generateRandomBytes();

View File

@@ -15,8 +15,6 @@ import org.rostislav.quickdrop.repository.ShareTokenRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; 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.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -24,10 +22,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.File; import java.io.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@@ -64,26 +59,14 @@ public class FileService {
this.shareTokenRepository = shareTokenRepository; this.shareTokenRepository = shareTokenRepository;
} }
private static StreamingResponseBody getStreamingResponseBody(Path outputFile, FileEntity fileEntity) { private static StreamingResponseBody getStreamingResponseBody(InputStream inputStream) {
return outputStream -> { return outputStream -> {
try (FileInputStream inputStream = new FileInputStream(outputFile.toFile())) { byte[] buffer = new byte[8192];
byte[] buffer = new byte[8192]; int bytesRead;
int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) {
while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead);
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());
}
}
} }
outputStream.flush();
}; };
} }
@@ -112,7 +95,7 @@ public class FileService {
logger.info("Saving file: {}", file.getName()); logger.info("Saving file: {}", file.getName());
FileEntity fileEntity = populateFileEntity(file, fileUploadRequest, uuid); FileEntity fileEntity = populateFileEntity(fileUploadRequest, uuid);
logger.info("FileEntity inserted into database: {}", fileEntity); logger.info("FileEntity inserted into database: {}", fileEntity);
return fileRepository.save(fileEntity); return fileRepository.save(fileEntity);
@@ -126,12 +109,12 @@ public class FileService {
return request.password != null && !request.password.isBlank() && applicationSettingsService.isEncryptionEnabled(); 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 fileEntity = new FileEntity();
fileEntity.name = request.fileName; fileEntity.name = request.fileName;
fileEntity.uuid = uuid; fileEntity.uuid = uuid;
fileEntity.description = request.description; fileEntity.description = request.description;
fileEntity.size = file.length(); fileEntity.size = request.fileSize;
fileEntity.keepIndefinitely = request.keepIndefinitely; fileEntity.keepIndefinitely = request.keepIndefinitely;
fileEntity.hidden = request.hidden; fileEntity.hidden = request.hidden;
fileEntity.encrypted = shouldEncrypt(request); fileEntity.encrypted = shouldEncrypt(request);
@@ -194,13 +177,26 @@ public class FileService {
Path filePath = Path.of(applicationSettingsService.getFileStoragePath(), fileEntity.uuid); Path filePath = Path.of(applicationSettingsService.getFileStoragePath(), fileEntity.uuid);
String password = getFilePasswordFromSessionToken(request); String password = getFilePasswordFromSessionToken(request);
Path decryptedFilePath = decryptFileIfNeeded(fileEntity, filePath, password);
if (decryptedFilePath == null) { InputStream inputStream;
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 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 { try {
return createFileDownloadResponse(decryptedFilePath, fileEntity, request); return createFileDownloadResponse(inputStream, fileEntity, request);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error preparing file download response: {}", e.getMessage()); logger.error("Error preparing file download response: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
@@ -386,7 +382,7 @@ public class FileService {
Path decryptedFilePath = encryptedFilePath.resolveSibling(file.uuid + "-decrypted"); Path decryptedFilePath = encryptedFilePath.resolveSibling(file.uuid + "-decrypted");
// Decrypt the file if necessary // Decrypt the file if necessary
if (file.passwordHash != null && !Files.exists(decryptedFilePath)) { if (file.encrypted && !Files.exists(decryptedFilePath)) {
try { try {
String password = sessionService.getPasswordForFileSessionToken(sessionToken).getPassword(); String password = sessionService.getPasswordForFileSessionToken(sessionToken).getPassword();
fileEncryptionService.decryptFile(encryptedFilePath.toFile(), decryptedFilePath.toFile(), password); fileEncryptionService.decryptFile(encryptedFilePath.toFile(), decryptedFilePath.toFile(), password);
@@ -409,32 +405,15 @@ public class FileService {
return shareTokenRepository.getShareTokenEntityByToken(token); return shareTokenRepository.getShareTokenEntityByToken(token);
} }
private Path decryptFileIfNeeded(FileEntity fileEntity, Path filePath, String password) { private ResponseEntity<StreamingResponseBody> createFileDownloadResponse(InputStream inputStream, FileEntity fileEntity, HttpServletRequest request) throws IOException {
if (!fileEntity.encrypted) { StreamingResponseBody responseBody = getStreamingResponseBody(inputStream);
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<StreamingResponseBody> createFileDownloadResponse(Path filePath, FileEntity fileEntity, HttpServletRequest request) throws IOException {
StreamingResponseBody responseBody = getStreamingResponseBody(filePath, fileEntity);
Resource resource = new UrlResource(filePath.toUri());
logger.info("Sending file: {}", fileEntity); logger.info("Sending file: {}", fileEntity);
logDownload(fileEntity, request); logDownload(fileEntity, request);
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(fileEntity.name, StandardCharsets.UTF_8) + "\"") .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + URLEncoder.encode(fileEntity.name, StandardCharsets.UTF_8) + "\"")
.header(HttpHeaders.CONTENT_TYPE, "application/octet-stream") .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") .header("X-Accel-Buffering", "no")
.body(responseBody); .body(responseBody);
} }

View File

@@ -76,7 +76,7 @@ function startChunkUpload() {
const end = Math.min(start + chunkSize, file.size); const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); 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(); const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/file/upload-chunk", true); xhr.open("POST", "/api/file/upload-chunk", true);
@@ -140,7 +140,7 @@ function startChunkUpload() {
uploadNextChunk(); uploadNextChunk();
} }
function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks) { function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks, fileSize) {
const uploadForm = document.getElementById("uploadForm"); const uploadForm = document.getElementById("uploadForm");
const formData = new FormData(); const formData = new FormData();
@@ -149,6 +149,7 @@ function buildChunkFormData(chunk, chunkNumber, fileName, totalChunks) {
formData.append("fileName", fileName); formData.append("fileName", fileName);
formData.append("chunkNumber", chunkNumber); formData.append("chunkNumber", chunkNumber);
formData.append("totalChunks", totalChunks); formData.append("totalChunks", totalChunks);
formData.append("fileSize", fileSize);
// Keep Indefinitely + hidden // Keep Indefinitely + hidden
const keepIndefinitelyCheckbox = document.getElementById("keepIndefinitely"); const keepIndefinitelyCheckbox = document.getElementById("keepIndefinitely");