Spring Boot File Upload and Document Management System
Introduction
File upload is a common requirement in web applications. Users may need to upload profile images, invoices, documents, CSV files, or other media through a REST API.
Spring Boot supports file uploads with HTTP multipart/form-data requests and the MultipartFile interface. A safe implementation validates the file, generates a server-side name, stores it in a controlled location, and returns a useful response to the client.
How file upload works
- The client sends a multipart request containing a file field.
- Spring Boot maps the field to a
MultipartFileparameter. - The application validates the file name, type, size, and content.
- The file is stored using a generated name rather than the original name.
- The API returns an identifier or download URL.
Configure multipart limits
Set upload limits in application.properties:
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
# Directory used by this example
app.upload.directory=uploads
Choose limits according to the application’s needs. Limiting request size helps protect the application from unexpectedly large requests.
Create the upload service
The service creates the upload directory when needed, generates a random storage name, and prevents path traversal by resolving the file inside the configured directory.
package com.example.fileupload.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@Service
public class FileStorageService {
private final Path uploadDirectory;
public FileStorageService(@Value("${app.upload.directory}") String directory) {
this.uploadDirectory = Path.of(directory).toAbsolutePath().normalize();
}
public String store(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("Cannot store an empty file");
}
Files.createDirectories(uploadDirectory);
String originalName = StringUtils.cleanPath(file.getOriginalFilename() == null
? "file"
: file.getOriginalFilename());
String extension = originalName.contains(".")
? originalName.substring(originalName.lastIndexOf('.'))
: "";
String storedName = UUID.randomUUID() + extension.toLowerCase();
Path target = uploadDirectory.resolve(storedName).normalize();
if (!target.getParent().equals(uploadDirectory)) {
throw new IllegalArgumentException("Invalid file path");
}
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
return storedName;
}
public Path load(String fileName) {
Path file = uploadDirectory.resolve(fileName).normalize();
if (!file.getParent().equals(uploadDirectory)) {
throw new IllegalArgumentException("Invalid file path");
}
return file;
}
}
Create the upload controller
The controller accepts the field named file. The client must use the same field name when creating the multipart request.
package com.example.fileupload.controller;
import com.example.fileupload.service.FileStorageService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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 org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
@RestController
@RequestMapping("/files")
public class FileController {
private final FileStorageService storageService;
public FileController(FileStorageService storageService) {
this.storageService = storageService;
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
String storedName = storageService.store(file);
return ResponseEntity.status(HttpStatus.CREATED).body(storedName);
}
@GetMapping("/{fileName}")
public ResponseEntity<byte[]> download(@PathVariable String fileName) throws IOException {
var path = storageService.load(fileName);
if (!Files.exists(path)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(Files.readAllBytes(path));
}
}
Test the upload endpoint
Start the Spring Boot application and send a multipart request with curl:
curl -X POST http://localhost:8080/files/upload \
-F "file=@C:/files/profile.png"
curl -O http://localhost:8080/files/{stored-file-name}
The upload response contains the generated storage name. Use that value with the download endpoint.
Validate uploaded files
Do not trust the original file name or the client-provided content type. Add validation for:
- Maximum file size
- Allowed extensions
- Allowed MIME types
- Actual file content or magic bytes
- Virus and malware scanning where required
For images and documents, inspect the file content rather than relying only on the extension. Never execute uploaded files on the server.
Use external storage in production
Local disk storage is suitable for development and some single-server applications. In a containerized or multi-instance deployment, local files may disappear when an instance is replaced or may not be available to another instance.
For production, consider object storage such as Amazon S3, Azure Blob Storage, or Google Cloud Storage. Store the object key and metadata in the database, and return a controlled download URL.
File upload security best practices
- Generate a random server-side file name and never use the raw client file name as the path.
- Normalize and validate paths to prevent path traversal attacks.
- Restrict file size, extension, MIME type, and content.
- Keep uploaded files outside the application classpath and executable directories.
- Require authentication and authorization for private files.
- Do not expose internal filesystem paths in API responses.
- Apply rate limits and monitor repeated upload failures.
- Use antivirus scanning when the application accepts untrusted documents.
Handle upload errors
Return clear status codes for common failures. For example, use 400 Bad Request for invalid files, 413 Payload Too Large when the request exceeds the configured limit, and 404 Not Found when a requested file does not exist.
Conclusion
Spring Boot makes multipart file uploads simple with MultipartFile, but a production-ready implementation must also validate content, protect paths, enforce size limits, and choose storage appropriate for the deployment environment. Keep file metadata in the database and use secure external storage when applications run across multiple instances.
Build a document management system with Spring Boot
A document management system (DMS) allows authenticated users to upload, download, and manage documents such as PDFs, images, and text files. The application should provide controlled storage, reliable retrieval, authorization, validation, and clear responses for successful and failed operations.
Actors
- User: An authenticated person who can upload and download documents that they are authorized to access.
- Admin: An authorized user who can manage documents and perform administrative operations such as deletion.
Preconditions
- The user is authenticated before uploading or downloading a document.
- The application has sufficient storage and the required directory permissions.
- Allowed file types and maximum file sizes are configured.
- Documents are stored outside executable application directories.
Postconditions
- An accepted document is stored using a generated server-side name.
- An authorized download request receives the requested document.
- An authorized delete request removes the document or returns a clear not-found response.
- Failures are converted into consistent HTTP responses without exposing internal paths.
Document management use cases
Use case 1: Upload a document
Primary actor: User
Trigger: The user selects a document and submits it through the application.
- The client sends a multipart request containing the document.
- The system validates the file size, extension, content type, and file content.
- The system creates the storage directory when necessary.
- The system generates a safe storage name and saves the document.
- The system returns a success response containing a document identifier.
Alternate flows: An invalid type returns 400 Bad Request, an oversized request returns 413 Payload Too Large, and a storage failure returns a controlled server error.
Use case 2: Download a document
Primary actor: User
Trigger: The user requests an authorized document.
- The system verifies the user’s permission to access the document.
- The system resolves the generated document name inside the configured storage directory.
- The system verifies that the document exists and is readable.
- The system streams the document to the user with an appropriate content type.
If the document does not exist, the API returns 404 Not Found. A read failure is logged internally and returned as a safe error response.
Use case 3: Delete a document
Primary actor: Admin
Trigger: An authorized administrator requests document deletion.
- The system verifies the administrator’s permission.
- The system checks that the document exists inside the configured directory.
- The system deletes the document and any associated metadata.
- The system returns a successful deletion response.
Document management controller
The following controller demonstrates upload, download, and delete endpoints. It generates a UUID-based storage name and validates that resolved paths remain inside the upload directory.
package com.javacodex.controller;
import com.javacodex.exception.FileStorageException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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 org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
private final Path uploadDirectory;
public FileUploadController(@Value("${upload-dir}") String uploadDir) {
this.uploadDirectory = Path.of(uploadDir).toAbsolutePath().normalize();
}
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> handleFileUpload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("A file is required");
}
try {
Files.createDirectories(uploadDirectory);
String originalName = file.getOriginalFilename() == null
? "document"
: Path.of(file.getOriginalFilename()).getFileName().toString();
String extension = originalName.contains(".")
? originalName.substring(originalName.lastIndexOf('.')).toLowerCase()
: "";
String storedName = UUID.randomUUID() + extension;
Path target = uploadDirectory.resolve(storedName).normalize();
if (!target.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid storage path");
}
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
log.info("Document stored with id {}", storedName);
return ResponseEntity.status(HttpStatus.CREATED)
.body("Document uploaded successfully: " + storedName);
} catch (IOException exception) {
throw new FileStorageException("Unable to store the document", exception);
}
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
Path filePath = uploadDirectory.resolve(fileName).normalize();
if (!filePath.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid document path");
}
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(filePath);
MediaType mediaType = contentType == null
? MediaType.APPLICATION_OCTET_STREAM
: MediaType.parseMediaType(contentType);
return ResponseEntity.ok()
.contentType(mediaType)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
} catch (IOException exception) {
throw new FileStorageException("Unable to read the document", exception);
}
}
@DeleteMapping("/delete/{fileName:.+}")
public ResponseEntity<String> deleteFile(@PathVariable String fileName) {
try {
Path filePath = uploadDirectory.resolve(fileName).normalize();
if (!filePath.getParent().equals(uploadDirectory)) {
throw new FileStorageException("Invalid document path");
}
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
Files.delete(filePath);
log.info("Document deleted with id {}", fileName);
return ResponseEntity.ok("Document deleted successfully");
} catch (IOException exception) {
throw new FileStorageException("Unable to delete the document", exception);
}
}
}
Custom storage exception
package com.javacodex.exception;
public class FileStorageException extends RuntimeException {
public FileStorageException(String message) {
super(message);
}
public FileStorageException(String message, Throwable cause) {
super(message, cause);
}
}
Global exception handler
A global handler keeps API responses consistent and prevents implementation details such as absolute filesystem paths from being returned to clients.
package com.javacodex.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FileStorageException.class)
public ResponseEntity<String> handleFileStorageException(FileStorageException exception) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("The document operation could not be completed");
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<String> handleMaxUploadSize() {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body("The uploaded document is too large");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException() {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred");
}
}
Configure storage and upload limits
upload-dir=/var/lib/javacodex/documents
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
Use an environment variable or a deployment secret for the production storage path. Ensure the application process has only the minimum directory permissions it needs.
Document management security checklist
- Require authentication for upload and download operations.
- Allow document deletion only for authorized administrators.
- Validate file size, extension, MIME type, and actual content.
- Generate server-side names instead of trusting original filenames.
- Normalize paths and reject path traversal attempts.
- Store files outside executable directories and scan untrusted documents when required.
- Store document ownership and metadata in a database for authorization and auditing.
- Use object storage for highly available, multi-instance deployments.
Conclusion
A Spring Boot document management system should combine multipart upload support with authentication, authorization, validation, safe storage names, path protection, controlled downloads, administrative deletion, and consistent exception handling. This design provides a reliable foundation for managing PDFs, images, text files, and other documents while reducing common file-upload security risks.
Continue learning
