Added a client side check for the file size and now deletes the temp decrypted file when downloading

This commit is contained in:
Rostislav Raykov
2024-10-25 22:45:00 +03:00
parent b283672e72
commit f0d4c43a1f
5 changed files with 94 additions and 33 deletions

View File

@@ -1,9 +1,10 @@
package org.rostislav.quickdrop.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.rostislav.quickdrop.model.FileEntity;
import org.rostislav.quickdrop.service.FileService;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
@@ -12,8 +13,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import static org.rostislav.quickdrop.util.FileUtils.populateModelAttributes;
@@ -66,7 +66,7 @@ public class FileViewController {
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable Long id, HttpServletRequest request) {
public ResponseEntity<StreamingResponseBody> downloadFile(@PathVariable Long id, HttpServletRequest request) {
FileEntity fileEntity = fileService.getFile(id);
if (fileEntity.passwordHash != null) {

View File

@@ -1,5 +1,16 @@
package org.rostislav.quickdrop.service;
import java.io.File;
import java.io.FileInputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.rostislav.quickdrop.model.FileEntity;
import org.rostislav.quickdrop.model.FileUploadRequest;
import org.rostislav.quickdrop.repository.FileRepository;
@@ -14,16 +25,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import static org.rostislav.quickdrop.util.DataValidator.validateObjects;
import static org.rostislav.quickdrop.util.FileEncryptionUtils.decryptFile;
@@ -42,6 +44,41 @@ public class FileService {
this.passwordEncoder = passwordEncoder;
}
private static StreamingResponseBody getStreamingResponseBody(Path outputFile, FileEntity fileEntity) {
return outputStream -> {
try (FileInputStream inputStream = new FileInputStream(outputFile.toFile())) {
byte[] buffer = new byte[1024];
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());
}
}
}
};
}
private boolean saveUnencryptedFile(MultipartFile file, Path path) {
try {
Files.createFile(path);
Files.write(path, file.getBytes());
logger.info("File saved: {}", path);
} catch (Exception e) {
logger.error("Error saving file: {}", e.getMessage());
return false;
}
return true;
}
public FileEntity saveFile(MultipartFile file, FileUploadRequest fileUploadRequest) {
if (!validateObjects(file, fileUploadRequest)) {
return null;
@@ -73,20 +110,12 @@ public class FileService {
fileEntity.passwordHash = passwordEncoder.encode(fileUploadRequest.password);
}
logger.info("FileEntity saved: {}", fileEntity);
logger.info("FileEntity inserted into database: {}", fileEntity);
return fileRepository.save(fileEntity);
}
private boolean saveUnencryptedFile(MultipartFile file, Path path) {
try {
Files.createFile(path);
Files.write(path, file.getBytes());
logger.info("File saved: {}", path);
} catch (Exception e) {
logger.error("Error saving file: {}", e.getMessage());
return false;
}
return true;
public List<FileEntity> getFiles() {
return fileRepository.findAll();
}
public boolean saveEncryptedFile(Path savePath, MultipartFile file, FileUploadRequest fileUploadRequest) {
@@ -102,7 +131,9 @@ public class FileService {
try {
Path encryptedFile = Files.createFile(savePath);
logger.info("Encrypting file: {}", encryptedFile);
encryptFile(tempFile.toFile(), encryptedFile.toFile(), fileUploadRequest.password);
logger.info("Encrypted file saved: {}", encryptedFile);
} catch (Exception e) {
logger.error("Error encrypting file: {}", e.getMessage());
return false;
@@ -110,6 +141,7 @@ public class FileService {
try {
Files.delete(tempFile);
logger.info("Temp file deleted: {}", tempFile);
} catch (Exception e) {
logger.error("Error deleting temp file: {}", e.getMessage());
return false;
@@ -118,13 +150,10 @@ public class FileService {
return true;
}
public List<FileEntity> getFiles() {
return fileRepository.findAll();
}
public ResponseEntity<Resource> downloadFile(Long id, String password) {
public ResponseEntity<StreamingResponseBody> downloadFile(Long id, String password) {
FileEntity fileEntity = fileRepository.findById(id).orElse(null);
if (fileEntity == null) {
logger.info("File not found: {}", id);
return ResponseEntity.notFound().build();
}
@@ -133,7 +162,9 @@ public class FileService {
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();
@@ -142,13 +173,16 @@ public class FileService {
outputFile = pathOfFile;
}
StreamingResponseBody responseBody = getStreamingResponseBody(outputFile, fileEntity);
try {
Resource resource = new UrlResource(outputFile.toUri());
logger.info("Sending file: {}", fileEntity);
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(resource);
.body(responseBody);
} catch (Exception e) {
logger.error("Error reading file: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

View File

@@ -8,8 +8,8 @@ spring.jpa.hibernate.ddl-auto=update
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
spring.servlet.multipart.max-file-size=1025MB
spring.servlet.multipart.max-request-size=1025MB
server.tomcat.connection-timeout=60000
file.save.path=files
file.max.age=30

View File

@@ -67,4 +67,17 @@ function isPasswordProtected() {
const passwordField = document.getElementById("password");
return passwordField && passwordField.value.trim() !== "";
}
function validateFileSize() {
const file = document.getElementById('file').files[0];
const maxSize = 1024 * 1024 * 1024; // 1GB
const fileSizeAlert = document.getElementById('fileSizeAlert');
if (file.size > maxSize) {
fileSizeAlert.style.display = 'block';
document.getElementById('file').value = '';
} else {
fileSizeAlert.style.display = 'none';
}
}

View File

@@ -67,9 +67,23 @@
name="file"
required
type="file"
onchange="validateFileSize()"
/>
</div>
<!-- File Size Alert -->
<div class="alert alert-danger"
id="fileSizeAlert"
role="alert"
style="display: none;">
File
size
exceeds
the
1GB
limit.
</div>
<!-- Description Input -->
<div class="mb-3">
<label class="form-label" for="description">Description:</label>