0

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

Java Backend Zero to Hello

📚 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

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í