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("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) {

View File

@@ -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;
}
}

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 {
FileOutputStream fos = new FileOutputStream(finalFile, true);
byte[] salt = generateRandomBytes();

View File

@@ -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<StreamingResponseBody> createFileDownloadResponse(Path filePath, FileEntity fileEntity, HttpServletRequest request) throws IOException {
StreamingResponseBody responseBody = getStreamingResponseBody(filePath, fileEntity);
Resource resource = new UrlResource(filePath.toUri());
private ResponseEntity<StreamingResponseBody> 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);
}

View File

@@ -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");