In modern web applications, handling file uploads and downloads is a core requirement. This article explores how to implement these features using Spring Boot REST controllers, following a “core-to-shell” approach by starting with fundamental Java NIO operations and building a robust solution around them.
The Core Logic: File Storage
The foundation of any file upload system is the ability to move data from an input stream to a physical location on the server. The Files.copy method from the Java NIO package is the most efficient tool for this task.
Handling Streams: to save the file data.
Files.copy(inputStream //Stream data of file
, destination //Path of the destination
, StandardCopyOption.REPLACE_EXISTING); //Copy option
Other kind of storage like S3 buckets can also be used, that will cover in upcoming posts.
Directory Management: Before copying, ensure the destination directory exists by using
Files.createDirectories(destination.getParent())
Implementing the Upload REST Controller
To handle uploads, we use the @PostMapping annotation and the MultipartFile interface, which allows us to intercept the file from an HTTP request.
Basic Upload with Size Validation
It is a best practice to validate file sizes before processing to prevent server strain.
@PostMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) {
// Example: Restricting file size to 5MB programmatically
if(file.getSize() < 1024 * 1024 * 5) {
InputStream inputStream = file.getInputStream();
// logic to save file...
}
}
You can also restrict file sizes globally in your application.properties using:
spring.servlet.multipart.max-file-size=10MB.
Complete Implementation
Combining validation, directory creation, and error handling results in a production-ready method:
@PostMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) {
if(file.getSize() < 1024 * 1024 * 5) {
try (InputStream inputStream = file.getInputStream()) {
Path destinationFile = Paths.get(rootPath).resolve(file.getOriginalFilename()); //rootpath from application.properties
Files.createDirectories(destinationFile.getParent());
Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING);
log.debug("File uploaded successfully");
return ResponseEntity.ok().build();
} catch(IOException ex) {
return ResponseEntity.internalServerError().body("Error uploading");
}
} else {
return ResponseEntity.badRequest().body("File size limit reached: 5MB");
}
}
Strategies for Fetching Files
Once files are stored, they need to be served back to the user. There are two primary strategies for this:
- Direct Path Access: Useful for publicly available locations.
- Binary Stream via REST: Preferred when you need to perform security checks or basic validation before serving the file.
Serving Files as a Resource
To send a file’s binary data back through a REST call, use the UrlResource class.
Resource file = new UrlResource(destination.toUri());
if (!resource.exists() && !resource.isReadable()) {
return ResponseEntity.internalServerError().body("File not found");
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("image/jpeg"))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + file.getFilename() + "\"")
.body(file);
Using Classpath Fallbacks
If a specific user-uploaded file is missing, you can serve a default fallback (like a generic profile picture) from your project’s resource folder:
Resource file = new ClassPathResource("user.jpg");