mirror of
https://github.com/RoastSlav/quickdrop.git
synced 2025-12-30 11:09:59 -06:00
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.
This commit is contained in:
397
README.md
397
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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FileEntity> 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<FileEntity> getFiles() {
|
||||
return fileRepository.findAll();
|
||||
}
|
||||
|
||||
public ResponseEntity<StreamingResponseBody> 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;
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -2,17 +2,27 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Upload File</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/images/favicon.png" rel="icon" type="image/png">
|
||||
<title>
|
||||
Upload
|
||||
File</title>
|
||||
<meta content="width=device-width, initial-scale=1"
|
||||
name="viewport">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet">
|
||||
<link href="/images/favicon.png"
|
||||
rel="icon"
|
||||
type="image/png">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<img alt="Website Logo" class="me-2" height="40" src="/images/favicon.png">
|
||||
<a class="navbar-brand d-flex align-items-center"
|
||||
href="/">
|
||||
<img alt="Website Logo"
|
||||
class="me-2"
|
||||
height="40"
|
||||
src="/images/favicon.png">
|
||||
QuickDrop
|
||||
</a>
|
||||
<button
|
||||
@@ -26,10 +36,13 @@
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<div class="collapse navbar-collapse"
|
||||
id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/file/list">View Files</a>
|
||||
<a class="nav-link"
|
||||
href="/file/list">View
|
||||
Files</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -38,10 +51,33 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container">
|
||||
<h1 class="text-center mb-4">Upload a File</h1>
|
||||
<p class="text-center mb-2">Max file size: 1GB</p>
|
||||
<h1 class="text-center mb-4">
|
||||
Upload
|
||||
a
|
||||
File</h1>
|
||||
<p class="text-center mb-2">
|
||||
Max
|
||||
file
|
||||
size:
|
||||
<span class="maxFileSize"
|
||||
th:text="${maxFileSize}">1GB</span>
|
||||
</p>
|
||||
<p class="text-center mb-4">
|
||||
Files are deleted after 30 days if the option for indefinite upload is not selected
|
||||
Files
|
||||
are
|
||||
deleted
|
||||
after
|
||||
30
|
||||
days
|
||||
if
|
||||
the
|
||||
option
|
||||
for
|
||||
indefinite
|
||||
upload
|
||||
is
|
||||
not
|
||||
selected
|
||||
</p>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
@@ -53,21 +89,28 @@
|
||||
th:action="@{/file/upload}"
|
||||
>
|
||||
<!-- CSRF Token -->
|
||||
<input th:name="${_csrf.parameterName}" th:value="${_csrf.token}" type="hidden"/>
|
||||
<input th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"/>
|
||||
|
||||
<!-- UUID -->
|
||||
<input name="uuid" th:value="${uuid}" type="hidden"/>
|
||||
<input name="uuid"
|
||||
th:value="${uuid}"
|
||||
type="hidden"/>
|
||||
|
||||
<!-- File Input -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="file">Select a file:</label>
|
||||
<label class="form-label"
|
||||
for="file">Select
|
||||
a
|
||||
file:</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="file"
|
||||
name="file"
|
||||
onchange="validateFileSize()"
|
||||
required
|
||||
type="file"
|
||||
onchange="validateFileSize()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -80,13 +123,14 @@
|
||||
size
|
||||
exceeds
|
||||
the
|
||||
1GB
|
||||
<span th:text="${maxFileSize}">1GB</span>
|
||||
limit.
|
||||
</div>
|
||||
|
||||
<!-- Description Input -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="description">Description:</label>
|
||||
<label class="form-label"
|
||||
for="description">Description:</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="description"
|
||||
@@ -103,14 +147,18 @@
|
||||
name="keepIndefinitely"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="keepIndefinitely">
|
||||
Keep indefinitely
|
||||
<label class="form-check-label"
|
||||
for="keepIndefinitely">
|
||||
Keep
|
||||
indefinitely
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Password Input -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password">Password (Optional):</label>
|
||||
<label class="form-label"
|
||||
for="password">Password
|
||||
(Optional):</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="password"
|
||||
@@ -120,15 +168,23 @@
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button class="btn btn-primary w-100" type="submit">Upload</button>
|
||||
<button class="btn btn-primary w-100"
|
||||
type="submit">
|
||||
Upload
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload Indicator -->
|
||||
<div class="mt-3 text-center">
|
||||
<div id="uploadIndicator" style="display: none;">
|
||||
<p class="text-info" id="uploadStatus">Upload started...</p>
|
||||
<div class="progress" style="width: 50%; margin: 0 auto;">
|
||||
<div id="uploadIndicator"
|
||||
style="display: none;">
|
||||
<p class="text-info"
|
||||
id="uploadStatus">
|
||||
Upload
|
||||
started...</p>
|
||||
<div class="progress"
|
||||
style="width: 50%; margin: 0 auto;">
|
||||
<div
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
@@ -143,7 +199,16 @@
|
||||
</div>
|
||||
<div class="container mt-4">
|
||||
<p class="text-center text-muted">
|
||||
Note: All password-protected files are also encrypted for additional security.
|
||||
Note:
|
||||
All
|
||||
password-protected
|
||||
files
|
||||
are
|
||||
also
|
||||
encrypted
|
||||
for
|
||||
additional
|
||||
security.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user