Added the link generation and a file uploaded page. Added a little indicator that the upload has started and alerts if it errors

This commit is contained in:
Rostislav Raykov
2024-10-06 16:23:30 +03:00
parent 733e1988c0
commit 43c13cea33
11 changed files with 133 additions and 19 deletions

View File

@@ -1,12 +1,17 @@
# QuickDrop
QuickDrop is a secure, easy-to-use file sharing application that allows users to upload files without an account, generate download links, and manage file availability, all with built-in malware scanning and optional password protection.
QuickDrop is a secure, easy-to-use file sharing application that allows users to upload files without an account,
generate download links, and manage file availability, all with built-in malware scanning and optional password
protection.
## This project is still under development, and all but the most basic features are still missing.
**Available Features:**
- **File Upload**: Users can upload files without needing to create an account.
- **File list**: Users can view a list of uploaded files.
- **File Download**: Users can download files from the file list.
- **File Upload**: Users can upload files without needing to create an account.
- **File list**: Users can view a list of uploaded files.
- **File Download**: Users can download files from the file list.
- **Download Links**: Generate download links for easy sharing.
## Features

View File

@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@@ -15,6 +16,9 @@ public class SecurityConfig {
http.authorizeHttpRequests(authorizeRequests ->
authorizeRequests.anyRequest().anonymous()
);
http.csrf(csrf ->
csrf.csrfTokenRepository(new CookieCsrfTokenRepository())
);
return http.build();
}

View File

@@ -20,7 +20,8 @@ public class FileRestController {
}
@PostMapping("/upload")
public ResponseEntity<FileEntity> saveFile(@RequestParam("file") MultipartFile file, FileUploadRequest fileUploadRequest) {
public ResponseEntity<FileEntity> saveFile(@RequestParam("file") MultipartFile file,
FileUploadRequest fileUploadRequest) {
FileEntity fileEntity = fileService.saveFile(file, fileUploadRequest);
if (fileEntity != null) {
return ResponseEntity.ok(fileEntity);

View File

@@ -1,5 +1,6 @@
package org.rostislav.quickdrop.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.rostislav.quickdrop.model.FileEntity;
import org.rostislav.quickdrop.model.FileUploadRequest;
import org.rostislav.quickdrop.service.FileService;
@@ -30,16 +31,13 @@ public class FileViewController {
public String saveFile(@RequestParam("file") MultipartFile file,
@RequestParam("description") String description,
@RequestParam(value = "keepIndefinitely", defaultValue = "false") boolean keepIndefinitely,
Model model) {
Model model, HttpServletRequest request) {
FileUploadRequest fileUploadRequest = new FileUploadRequest(description, keepIndefinitely);
FileEntity fileEntity = fileService.saveFile(file, fileUploadRequest);
if (fileEntity != null) {
model.addAttribute("message", "File uploaded successfully");
} else {
model.addAttribute("message", "File upload failed");
}
if (fileEntity != null) {
return filePage(fileEntity.uuid, model, request);
}
return "upload";
}
@@ -50,6 +48,16 @@ public class FileViewController {
return "listFiles";
}
@GetMapping("/{UUID}")
public String filePage(@PathVariable String UUID, Model model, HttpServletRequest request) {
FileEntity file = fileService.getFile(UUID);
model.addAttribute("file", file);
String downloadLink = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/file/" + file.uuid;
model.addAttribute("downloadLink", downloadLink);
return "fileUploaded";
}
@GetMapping("/download/{id}")
public ResponseEntity<Resource> downloadFile(@PathVariable Long id) {
return fileService.downloadFile(id);

View File

@@ -11,7 +11,7 @@ public class FileEntity {
public Long id;
public String name;
public String UUID;
public String uuid;
public String description;
public long size;
public boolean keepIndefinitely;

View File

@@ -2,7 +2,12 @@ package org.rostislav.quickdrop.repository;
import org.rostislav.quickdrop.model.FileEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface FileRepository extends JpaRepository<FileEntity, Long> {
@Query("SELECT f FROM FileEntity f WHERE f.uuid = :uuid")
public Optional<FileEntity> findByUUID(@Param("uuid") String uuid);
}

View File

@@ -3,6 +3,8 @@ package org.rostislav.quickdrop.service;
import org.rostislav.quickdrop.model.FileEntity;
import org.rostislav.quickdrop.model.FileUploadRequest;
import org.rostislav.quickdrop.repository.FileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
@@ -20,6 +22,7 @@ import java.util.UUID;
@Service
public class FileService {
private static final Logger logger = LoggerFactory.getLogger(FileService.class);
private final FileRepository fileRepository;
@Value("${file.save.path}")
private String fileSavePath;
@@ -34,17 +37,19 @@ public class FileService {
try {
Files.createFile(path);
Files.write(path, file.getBytes());
logger.info("File saved: {}", path);
} catch (Exception e) {
return null;
}
FileEntity fileEntity = new FileEntity();
fileEntity.name = file.getOriginalFilename();
fileEntity.UUID = uuid;
fileEntity.uuid = uuid;
fileEntity.description = fileUploadRequest.description;
fileEntity.size = file.getSize();
fileEntity.keepIndefinitely = fileUploadRequest.keepIndefinitely;
logger.info("FileEntity saved: {}", fileEntity);
return fileRepository.save(fileEntity);
}
@@ -62,7 +67,7 @@ public class FileService {
return ResponseEntity.notFound().build();
}
Path path = Path.of(fileSavePath, referenceById.get().UUID);
Path path = Path.of(fileSavePath, referenceById.get().uuid);
try {
Resource resource = new UrlResource(path.toUri());
return ResponseEntity.ok()
@@ -76,4 +81,8 @@ public class FileService {
public FileEntity getFile(Long id) {
return fileRepository.findById(id).orElse(null);
}
public FileEntity getFile(String uuid) {
return fileRepository.findByUUID(uuid).orElse(null);
}
}

View File

@@ -9,4 +9,5 @@ spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
server.tomcat.connection-timeout=60000
file.save.path=/files

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File has been uploaded</title>
</head>
<body>
<h1>File has been uploaded</h1>
<div>
<h3>File Name</h3>
<p th:text="${file.name}"></p>
<h3>File description</h3>
<p th:text="${file.description}"></p>
<h3>Uploaded At</h3>
<p th:text="${file.uploadDate}"></p>
<h3>Keep Indefinitely</h3>
<p th:text="${file.keepIndefinitely} ? 'Yes' : 'No'"></p>
<h3>File Size</h3>
<p th:text="${file.size}"></p>
<h3>Link: </h3>
<p th:text="${downloadLink}"></p>
<h3>Download</h3>
<p>
<a th:href="@{/file/download/{id}(id=${file.id})}">Download</a>
</p>
</div>
<p>
<a href="/file/list">View all files</a>
</p>
</body>
</html>

View File

@@ -23,7 +23,7 @@
<td th:text="${file.uploadDate}"></td>
<td th:text="${file.keepIndefinitely} ? 'Yes' : 'No'"></td>
<td>
<a th:href="@{/file/download/{id}(id=${file.id})}">Download</a>
<a th:href="@{/file/{UUID}(UUID=${file.UUID})}">Go to file page</a>
</td>
</tr>
</tbody>

View File

@@ -6,7 +6,7 @@
</head>
<body>
<h1>Upload a File</h1>
<form enctype="multipart/form-data" method="post" th:action="@{/file/upload}">
<form enctype="multipart/form-data" id="uploadForm" method="post" th:action="@{/file/upload}">
<input th:name="${_csrf.parameterName}" th:value="${_csrf.token}" type="hidden"/>
<div>
<label for="file">Select a file:</label>
@@ -24,5 +24,55 @@
<button type="submit">Upload</button>
</div>
</form>
<div id="uploadIndicator" style="display: none;">Upload started...</div>
</body>
</html>
</html>
<script>
document.getElementById("uploadForm").addEventListener("submit", function (event) {
event.preventDefault(); // Prevent the form from submitting synchronously
// Display the indicator
document.getElementById("uploadIndicator").style.display = "block";
// Prepare form data
const formData = new FormData(event.target);
// Create an AJAX request
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/file/upload", true); // Updated to use the new API endpoint
// Add CSRF token if required
const csrfTokenElement = document.querySelector('input[name="_csrf"]');
if (csrfTokenElement) {
xhr.setRequestHeader("X-CSRF-TOKEN", csrfTokenElement.value);
}
// Handle response
xhr.onload = function () {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.uuid) {
// Redirect to the view page using the UUID from the JSON response
window.location.href = "/file/" + response.uuid;
} else {
alert("Unexpected response. Please try again.");
document.getElementById("uploadIndicator").style.display = "none";
}
} else {
alert("Upload failed. Please try again.");
document.getElementById("uploadIndicator").style.display = "none";
}
};
// Handle network or server errors
xhr.onerror = function () {
alert("An error occurred during the upload. Please try again.");
document.getElementById("uploadIndicator").style.display = "none";
};
// Send the form data
xhr.send(formData);
});
</script>