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