From 4b8dd406ddb5ec87999aff355d581263d467e54c Mon Sep 17 00:00:00 2001 From: Rostislav Raykov Date: Fri, 25 Oct 2024 23:41:17 +0300 Subject: [PATCH] Moved to a custom property for the file size which is now also reflected on the front-end. Also moved to using .transferTo on the multipart to reduce memory usage. --- README.md | 397 ++++++++++++++++-- .../quickdrop/config/MultipartConfig.java | 28 ++ .../controller/FileViewController.java | 6 +- .../quickdrop/service/FileService.java | 55 +-- src/main/resources/application.properties | 3 +- src/main/resources/static/js/upload.js | 24 +- src/main/resources/templates/upload.html | 115 +++-- 7 files changed, 535 insertions(+), 93 deletions(-) create mode 100644 src/main/java/org/rostislav/quickdrop/config/MultipartConfig.java diff --git a/README.md b/README.md index dab514d..4310dfa 100644 --- a/README.md +++ b/README.md @@ -4,52 +4,282 @@ # QuickDrop -QuickDrop is an easy-to-use file sharing application that allows users to upload files without an account, -generate download links, and manage file availability, file encryption and optional password +QuickDrop +is +an +easy-to-use +file +sharing +application +that +allows +users +to +upload +files +without +an +account, +generate +download +links, +and +manage +file +availability, +file +encryption +and +optional +password protection. - ## Features -- **File Upload**: Users can upload files without needing to create an account. -- **Download Links**: Generate download links for easy sharing. -- **File Management**: Manage file availability with options to keep files indefinitely or delete them. -- **Password Protection**: Optionally protect files with a password. -- **File Encryption**: Encrypt files to ensure privacy. -- **Whole app password protection**: Optionally protect the entire app with a password. +- + +* + +* +File +Upload +**: +Users +can +upload +files +without +needing +to +create +an +account. + +- + +* + +* +Download +Links +**: +Generate +download +links +for +easy +sharing. + +- + +* + +* +File +Management +**: +Manage +file +availability +with +options +to +keep +files +indefinitely +or +delete +them. + +- + +* + +* +Password +Protection +**: +Optionally +protect +files +with +a +password. + +- + +* + +* +File +Encryption +**: +Encrypt +files +to +ensure +privacy. + +- + +* + +* +Whole +app +password +protection +**: +Optionally +protect +the +entire +app +with +a +password. ## Technologies Used -- **Java** -- **SQLite** -- **Spring Framework** -- **Spring Security** -- **Spring Data JPA (Hibernate)** -- **Spring Web** -- **Spring Boot** -- **Thymeleaf** -- **Bootstrap** -- **Maven** +- + +* + +* +Java +** + +- + +* + +* +SQLite +** + +- + +* + +* +Spring +Framework +** + +- + +* + +* +Spring +Security +** + +- + +* + +* +Spring +Data +JPA ( +Hibernate) +** + +- + +* + +* +Spring +Web +** + +- + +* + +* +Spring +Boot +** + +- + +* + +* +Thymeleaf +** + +- + +* + +* +Bootstrap +** + +- + +* + +* +Maven +** ## Getting Started ### Installation -**Installation with Docker** +* -1. Pull the Docker image: +* +Installation +with +Docker +** + +1. + +Pull +the +Docker +image: ``` docker pull roastslav/quickdrop:latest ``` -2. Run the Docker container: +2. + +Run +the +Docker +container: ``` docker run -d -p 8080:8080 roastslav/quickdrop:latest ``` -Optional: Use a volume to persist the uploaded files or if you want to change the default configuration: +Optional: +Use +a +volume +to +persist +the +uploaded +files +or +if +you +want +to +change +the +default +configuration: ``` docker run -d -p 8080:8080 \ @@ -59,51 +289,125 @@ docker run -d -p 8080:8080 \ quickdrop ``` -**Installation without Docker** +* + +* +Installation +without +Docker +** Prerequisites -- Java 21 or higher -- Maven -- SQLite -1. Clone the repository: +- + +Java +21 +or +higher + +- + +Maven +- +SQLite + +1. + +Clone +the +repository: ``` git clone https://github.com/RoastSlav/quickdrop.git cd quickdrop ``` -2. Build the application: +2. + +Build +the +application: ``` mvn clean package ``` -3. Run the application: +3. + +Run +the +application: ``` java -jar target/quickdrop-0.0.1-SNAPSHOT.jar ``` -4. Using an external application.properties file: - - Create an **application.properties** file in the same directory as the JAR file or specify its location in the - start command. +4. - - Add your custom settings, for example (Listed below are the default values): +Using +an +external +application.properties +file: +- +Create +an +* +* +application.properties +** +file +in +the +same +directory +as +the +JAR +file +or +specify +its +location +in +the +start +command. + + - + Add + your + custom + settings, + for + example ( + Listed + below + are + the + default + values): ``` -spring.servlet.multipart.max-file-size=1024MB -spring.servlet.multipart.max-request-size=1024MB -server.tomcat.connection-timeout=60000 file.save.path=files -file.max.age=30 (In days) +file.max.age=30 logging.file.name=log/quickdrop.log file.deletion.cron=0 0 2 * * * app.basic.password=test app.enable.password=false +max-upload-file-size=1GB ``` -- Run the application with the external configuration: +- + +Run +the +application +with +the +external +configuration: ``` java -jar target/quickdrop-0.0.1-SNAPSHOT.jar --spring.config.location=./application.properties @@ -111,4 +415,17 @@ java -jar target/quickdrop-0.0.1-SNAPSHOT.jar --spring.config.location=./applica ## License -This project is licensed under the MIT License. See the `LICENSE` file for details. +This +project +is +licensed +under +the +MIT +License. +See +the +`LICENSE` +file +for +details. diff --git a/src/main/java/org/rostislav/quickdrop/config/MultipartConfig.java b/src/main/java/org/rostislav/quickdrop/config/MultipartConfig.java new file mode 100644 index 0000000..6a61137 --- /dev/null +++ b/src/main/java/org/rostislav/quickdrop/config/MultipartConfig.java @@ -0,0 +1,28 @@ +package org.rostislav.quickdrop.config; + +import jakarta.servlet.MultipartConfigElement; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.MultipartConfigFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; + +@Configuration +public class MultipartConfig { + private final long ADDITIONAL_REQUEST_SIZE = 1024L * 1024L * 10L; // 10 MB + @Value("${max-upload-file-size}") + private String maxUploadFileSize; + + @Bean + public MultipartConfigElement multipartConfigElement() { + MultipartConfigFactory factory = new MultipartConfigFactory(); + + factory.setMaxFileSize(DataSize.parse(maxUploadFileSize)); + + DataSize maxRequestSize = DataSize.parse(maxUploadFileSize); + maxRequestSize = DataSize.ofBytes(maxRequestSize.toBytes() + ADDITIONAL_REQUEST_SIZE); + factory.setMaxRequestSize(maxRequestSize); + + return factory.createMultipartConfig(); + } +} diff --git a/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java b/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java index 955409c..0b1f863 100644 --- a/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java +++ b/src/main/java/org/rostislav/quickdrop/controller/FileViewController.java @@ -5,6 +5,7 @@ import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.rostislav.quickdrop.model.FileEntity; import org.rostislav.quickdrop.service.FileService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -21,13 +22,16 @@ import static org.rostislav.quickdrop.util.FileUtils.populateModelAttributes; @RequestMapping("/file") public class FileViewController { private final FileService fileService; + @Value("${max-upload-file-size}") + private String maxFileSize; public FileViewController(FileService fileService) { this.fileService = fileService; } @GetMapping("/upload") - public String showUploadFile() { + public String showUploadFile(Model model) { + model.addAttribute("maxFileSize", maxFileSize); return "upload"; } diff --git a/src/main/java/org/rostislav/quickdrop/service/FileService.java b/src/main/java/org/rostislav/quickdrop/service/FileService.java index aa771cb..b6c48d9 100644 --- a/src/main/java/org/rostislav/quickdrop/service/FileService.java +++ b/src/main/java/org/rostislav/quickdrop/service/FileService.java @@ -67,18 +67,6 @@ public class FileService { }; } - 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; @@ -114,17 +102,27 @@ public class FileService { return fileRepository.save(fileEntity); } - public List getFiles() { - return fileRepository.findAll(); + private boolean saveUnencryptedFile(MultipartFile file, Path path) { + try { + Files.createFile(path); + file.transferTo(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, MultipartFile file, FileUploadRequest fileUploadRequest) { Path tempFile; try { tempFile = Files.createTempFile("Unencrypted", "tmp"); - Files.write(tempFile, file.getBytes()); + file.transferTo(tempFile); logger.info("Unencrypted temp file saved: {}", tempFile); - } catch (Exception e) { + } catch ( + Exception e) { logger.error("Error saving unencrypted temp file: {}", e.getMessage()); return false; } @@ -134,7 +132,8 @@ public class FileService { logger.info("Encrypting file: {}", encryptedFile); encryptFile(tempFile.toFile(), encryptedFile.toFile(), fileUploadRequest.password); logger.info("Encrypted file saved: {}", encryptedFile); - } catch (Exception e) { + } catch ( + Exception e) { logger.error("Error encrypting file: {}", e.getMessage()); return false; } @@ -142,7 +141,8 @@ public class FileService { try { Files.delete(tempFile); logger.info("Temp file deleted: {}", tempFile); - } catch (Exception e) { + } catch ( + Exception e) { logger.error("Error deleting temp file: {}", e.getMessage()); return false; } @@ -150,6 +150,10 @@ public class FileService { return true; } + public List getFiles() { + return fileRepository.findAll(); + } + public ResponseEntity downloadFile(Long id, String password) { FileEntity fileEntity = fileRepository.findById(id).orElse(null); if (fileEntity == null) { @@ -165,7 +169,8 @@ public class FileService { logger.info("Decrypting file: {}", pathOfFile); decryptFile(pathOfFile.toFile(), outputFile.toFile(), password); logger.info("File decrypted: {}", outputFile); - } catch (Exception e) { + } catch ( + Exception e) { logger.error("Error decrypting file: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } @@ -179,11 +184,12 @@ public class FileService { 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())) + .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) { + } catch ( + Exception e) { logger.error("Error reading file: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } @@ -213,7 +219,8 @@ public class FileService { try { Files.delete(path); logger.info("File deleted: {}", path); - } catch (Exception e) { + } catch ( + Exception e) { return false; } return true; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ebb6105..ade4f5b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,8 +8,6 @@ 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=1025MB -spring.servlet.multipart.max-request-size=1025MB server.tomcat.connection-timeout=60000 file.save.path=files file.max.age=30 @@ -17,5 +15,6 @@ logging.file.name=log/quickdrop.log file.deletion.cron=0 0 2 * * * app.basic.password=test app.enable.password=false +max-upload-file-size=1GB #logging.level.org.springframework=DEBUG #logging.level.org.hibernate=DEBUG \ No newline at end of file diff --git a/src/main/resources/static/js/upload.js b/src/main/resources/static/js/upload.js index 59dbe58..7d8b793 100644 --- a/src/main/resources/static/js/upload.js +++ b/src/main/resources/static/js/upload.js @@ -70,8 +70,9 @@ function isPasswordProtected() { } function validateFileSize() { + const maxFileSize = document.getElementsByClassName('maxFileSize')[0].innerText; const file = document.getElementById('file').files[0]; - const maxSize = 1024 * 1024 * 1024; // 1GB + const maxSize = parseSize(maxFileSize); const fileSizeAlert = document.getElementById('fileSizeAlert'); if (file.size > maxSize) { @@ -80,4 +81,25 @@ function validateFileSize() { } else { fileSizeAlert.style.display = 'none'; } +} + +function parseSize(size) { + const units = { + B: 1, + KB: 1024, + MB: 1024 * 1024, + GB: 1024 * 1024 * 1024 + }; + + const unitMatch = size.match(/[a-zA-Z]+/); + const valueMatch = size.match(/[0-9.]+/); + + if (!unitMatch || !valueMatch) { + throw new Error("Invalid size format"); + } + + const unit = unitMatch[0]; + const value = parseFloat(valueMatch[0]); + + return value * (units[unit] || 1); } \ No newline at end of file diff --git a/src/main/resources/templates/upload.html b/src/main/resources/templates/upload.html index ab07dc5..0404eed 100644 --- a/src/main/resources/templates/upload.html +++ b/src/main/resources/templates/upload.html @@ -2,17 +2,27 @@ - Upload File - - - + + Upload + File + + +