[Java Backend Zero to Hello] BÀI 6.8: FILE UPLOAD/DOWNLOAD
📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 6: REST API & Best Practices | Bài 67/86
BÀI 6.8: FILE UPLOAD/DOWNLOAD
Mục tiêu
- Upload file với Spring Boot
- Download file
- Lưu trữ local và cloud (S3)
- Validate file
1. UPLOAD FILE CƠ BẢN
1.1 Controller
@RestController
@RequestMapping("/api/files")
public class FileController {
@PostMapping("/upload")
public ResponseEntity<FileResponse> upload(@RequestParam("file") MultipartFile file) {
// Xử lý upload
return ResponseEntity.ok(new FileResponse(file.getOriginalFilename()));
}
}
1.2 Cấu hình
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
file-size-threshold: 1MB
1.3 MultipartFile API
String originalName = file.getOriginalFilename();
String contentType = file.getContentType();
long size = file.getSize();
byte[] bytes = file.getBytes();
InputStream is = file.getInputStream();
boolean isEmpty = file.isEmpty();
2. LƯU TRỮ LOCAL
2.1 Service
@Service
public class FileStorageService {
@Value("${app.upload.dir}")
private String uploadDir;
public String save(MultipartFile file) throws IOException {
// Validate
validateFile(file);
// Tạo thư mục nếu chưa có
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// Tạo tên file unique
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
Path filePath = uploadPath.resolve(fileName);
// Lưu file
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return fileName;
}
public Resource load(String fileName) throws MalformedURLException {
Path filePath = Paths.get(uploadDir).resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
throw new NotFoundException("File not found: " + fileName);
}
return resource;
}
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("File is empty");
}
if (file.getSize() > 10 * 1024 * 1024) {
throw new IllegalArgumentException("File too large");
}
// Validate content type
String contentType = file.getContentType();
if (!isAllowedType(contentType)) {
throw new IllegalArgumentException("Invalid file type");
}
}
private boolean isAllowedType(String contentType) {
return contentType != null && (
contentType.equals("image/jpeg") ||
contentType.equals("image/png") ||
contentType.equals("application/pdf")
);
}
}
2.2 Controller
@PostMapping("/upload")
public FileResponse upload(@RequestParam("file") MultipartFile file) throws IOException {
String fileName = fileStorageService.save(file);
return new FileResponse(fileName);
}
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> download(@PathVariable String fileName) throws Exception {
Resource resource = fileStorageService.load(fileName);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
3. UPLOAD NHIỀU FILE
@PostMapping("/upload-multiple")
public List<FileResponse> uploadMultiple(@RequestParam("files") MultipartFile[] files) {
return Arrays.stream(files)
.map(file -> {
try {
String fileName = fileStorageService.save(file);
return new FileResponse(fileName);
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.toList();
}
4. UPLOAD VỚI METADATA
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public FileResponse upload(
@RequestPart("file") MultipartFile file,
@RequestPart("metadata") FileMetadata metadata) {
// ...
}
public record FileMetadata(
String description,
String category,
List<String> tags
) {}
5. LƯU TRỮ AWS S3
5.1 Dependency
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
5.2 Cấu hình
aws:
s3:
bucket-name: my-bucket
region: ap-southeast-1
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
5.3 S3 Service
@Service
public class S3Service {
private final S3Client s3Client;
private final String bucketName;
public String upload(MultipartFile file) throws IOException {
String key = UUID.randomUUID() + "_" + file.getOriginalFilename();
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.contentType(file.getContentType())
.contentLength(file.getSize())
.build();
s3Client.putObject(request, RequestBody.fromInputStream(
file.getInputStream(), file.getSize()
));
return key;
}
public byte[] download(String key) {
GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
return s3Client.getObjectAsBytes(request).asByteArray();
}
public void delete(String key) {
DeleteObjectRequest request = DeleteObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.deleteObject(request);
}
public String getUrl(String key) {
GetUrlRequest request = GetUrlRequest.builder()
.bucket(bucketName)
.key(key)
.build();
return s3Client.utilities().getUrl(request).toString();
}
}
6. STREAMING FILE LỚN
@GetMapping("/stream/{fileName}")
public ResponseEntity<StreamingResponseBody> stream(@PathVariable String fileName) {
Path path = Paths.get(uploadDir).resolve(fileName);
StreamingResponseBody body = outputStream -> {
try (InputStream is = new FileInputStream(path.toFile())) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(body);
}
7. VALIDATION
7.1 Custom Validator
public class FileValidator {
private static final long MAX_SIZE = 10 * 1024 * 1024; // 10MB
private static final Set<String> ALLOWED_TYPES = Set.of(
"image/jpeg", "image/png", "image/gif", "application/pdf"
);
public static void validate(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File is required");
}
if (file.getSize() > MAX_SIZE) {
throw new IllegalArgumentException("File size exceeds limit");
}
if (!ALLOWED_TYPES.contains(file.getContentType())) {
throw new IllegalArgumentException("File type not allowed");
}
// Validate filename
String filename = file.getOriginalFilename();
if (filename != null && filename.contains("..")) {
throw new IllegalArgumentException("Invalid filename");
}
}
}
7.2 Virus Scan (giả lập)
public class VirusScanner {
public void scan(MultipartFile file) {
// Tích hợp với ClamAV hoặc service khác
if (isInfected(file)) {
throw new VirusDetectedException("File contains virus");
}
}
}
8. IMAGE PROCESSING
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.20</version>
</dependency>
public byte[] createThumbnail(MultipartFile file, int width, int height) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Thumbnails.of(file.getInputStream())
.size(width, height)
.toOutputStream(os);
return os.toByteArray();
}
9. BẢO MẬT
9.1 Không lộ đường dẫn thật
// Lưu với tên UUID, không dùng tên gốc
String fileName = UUID.randomUUID().toString() + extension;
9.2 Kiểm tra quyền
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/upload")
public FileResponse upload(...) { ... }
9.3 Giới hạn rate
// Dùng Bucket4j hoặc Resilience4j
@RateLimiter(name = "upload", fallbackMethod = "rateLimitFallback")
@PostMapping("/upload")
public FileResponse upload(...) { ... }
10. BÀI TẬP THỰC HÀNH
Bài 1: Avatar Upload
Implement API upload avatar cho user.
Bài 2: Document Storage
Xây dựng hệ thống lưu trữ tài liệu với S3.
Bài 3: Image Resize
Upload ảnh và tự động tạo thumbnail.
11. TÓM TẮT
| Khái niệm | Mô tả |
|---|---|
| MultipartFile | File upload |
| @RequestParam | Nhận file |
| @RequestPart | File + metadata |
| Local Storage | Lưu trên server |
| S3 | Cloud storage |
| Streaming | File lớn |
| Validation | Kiểm tra file |
Bài tiếp theo: 6.9 Email & Notification
🧭 Điều Hướng Series
⬅️ Bài trước: BÀI 6.7: PAGINATION & SORTING
📋 Lộ trình tổng quan: Xem Toàn Bộ Series
➡️ Bài tiếp theo: BÀI 6.9: EMAIL & NOTIFICATION
All rights reserved