0

[Java Backend Zero to Hello] [Phase 6] BÀI 6.8: FILE UPLOAD/DOWNLOAD

📚 Series: Java Backend Zero to Hello 📂 Phân đoạn: Phase 6: REST API & Best Practices 📖 Nội dung: BÀI 6.8: FILE UPLOAD/DOWNLOAD 💡 Khóa học lập trình Backend Java & Spring Boot chuẩn doanh nghiệp từ con số 0.


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 bài học

⭐ Hãy bookmark (clip) lại series để tiện theo dõi các bài học tiếp theo nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí