Refactor file upload and password handling logic

Renamed and reorganized methods for clearer functionality (e.g., `checkPassword` to `checkFilePassword`). Improved chunk upload handling by adding helper methods and error handling for chunk processing. Simplified file encryption and merging logic, centralizing repeated code for reliability and readability.
This commit is contained in:
Rostislav Raykov
2024-12-14 18:24:30 +02:00
parent 5dd3667a77
commit 5fa835e2a0
7 changed files with 307 additions and 264 deletions

View File

@@ -38,34 +38,28 @@ public class SecurityConfig {
@RefreshScope
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
if (applicationSettingsService.isAppPasswordEnabled()) {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/password/login", "/favicon.ico", "/error", "/file/share/**", "/api/file/download/**").permitAll()
.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/password/login")
.permitAll()
.failureUrl("/password/login?error")
.defaultSuccessUrl("/", true)
).authenticationProvider(authenticationProvider()
).csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
).headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
.contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors *;"))
).cors(Customizer.withDefaults());
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/password/login", "/favicon.ico", "/error", "/file/share/**", "/api/file/download/**").permitAll()
.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/password/login")
.permitAll()
.failureUrl("/password/login?error")
.defaultSuccessUrl("/", true)
).authenticationProvider(authenticationProvider()
);
} else {
http
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
).csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
).headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
.contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors *;"))
).cors(Customizer.withDefaults());
http.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll());
}
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
).headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
.contentSecurityPolicy(csp -> csp.policyDirectives("frame-ancestors *;"))
).cors(Customizer.withDefaults());
return http.build();
}

View File

@@ -83,8 +83,8 @@ public class AdminViewController {
if (!applicationSettingsService.checkForAdminPassword(request)) {
return "redirect:password";
}
settings.setMaxFileSize(megabytesToBytes(settings.getMaxFileSize()));
settings.setMaxFileSize(megabytesToBytes(settings.getMaxFileSize()));
applicationSettingsService.updateApplicationSettings(settings, settings.getAppPassword());
return "redirect:dashboard";
@@ -93,6 +93,7 @@ public class AdminViewController {
@PostMapping("/password")
public String checkAdminPassword(@RequestParam String password, HttpServletRequest request) {
String adminPasswordHash = applicationSettingsService.getAdminPasswordHash();
if (BCrypt.checkpw(password, adminPasswordHash)) {
request.getSession().setAttribute("adminPassword", adminPasswordHash);
return "redirect:dashboard";

View File

@@ -2,7 +2,6 @@ package org.rostislav.quickdrop.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.rostislav.quickdrop.entity.FileEntity;
import org.rostislav.quickdrop.model.FileUploadRequest;
import org.rostislav.quickdrop.service.FileService;
import org.rostislav.quickdrop.util.FileUtils;
import org.springframework.http.HttpStatus;
@@ -36,28 +35,22 @@ public class FileRestController {
@RequestParam(value = "password", required = false) String password,
@RequestParam(value = "hidden", defaultValue = "false") Boolean hidden) {
try {
fileService.saveChunk(file, fileName, chunkNumber);
fileService.saveFileChunk(file, fileName, chunkNumber);
if (chunkNumber + 1 == totalChunks) {
FileUploadRequest fileUploadRequest = new FileUploadRequest(description, keepIndefinitely, password, hidden);
FileEntity fileEntity = fileService.assembleChunks(fileName, totalChunks, fileUploadRequest);
if (fileEntity != null) {
return ResponseEntity.ok(fileEntity);
} else {
return ResponseEntity.badRequest().build();
}
return fileService.finalizeFile(fileName, totalChunks, description, keepIndefinitely, password, hidden);
}
Map<String, String> response = new HashMap<>();
response.put("message", "Chunk " + chunkNumber + " uploaded successfully");
return ResponseEntity.ok(response);
} catch (IOException e) {
fileService.deleteTempFiles(fileName);
fileService.deleteChunkFilesFromTemp(fileName);
fileService.deleteFullFileFromTemp(fileName);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("{\"error\": \"Error processing chunk\"}");
}
Map<String, String> response = new HashMap<>();
response.put("message", "Chunk " + chunkNumber + " uploaded successfully");
return ResponseEntity.ok(response);
}
@PostMapping("/share/{id}")
@@ -67,12 +60,8 @@ public class FileRestController {
return ResponseEntity.badRequest().body("File not found.");
}
String password = (String) request.getSession().getAttribute("password");
if (fileEntity.passwordHash != null) {
if (password == null || !fileService.checkPassword(fileEntity.uuid, password)) {
System.out.println("Invalid or missing password.");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid or missing password in session.");
}
if (!isFilePasswordValid(fileEntity, request)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid or missing password in session.");
}
String token = fileService.generateShareToken(id, LocalDate.now().plusDays(30));
@@ -100,4 +89,12 @@ public class FileRestController {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private boolean isFilePasswordValid(FileEntity fileEntity, HttpServletRequest request) {
String sessionPassword = (String) request.getSession().getAttribute("password");
if (fileEntity.passwordHash != null) {
return sessionPassword != null && fileService.checkFilePassword(fileEntity.uuid, sessionPassword);
}
return true;
}
}

View File

@@ -55,7 +55,7 @@ public class FileViewController {
String password = (String) request.getSession().getAttribute("password");
if (fileEntity.passwordHash != null &&
(password == null || !fileService.checkPassword(uuid, password))) {
(password == null || !fileService.checkFilePassword(uuid, password))) {
model.addAttribute("uuid", uuid);
return "file-password";
}
@@ -84,7 +84,7 @@ public class FileViewController {
@PostMapping("/password")
public String checkPassword(String uuid, String password, HttpServletRequest request, Model model) {
if (fileService.checkPassword(uuid, password)) {
if (fileService.checkFilePassword(uuid, password)) {
request.getSession().setAttribute("password", password);
return "redirect:/file/" + uuid;
} else {
@@ -99,7 +99,7 @@ public class FileViewController {
if (fileEntity.passwordHash != null) {
String password = (String) request.getSession().getAttribute("password");
if (password == null || !fileService.checkPassword(fileEntity.uuid, password)) {
if (password == null || !fileService.checkFilePassword(fileEntity.uuid, password)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
}
@@ -159,7 +159,7 @@ public class FileViewController {
if (filePassword != null) {
FileEntity fileEntity = fileService.getFile(fileId);
// Validate file password if the file is password-protected
if (fileEntity.passwordHash != null && !fileService.checkPassword(fileEntity.uuid, filePassword)) {
if (fileEntity.passwordHash != null && !fileService.checkFilePassword(fileEntity.uuid, filePassword)) {
model.addAttribute("uuid", fileEntity.uuid);
return "file-password"; // Redirect to file password page if the password is incorrect
}

View File

@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@@ -76,8 +77,8 @@ public class FileService {
};
}
public void saveChunk(MultipartFile file, String fileName, int chunkNumber) throws IOException {
File chunkFile = new File(tempDir, getFileChunkName(fileName) + chunkNumber);
public void saveFileChunk(MultipartFile file, String fileName, int chunkNumber) throws IOException {
File chunkFile = new File(tempDir, getFileChunkName(fileName, chunkNumber));
try (FileOutputStream fos = new FileOutputStream(chunkFile)) {
fos.write(file.getBytes());
}
@@ -85,24 +86,34 @@ public class FileService {
public FileEntity assembleChunks(String fileName, int totalChunks, FileUploadRequest fileUploadRequest) throws IOException {
File finalFile = new File(tempDir, fileName);
boolean successfullyCreated = finalFile.createNewFile();
if (!successfullyCreated) {
if (!finalFile.createNewFile()) {
throw new IOException("Failed to create new file");
}
try (FileOutputStream fos = new FileOutputStream(finalFile)) {
for (int i = 0; i < totalChunks; i++) {
File partFile = new File(tempDir, getFileChunkName(fileName) + i);
Files.copy(partFile.toPath(), fos);
Files.delete(partFile.toPath());
}
}
mergeChunksIntoFile(finalFile, fileName, totalChunks);
deleteChunkFilesFromTemp(fileName);
return saveFile(finalFile, fileUploadRequest);
}
public void deleteTempFiles(String fileName) {
File[] tempFiles = tempDir.listFiles((dir, name) -> name.startsWith(getFileChunkName(fileName)));
private void mergeChunksIntoFile(File finalFile, String fileName, int totalChunks) throws IOException {
try (FileOutputStream fos = new FileOutputStream(finalFile)) {
for (int i = 0; i < totalChunks; i++) {
File chunk = new File(tempDir, fileName + "_chunk_" + i);
try {
Files.copy(chunk.toPath(), fos);
Files.delete(chunk.toPath());
} catch (IOException ex) {
logger.error("Error processing chunk {}: {}", i, ex.getMessage());
throw ex;
}
}
}
}
public void deleteChunkFilesFromTemp(String fileName) {
File[] tempFiles = tempDir.listFiles((dir, name) -> name.startsWith(fileName + "_chunk_"));
if (tempFiles != null) {
for (File tempFile : tempFiles) {
@@ -115,8 +126,16 @@ public class FileService {
}
}
private String getFileChunkName(String fileName) {
return fileName + "_chunk";
public void deleteFullFileFromTemp(String fileName) {
Path assembledFilePath = Paths.get(tempDir.getAbsolutePath(), fileName);
try {
if (Files.exists(assembledFilePath)) {
Files.delete(assembledFilePath);
logger.info("Deleted assembled file: {}", assembledFilePath);
}
} catch (IOException e) {
logger.error("Failed to delete assembled file: {}", assembledFilePath, e);
}
}
public FileEntity saveFile(File file, FileUploadRequest fileUploadRequest) {
@@ -127,137 +146,40 @@ public class FileService {
logger.info("File received: {}", file.getName());
String uuid = UUID.randomUUID().toString();
Path path = Path.of(applicationSettingsService.getFileStoragePath(), uuid);
Path targetPath = Path.of(applicationSettingsService.getFileStoragePath(), uuid);
if (fileUploadRequest.password == null || fileUploadRequest.password.isEmpty()) {
if (!moveAndRenameUnencryptedFile(file, path)) {
return null;
}
boolean isEncrypted = fileUploadRequest.password != null && !fileUploadRequest.password.isBlank();
if (isEncrypted) {
if (!saveEncryptedFile(targetPath, file, fileUploadRequest)) return null;
} else {
if (!saveEncryptedFile(path, file, fileUploadRequest)) {
return null;
}
}
FileEntity fileEntity = new FileEntity();
fileEntity.name = file.getName();
fileEntity.uuid = uuid;
fileEntity.description = fileUploadRequest.description;
fileEntity.size = file.getTotalSpace();
fileEntity.keepIndefinitely = fileUploadRequest.keepIndefinitely;
fileEntity.hidden = fileUploadRequest.hidden;
if (fileUploadRequest.password != null && !fileUploadRequest.password.isEmpty()) {
fileEntity.passwordHash = passwordEncoder.encode(fileUploadRequest.password);
if (!moveAndRenameUnencryptedFile(file, targetPath)) return null;
}
FileEntity fileEntity = populateFileEntity(file, fileUploadRequest, uuid);
logger.info("FileEntity inserted into database: {}", fileEntity);
return fileRepository.save(fileEntity);
}
private boolean moveAndRenameUnencryptedFile(File file, Path path) {
try {
Files.move(file.toPath(), path);
logger.info("File saved: {}", path);
} catch (
Exception e) {
logger.error("Error saving file: {}", e.getMessage());
return false;
}
return true;
}
public boolean saveEncryptedFile(Path savePath, File file, FileUploadRequest fileUploadRequest) {
try {
Path encryptedFile = Files.createFile(savePath);
logger.info("Encrypting file: {}", encryptedFile);
encryptFile(file, encryptedFile.toFile(), fileUploadRequest.password);
logger.info("Encrypted file saved: {}", encryptedFile);
} catch (
Exception e) {
logger.error("Error encrypting file: {}", e.getMessage());
return false;
}
try {
Files.delete(file.toPath());
logger.info("Temp file deleted: {}", file);
} catch (
Exception e) {
logger.error("Error deleting temp file: {}", e.getMessage());
return false;
}
return true;
}
public List<FileEntity> getFiles() {
return fileRepository.findAll();
}
public ResponseEntity<StreamingResponseBody> downloadFile(Long id, String password, HttpServletRequest request) {
FileEntity fileEntity = fileRepository.findById(id).orElse(null);
if (fileEntity == null) {
logger.info("File not found: {}", id);
return ResponseEntity.notFound().build();
private FileEntity populateFileEntity(File file, FileUploadRequest request, String uuid) {
FileEntity fileEntity = new FileEntity();
fileEntity.name = file.getName();
fileEntity.uuid = uuid;
fileEntity.description = request.description;
fileEntity.size = file.length();
fileEntity.keepIndefinitely = request.keepIndefinitely;
fileEntity.hidden = request.hidden;
if (request.password != null && !request.password.isBlank()) {
fileEntity.passwordHash = passwordEncoder.encode(request.password);
}
Path pathOfFile = Path.of(applicationSettingsService.getFileStoragePath(), fileEntity.uuid);
Path outputFile = null;
if (fileEntity.passwordHash != null) {
try {
outputFile = File.createTempFile("Decrypted", "tmp").toPath();
logger.info("Decrypting file: {}", pathOfFile);
decryptFile(pathOfFile.toFile(), outputFile.toFile(), password);
logger.info("File decrypted: {}", outputFile);
} catch (
Exception e) {
logger.error("Error decrypting file: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
} else {
outputFile = pathOfFile;
}
StreamingResponseBody responseBody = getStreamingResponseBody(outputFile, fileEntity);
try {
Resource resource = new UrlResource(outputFile.toUri());
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()))
.body(responseBody);
} catch (
Exception e) {
logger.error("Error reading file: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
return fileEntity;
}
private void logDownload(FileEntity fileEntity, HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
String realIp = request.getHeader("X-Real-IP");
String downloaderIp;
if (forwardedFor != null && !forwardedFor.isEmpty()) {
// The X-Forwarded-For header can contain multiple IPs, pick the first one
downloaderIp = forwardedFor.split(",")[0].trim();
} else if (realIp != null && !realIp.isEmpty()) {
downloaderIp = realIp;
} else {
downloaderIp = request.getRemoteAddr();
}
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
DownloadLog downloadLog = new DownloadLog(fileEntity, downloaderIp, userAgent);
downloadLogRepository.save(downloadLog);
}
public FileEntity getFile(Long id) {
return fileRepository.findById(id).orElse(null);
}
@@ -301,14 +223,25 @@ public class FileService {
return deleteFileFromFileSystem(fileEntity.uuid);
}
public boolean checkPassword(String uuid, String password) {
Optional<FileEntity> referenceByUUID = fileRepository.findByUUID(uuid);
if (referenceByUUID.isEmpty()) {
return false;
public ResponseEntity<StreamingResponseBody> downloadFile(Long id, String password, HttpServletRequest request) {
FileEntity fileEntity = fileRepository.findById(id).orElse(null);
if (fileEntity == null) {
logger.info("File not found: {}", id);
return ResponseEntity.notFound().build();
}
FileEntity fileEntity = referenceByUUID.get();
return passwordEncoder.matches(password, fileEntity.passwordHash);
Path filePath = Path.of(applicationSettingsService.getFileStoragePath(), fileEntity.uuid);
Path decryptedFilePath = decryptFileIfNeeded(fileEntity, filePath, password);
if (decryptedFilePath == null) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
try {
return createFileDownloadResponse(decryptedFilePath, fileEntity, request);
} catch (Exception e) {
logger.error("Error preparing file download response: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
public List<FileEntity> searchFiles(String query) {
@@ -389,19 +322,14 @@ public class FileService {
return file.tokenExpirationDate == null || !LocalDate.now().isAfter(file.tokenExpirationDate);
}
private void writeFileToStream(String uuid, OutputStream outputStream) {
Path path = Path.of(applicationSettingsService.getFileStoragePath(), uuid);
try (FileInputStream inputStream = new FileInputStream(path.toFile())) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} catch (
Exception e) {
logger.error("Error writing file to stream: {}", e.getMessage());
public boolean checkFilePassword(String uuid, String password) {
Optional<FileEntity> referenceByUUID = fileRepository.findByUUID(uuid);
if (referenceByUUID.isEmpty()) {
return false;
}
FileEntity fileEntity = referenceByUUID.get();
return passwordEncoder.matches(password, fileEntity.passwordHash);
}
public StreamingResponseBody streamFileAndInvalidateToken(String uuid, String token, HttpServletRequest request) {
@@ -428,4 +356,121 @@ public class FileService {
}
};
}
public ResponseEntity<?> finalizeFile(String fileName, int totalChunks, String description,
Boolean keepIndefinitely, String password, Boolean hidden) throws IOException {
FileUploadRequest fileUploadRequest = new FileUploadRequest(description, keepIndefinitely, password, hidden);
FileEntity fileEntity = assembleChunks(fileName, totalChunks, fileUploadRequest);
if (fileEntity != null) {
return ResponseEntity.ok(fileEntity);
}
return ResponseEntity.badRequest().build();
}
private boolean saveEncryptedFile(Path savePath, File file, FileUploadRequest fileUploadRequest) {
try {
Path encryptedFile = Files.createFile(savePath);
logger.info("Encrypting file: {}", encryptedFile);
encryptFile(file, encryptedFile.toFile(), fileUploadRequest.password);
logger.info("Encrypted file saved: {}", encryptedFile);
} catch (
Exception e) {
logger.error("Error encrypting file: {}", e.getMessage());
return false;
}
try {
Files.delete(file.toPath());
logger.info("Temp file deleted: {}", file);
} catch (
Exception e) {
logger.error("Error deleting temp file: {}", e.getMessage());
return false;
}
return true;
}
private boolean moveAndRenameUnencryptedFile(File file, Path path) {
for (int retry = 0; retry < 3; retry++) {
try {
Files.move(file.toPath(), path, StandardCopyOption.REPLACE_EXISTING);
logger.info("File moved successfully: {}", path);
return true;
} catch (IOException e) {
logger.error("Attempt {}/3 failed to move file {}: {}", retry + 1, file.getName(), e.getMessage());
}
}
logger.error("Failed to move file after 3 attempts: {}", file.getName());
return false;
}
private void writeFileToStream(String uuid, OutputStream outputStream) {
Path path = Path.of(applicationSettingsService.getFileStoragePath(), uuid);
try (FileInputStream inputStream = new FileInputStream(path.toFile())) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
} catch (
Exception e) {
logger.error("Error writing file to stream: {}", e.getMessage());
}
}
private void logDownload(FileEntity fileEntity, HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
String realIp = request.getHeader("X-Real-IP");
String downloaderIp;
if (forwardedFor != null && !forwardedFor.isEmpty()) {
// The X-Forwarded-For header can contain multiple IPs, pick the first one
downloaderIp = forwardedFor.split(",")[0].trim();
} else if (realIp != null && !realIp.isEmpty()) {
downloaderIp = realIp;
} else {
downloaderIp = request.getRemoteAddr();
}
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
DownloadLog downloadLog = new DownloadLog(fileEntity, downloaderIp, userAgent);
downloadLogRepository.save(downloadLog);
}
private String getFileChunkName(String fileName, int chunkNumber) {
return fileName + "_chunk_" + chunkNumber;
}
private Path decryptFileIfNeeded(FileEntity fileEntity, Path filePath, String password) {
if (fileEntity.passwordHash == null) {
return filePath;
}
try {
Path tempFile = File.createTempFile("Decrypted", "tmp").toPath();
logger.info("Decrypting file: {}", filePath);
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);
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()))
.body(responseBody);
}
}

View File

@@ -1,89 +1,107 @@
document.getElementById("uploadForm").addEventListener("submit", function (event) {
event.preventDefault();
const file = document.getElementById("file").files[0];
const passwordField = document.getElementById("password");
const isPasswordProtected = passwordField && passwordField.value.trim() !== "";
const uploadForm = event.target;
const fileInput = document.getElementById("file");
const passwordInput = document.getElementById("password");
const uploadIndicator = document.getElementById("uploadIndicator");
const progressBar = document.getElementById("uploadProgress");
const uploadStatus = document.getElementById("uploadStatus");
const CSRF_SELECTOR = 'input[name="_csrf"]';
const chunkSize = 10 * 1024 * 1024; // 10MB chunks
const file = fileInput.files[0];
const chunkSize = 10 * 1024 * 1024; // 10MB
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
// Display the indicator
document.getElementById("uploadIndicator").style.display = "block";
const progressBar = document.getElementById("uploadProgress");
const uploadStatus = document.getElementById("uploadStatus");
progressBar.style.width = "0%";
progressBar.setAttribute("aria-valuenow", 0);
const isPasswordProtected = passwordInput && passwordInput.value.trim() !== "";
const formData = new FormData(event.target);
// Helpers
function initializeProgressBar() {
uploadIndicator.style.display = "block";
progressBar.style.width = "0%";
progressBar.setAttribute("aria-valuenow", 0);
}
function uploadChunk() {
function createChunkFormData(chunk, chunkNumber) {
const formData = new FormData(uploadForm);
formData.append("file", chunk);
formData.append("fileName", file.name);
formData.append("chunkNumber", chunkNumber);
formData.append("totalChunks", totalChunks);
// Only include the password if it exists
if (passwordInput && passwordInput.value.trim() !== "") {
formData.append("password", passwordInput.value.trim());
}
return formData;
}
function handleChunkUploadResponse(xhr, onSuccess, onError) {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (onSuccess) onSuccess(response);
} catch (error) {
console.error("Error parsing JSON:", error);
alert("Unexpected server response. Please try again.");
}
} else {
console.error("Upload error:", xhr.responseText);
alert("Chunk upload failed. Please try again.");
uploadIndicator.style.display = "none";
}
}
function uploadNextChunk() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const chunkFormData = new FormData();
chunkFormData.append("file", chunk);
chunkFormData.append("fileName", file.name);
chunkFormData.append("chunkNumber", currentChunk);
chunkFormData.append("totalChunks", totalChunks);
chunkFormData.append("description", formData.get("description"));
chunkFormData.append("keepIndefinitely", formData.get("keepIndefinitely") || "false");
chunkFormData.append("hidden", formData.get("hidden") || "false");
chunkFormData.append("password", passwordField.value.trim() || ""); // Add password field
const chunkFormData = createChunkFormData(chunk, currentChunk);
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/file/upload-chunk", true);
const csrfTokenElement = document.querySelector('input[name="_csrf"]');
const csrfTokenElement = document.querySelector(CSRF_SELECTOR);
if (csrfTokenElement) {
xhr.setRequestHeader("X-CSRF-TOKEN", csrfTokenElement.value);
}
xhr.onload = function () {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText); // Parse JSON response
handleChunkUploadResponse(xhr, (response) => {
currentChunk++;
const percentComplete = (currentChunk / totalChunks) * 100;
progressBar.style.width = percentComplete + "%";
progressBar.setAttribute("aria-valuenow", percentComplete);
currentChunk++;
const percentComplete = (currentChunk / totalChunks) * 100;
progressBar.style.width = percentComplete + "%";
progressBar.setAttribute("aria-valuenow", percentComplete);
if (currentChunk < totalChunks) {
uploadChunk();
if (currentChunk === totalChunks - 1 && isPasswordProtected) {
uploadStatus.innerText = "Upload complete. Encrypting..."
}
} else {
uploadStatus.innerText = "Upload complete.";
if (response.uuid) {
window.location.href = "/file/" + response.uuid;
} else {
alert("Upload completed but no UUID received.");
}
if (currentChunk < totalChunks) {
uploadNextChunk();
if (currentChunk === totalChunks - 1 && isPasswordProtected) {
uploadStatus.innerText = "Upload complete. Encrypting...";
}
} else {
uploadStatus.innerText = "Upload complete.";
if (response.uuid) {
window.location.href = "/file/" + response.uuid;
} else {
alert("Upload completed but no UUID received.");
}
} catch (error) {
console.error("Error parsing JSON:", error);
alert("Unexpected server response. Please try again.");
}
} else {
console.error("Upload error:", xhr.responseText);
alert("Chunk upload failed. Please try again.");
document.getElementById("uploadIndicator").style.display = "none";
}
});
};
xhr.onerror = function () {
alert("An error occurred during the upload. Please try again.");
document.getElementById("uploadIndicator").style.display = "none";
uploadIndicator.style.display = "none";
};
xhr.send(chunkFormData);
}
uploadChunk();
// Start upload process
initializeProgressBar();
uploadNextChunk();
});
function validateKeepIndefinitely() {

View File

@@ -99,18 +99,6 @@ public class FileServiceTests {
assertNotNull(result.passwordHash);
}
// Successfully encrypts a file
@Test
void test_encrypt_file() {
File file = mock(File.class);
Path encryptedFile = Path.of(fileSavePath, "test.txt");
// Call the method responsible for encrypting files directly
boolean encryptionResult = fileService.saveEncryptedFile(encryptedFile, file, getFileUploadRequest());
assertTrue(encryptionResult);
}
// Correctly encodes password when provided
@Test
void test_correctly_encodes_password_when_provided() {