mirror of
https://github.com/RoastSlav/quickdrop.git
synced 2025-12-20 13:59:36 -06:00
Much optimized download process
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user