This in-depth guide will walk you through creating a complete file upload and download system using Spring Boot for the backend and Angular for the frontend. We'll cover everything from project setup to advanced features.
Table of Contents
Project Setup
Backend (Spring Boot)
- Create a new Spring Boot project using:
- Spring Initializr
- Or your IDE's project creation wizard
Select:
- Maven or Gradle (we'll use Maven in this guide)
- Java 11 or higher
- Spring Boot 2.7.x or 3.x
- Dependencies: Spring Web, Spring Data JPA (optional for database storage)
Frontend (Angular)
- Install Angular CLI if you haven't:bashCopyDownloadnpm install -g @angular/cli
- Create a new Angular project:bashCopyDownloadng new file-upload-download-ui cd file-upload-download-ui
Spring Boot Backend
Dependencies
Add these to your pom.xml
:
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- File Operations -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- Optional: Database storage -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
Configuration
Create a configuration class to handle file storage settings:
@Configuration
public class FileStorageConfig {
@Value("${file.upload-dir}")
private String uploadDir;
@Bean
public Path fileStorageLocation() {
Path fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
try {
Files.createDirectories(fileStorageLocation);
return fileStorageLocation;
} catch (Exception ex) {
throw new RuntimeException("Could not create upload directory", ex);
}
}
}
Add to application.properties
:
file.upload-dir=uploads
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
File Storage Service
Create a service to handle file operations:
@Service
public class FileStorageService {
private final Path fileStorageLocation;
@Autowired
public FileStorageService(FileStorageConfig fileStorageConfig) {
this.fileStorageLocation = fileStorageConfig.fileStorageLocation();
}
public String storeFile(MultipartFile file) {
// Normalize file name
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
try {
// Check for invalid characters
if(fileName.contains("..")) {
throw new FileStorageException("Invalid file name: " + fileName);
}
// Copy file to target location
Path targetLocation = this.fileStorageLocation.resolve(fileName);
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
return fileName;
} catch (IOException ex) {
throw new FileStorageException("Could not store file " + fileName, ex);
}
}
public Resource loadFileAsResource(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if(resource.exists()) {
return resource;
} else {
throw new FileNotFoundException("File not found: " + fileName);
}
} catch (MalformedURLException | FileNotFoundException ex) {
throw new FileNotFoundException("File not found: " + fileName);
}
}
public List<String> listAllFiles() {
try {
return Files.list(this.fileStorageLocation)
.map(Path::getFileName)
.map(Path::toString)
.collect(Collectors.toList());
} catch (IOException ex) {
throw new RuntimeException("Could not list files", ex);
}
}
public void deleteFile(String fileName) {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Files.deleteIfExists(filePath);
} catch (IOException ex) {
throw new RuntimeException("Could not delete file: " + fileName, ex);
}
}
}
public class FileStorageException extends RuntimeException {
public FileStorageException(String message) {
super(message);
}
public FileStorageException(String message, Throwable cause) {
super(message, cause);
}
}
REST Controller
@RestController
@RequestMapping("/api/files")
public class FileController {
private final FileStorageService fileStorageService;
@Autowired
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@PostMapping("/upload")
public ResponseEntity<FileResponse> uploadFile(@RequestParam("file") MultipartFile file) {
String fileName = fileStorageService.storeFile(file);
FileResponse response = new FileResponse(
fileName,
file.getContentType(),
file.getSize()
);
return ResponseEntity.ok(response);
}
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName,
HttpServletRequest request) {
// Load file as Resource
Resource resource = fileStorageService.loadFileAsResource(fileName);
// Try to determine file's content type
String contentType = null;
try {
contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException ex) {
// Fallback to default content type
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
@GetMapping("/list")
public ResponseEntity<List<String>> listFiles() {
return ResponseEntity.ok(fileStorageService.listAllFiles());
}
@DeleteMapping("/delete/{fileName:.+}")
public ResponseEntity<Void> deleteFile(@PathVariable String fileName) {
fileStorageService.deleteFile(fileName);
return ResponseEntity.noContent().build();
}
}
// Response DTO
public class FileResponse {
private String fileName;
private String fileType;
private long size;
// constructor, getters, setters
}
Error Handling
Create a global exception handler:
@ControllerAdvice
public class FileUploadExceptionAdvice {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ErrorResponse> handleMaxSizeException(
MaxUploadSizeExceededException exc) {
return ResponseEntity
.status(HttpStatus.PAYLOAD_TOO_LARGE)
.body(new ErrorResponse("File too large!"));
}
@ExceptionHandler(FileStorageException.class)
public ResponseEntity<ErrorResponse> handleStorageException(FileStorageException exc) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(exc.getMessage()));
}
@ExceptionHandler(FileNotFoundException.class)
public ResponseEntity<ErrorResponse> handleFileNotFound(FileNotFoundException exc) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(exc.getMessage()));
}
}
public class ErrorResponse {
private String message;
// constructor, getters, setters
}
Security Considerations
If you add Spring Security, configure CORS and CSRF:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/files/**").permitAll()
.anyRequest().authenticated();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Angular Frontend
Angular Project Setup
Install necessary packages:
npm install @angular/material @angular/cdk @angular/animations
ng add @angular/material
File Service
Create a file service to communicate with the backend:
import { Injectable } from '@angular/core';
import { HttpClient, HttpEvent, HttpRequest, HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class FileService {
private baseUrl = 'http://localhost:8080/api/files';
constructor(private http: HttpClient) { }
upload(file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file);
const req = new HttpRequest('POST', `${this.baseUrl}/upload`, formData, {
reportProgress: true,
responseType: 'json'
});
return this.http.request(req);
}
download(fileName: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/download/${fileName}`, {
responseType: 'blob'
});
}
listFiles(): Observable<string[]> {
return this.http.get<string[]>(`${this.baseUrl}/list`);
}
deleteFile(fileName: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/delete/${fileName}`);
}
}
Upload Component
import { Component } from '@angular/core';
import { FileService } from './file.service';
@Component({
selector: 'app-upload',
templateUrl: './upload.component.html',
styleUrls: ['./upload.component.css']
})
export class UploadComponent {
selectedFiles?: FileList;
currentFile?: File;
progress = 0;
message = '';
fileInfos?: Observable<any>;
constructor(private fileService: FileService) { }
selectFile(event: any): void {
this.selectedFiles = event.target.files;
}
upload(): void {
this.progress = 0;
if (this.selectedFiles) {
const file: File | null = this.selectedFiles.item(0);
if (file) {
this.currentFile = file;
this.fileService.upload(this.currentFile).subscribe(
(event: any) => {
if (event.type === HttpEventType.UploadProgress) {
this.progress = Math.round(100 * event.loaded / event.total);
} else if (event instanceof HttpResponse) {
this.message = event.body.message;
this.fileInfos = this.fileService.getFiles();
}
},
(err: any) => {
console.log(err);
this.progress = 0;
if (err.error && err.error.message) {
this.message = err.error.message;
} else {
this.message = 'Could not upload the file!';
}
this.currentFile = undefined;
});
}
this.selectedFiles = undefined;
}
}
}
Upload component HTML:
<div class="upload-container">
<mat-card>
<mat-card-header>
<mat-card-title>File Upload</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="file-upload">
<button mat-raised-button color="primary" (click)="fileInput.click()">
Choose File
</button>
<input #fileInput type="file" (change)="selectFile($event)" style="display:none"/>
<span class="file-name" *ngIf="currentFile">
{{ currentFile.name }}
</span>
</div>
<button mat-raised-button color="accent" [disabled]="!currentFile" (click)="upload()">
Upload
</button>
<div *ngIf="progress > 0" class="progress-container">
<mat-progress-bar mode="determinate" [value]="progress"></mat-progress-bar>
<span>{{ progress }}%</span>
</div>
<div *ngIf="message" class="message">
{{ message }}
</div>
</mat-card-content>
</mat-card>
</div>
Download Component
import { Component, OnInit } from '@angular/core';
import { FileService } from '../file.service';
@Component({
selector: 'app-download',
templateUrl: './download.component.html',
styleUrls: ['./download.component.css']
})
export class DownloadComponent implements OnInit {
fileList: string[] = [];
constructor(private fileService: FileService) { }
ngOnInit(): void {
this.loadFiles();
}
loadFiles(): void {
this.fileService.listFiles().subscribe(
files => {
this.fileList = files;
},
error => {
console.error('Error loading files:', error);
}
);
}
downloadFile(fileName: string): void {
this.fileService.download(fileName).subscribe(
(blob: Blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
error => {
console.error('Error downloading file:', error);
}
);
}
deleteFile(fileName: string): void {
if (confirm(`Are you sure you want to delete ${fileName}?`)) {
this.fileService.deleteFile(fileName).subscribe(
() => {
this.loadFiles(); // Refresh the list
},
error => {
console.error('Error deleting file:', error);
}
);
}
}
}
Download component HTML:
<div class="download-container">
<mat-card>
<mat-card-header>
<mat-card-title>Available Files</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-list>
<mat-list-item *ngFor="let file of fileList">
<span class="file-name">{{ file }}</span>
<button mat-icon-button color="primary" (click)="downloadFile(file)">
<mat-icon>download</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteFile(file)">
<mat-icon>delete</mat-icon>
</button>
</mat-list-item>
</mat-list>
<div *ngIf="fileList.length === 0" class="no-files">
No files available for download.
</div>
</mat-card-content>
</mat-card>
</div>
Progress Tracking
Enhance the upload component with detailed progress tracking:
// Add to upload component
uploadStatus: 'ready' | 'uploading' | 'success' | 'error' = 'ready';
uploadedBytes = 0;
totalBytes = 0;
speed = 0;
timeRemaining = 0;
private calculateSpeedAndTime(startTime: number, loaded: number, total: number) {
const timeElapsed = (Date.now() - startTime) / 1000; // in seconds
this.speed = loaded / timeElapsed; // bytes per second
if (this.speed > 0) {
this.timeRemaining = (total - loaded) / this.speed;
}
}
upload(): void {
if (!this.selectedFiles) return;
const file = this.selectedFiles.item(0);
if (!file) return;
this.currentFile = file;
this.uploadStatus = 'uploading';
this.message = '';
this.progress = 0;
this.totalBytes = file.size;
this.uploadedBytes = 0;
const startTime = Date.now();
this.fileService.upload(this.currentFile).subscribe(
(event: any) => {
if (event.type === HttpEventType.UploadProgress) {
this.uploadedBytes = event.loaded;
this.totalBytes = event.total || this.totalBytes;
this.progress = Math.round(100 * this.uploadedBytes / this.totalBytes);
this.calculateSpeedAndTime(startTime, this.uploadedBytes, this.totalBytes);
} else if (event instanceof HttpResponse) {
this.uploadStatus = 'success';
this.message = 'File uploaded successfully!';
this.loadFiles(); // Refresh file list
}
},
(err: any) => {
this.uploadStatus = 'error';
this.progress = 0;
this.message = err.error?.message || 'Could not upload the file!';
}
);
this.selectedFiles = undefined;
}
Enhanced progress display in HTML:
<div *ngIf="uploadStatus === 'uploading'" class="upload-details">
<div>Progress: {{ progress }}%</div>
<div>Uploaded: {{ uploadedBytes | fileSize }} of {{ totalBytes | fileSize }}</div>
<div>Speed: {{ speed | fileSize }}/s</div>
<div>Time remaining: ~{{ timeRemaining | number:'1.0-0' }}s</div>
</div>
Create a file size pipe:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'fileSize'
})
export class FileSizePipe implements PipeTransform {
transform(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm) + ' ' + sizes[i];
}
}
Advanced Features
1. Multiple File Upload
Modify the backend controller:
@PostMapping("/upload-multiple")
public ResponseEntity<List<FileResponse>> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
List<FileResponse> responses = Arrays.stream(files)
.map(file -> {
String fileName = fileStorageService.storeFile(file);
return new FileResponse(
fileName,
file.getContentType(),
file.getSize()
);
})
.collect(Collectors.toList());
return ResponseEntity.ok(responses);
}
Frontend service:
uploadMultiple(files: FileList): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
Array.from(files).forEach(file => {
formData.append('files', file);
});
const req = new HttpRequest('POST', `${this.baseUrl}/upload-multiple`, formData, {
reportProgress: true,
responseType: 'json'
});
return this.http.request(req);
}
2. File Type Validation
Backend validation:
@PostMapping("/upload")
public ResponseEntity<FileResponse> uploadFile(
@RequestParam("file") @ValidFileType(allowedTypes = {"image/jpeg", "application/pdf"})
MultipartFile file) {
// ...
}
// Custom validation annotation
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidFileTypeValidator.class)
public @interface ValidFileType {
String message() default "Invalid file type";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] allowedTypes() default {};
}
public class ValidFileTypeValidator implements ConstraintValidator<ValidFileType, MultipartFile> {
private String[] allowedTypes;
@Override
public void initialize(ValidFileType constraintAnnotation) {
this.allowedTypes = constraintAnnotation.allowedTypes();
}
@Override
public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
if (file.isEmpty()) return false;
String fileType = file.getContentType();
if (fileType == null) return false;
return Arrays.asList(allowedTypes).contains(fileType);
}
}
Frontend validation:
// In upload component
allowedTypes = ['image/jpeg', 'application/pdf'];
isFileTypeValid(file: File): boolean {
return this.allowedTypes.includes(file.type);
}
upload(): void {
if (!this.selectedFiles) return;
const file = this.selectedFiles.item(0);
if (!file) return;
if (!this.isFileTypeValid(file)) {
this.message = 'Invalid file type. Only JPEG images and PDFs are allowed.';
return;
}
// Proceed with upload
}
3. Database Storage
@Entity
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String fileName;
private String fileType;
private long size;
private LocalDateTime uploadDate;
@Lob
private byte[] data;
// constructors, getters, setters
}
Repository:
public interface FileRepository extends JpaRepository<FileEntity, Long> {
Optional<FileEntity> findByFileName(String fileName);
}
Modified storage service:
@Service
public class DatabaseFileStorageService {
private final FileRepository fileRepository;
@Autowired
public DatabaseFileStorageService(FileRepository fileRepository) {
this.fileRepository = fileRepository;
}
public FileEntity storeFile(MultipartFile file) throws IOException {
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
FileEntity fileEntity = new FileEntity();
fileEntity.setFileName(fileName);
fileEntity.setFileType(file.getContentType());
fileEntity.setSize(file.getSize());
fileEntity.setUploadDate(LocalDateTime.now());
fileEntity.setData(file.getBytes());
return fileRepository.save(fileEntity);
}
public FileEntity getFile(Long fileId) {
return fileRepository.findById(fileId)
.orElseThrow(() -> new FileNotFoundException("File not found with id " + fileId));
}
public Stream<FileEntity> getAllFiles() {
return fileRepository.findAll().stream();
}
}
4. Chunked Uploads for Large Files
Backend controller:
@PostMapping("/upload-chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("originalFileName") String originalFileName) {
// Create a temporary directory for chunks
Path chunkDir = Paths.get("temp-chunks", originalFileName);
try {
Files.createDirectories(chunkDir);
} catch (IOException e) {
throw new RuntimeException("Could not create chunk directory", e);
}
// Save the chunk
Path chunkPath = chunkDir.resolve(String.valueOf(chunkNumber));
try {
file.transferTo(chunkPath);
} catch (IOException e) {
throw new RuntimeException("Could not save chunk", e);
}
// If this was the last chunk, combine all chunks
if (chunkNumber == totalChunks - 1) {
try {
Path outputPath = fileStorageLocation.resolve(originalFileName);
Files.createFile(outputPath);
for (int i = 0; i < totalChunks; i++) {
Path currentChunk = chunkDir.resolve(String.valueOf(i));
Files.write(outputPath, Files.readAllBytes(currentChunk), StandardOpenOption.APPEND);
Files.delete(currentChunk);
}
Files.delete(chunkDir);
} catch (IOException e) {
throw new RuntimeException("Could not combine chunks", e);
}
}
return ResponseEntity.ok("Chunk uploaded successfully");
}
Frontend chunked upload service:
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
uploadInChunks(file: File): Observable<any> {
return new Observable(observer => {
const chunks = Math.ceil(file.size / CHUNK_SIZE);
const fileReader = new FileReader();
let currentChunk = 0;
fileReader.onload = (e) => {
const chunkData = e.target?.result as ArrayBuffer;
const formData = new FormData();
const blob = new Blob([new Uint8Array(chunkData)]);
formData.append('file', blob, file.name);
formData.append('chunkNumber', currentChunk.toString());
formData.append('totalChunks', chunks.toString());
formData.append('originalFileName', file.name);
this.http.post(`${this.baseUrl}/upload-chunk`, formData).subscribe(
() => {
currentChunk++;
if (currentChunk < chunks) {
this.readNextChunk(file, fileReader, currentChunk, CHUNK_SIZE);
} else {
observer.next({ message: 'File uploaded successfully' });
observer.complete();
}
},
err => {
observer.error(err);
}
);
};
this.readNextChunk(file, fileReader, currentChunk, CHUNK_SIZE);
});
}
private readNextChunk(file: File, fileReader: FileReader, chunkNumber: number, chunkSize: number) {
const start = chunkNumber * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const slice = file.slice(start, end);
fileReader.readAsArrayBuffer(slice);
}
Deployment Considerations
Backend
- File Storage:
- For production, use absolute paths for file storage
- Consider cloud storage (AWS S3, Google Cloud Storage, Azure Blob Storage)
- Security:
- Implement proper authentication/authorization
- Validate file names and content
- Scan for viruses/malware
- Performance:
- Add rate limiting
- Consider asynchronous processing for large files
- Configuration:propertiesCopyDownload# Production properties file.upload-dir=/var/uploads spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-request-size=50MB
Frontend
- Environment Configuration:typescriptCopyDownload// environment.prod.ts export const environment = { production: true, apiUrl: 'https://your-api-domain.com/api' };
- Build Optimization:bashCopyDownloadng build --prod
- CORS:
- Ensure your production API has the correct CORS settings
- Or use a proxy in production
- Error Handling:
- Add more robust error handling and user notifications
- Implement retry logic for failed uploads
Conclusion
This comprehensive guide covered building a complete file upload and download system with Spring Boot and Angular, including:
- Basic single file upload/download
- Progress tracking
- Multiple file uploads
- File type validation
- Database storage option
- Chunked uploads for large files
- Deployment considerations
You can extend this further by adding:
- File previews (for images/PDFs)
- Metadata editing
- File sharing functionality
- Advanced search and filtering
- Integration with cloud storage providers
Remember to always implement proper security measures when handling file uploads in production applications.