← Kursa Dön
📄 Text · 35 min

File Upload & Download

Dosya yükleme ve indirme, neredeyse her web uygulamasının ihtiyaç duyduğu temel bir özelliktir: profil fotoğrafı, belge, rapor, Excel dışa aktarma gibi senaryolar. Spring Boot, MultipartFile arayüzü ve çeşitli Resource implementasyonları ile dosya işlemlerini son derece kolaylaştırır. Bu derste dosya yükleme, indirme, streaming ve chunk upload konularını detaylı inceleyeceğiz.

MultipartFile ile Dosya Yükleme

HTTP'de dosya yükleme multipart/form-data content type ile yapılır. Spring Boot'ta MultipartFile arayüzü gelen dosyayı temsil eder:

@RestController
@RequestMapping("/api/files")
public class FileController {

    private final Path uploadDir = Path.of("uploads");

    @PostMapping("/upload")
    public ResponseEntity<FileResponse> uploadFile(
            @RequestParam("file") MultipartFile file) throws IOException {

        // Validasyonlar
        if (file.isEmpty()) {
            throw new BadRequestException("Dosya boş olamaz");
        }

        // Dosya boyutu kontrolü (10MB)
        if (file.getSize() > 10 * 1024 * 1024) {
            throw new BadRequestException("Dosya 10MB'dan büyük olamaz");
        }

        // Dosya tipi kontrolü
        String contentType = file.getContentType();
        Set<String> allowed = Set.of(
            "image/jpeg", "image/png", "image/gif", "application/pdf");
        if (!allowed.contains(contentType)) {
            throw new BadRequestException("Desteklenmeyen dosya tipi: "
                + contentType);
        }

        // Güvenli dosya adı oluştur
        String originalName = StringUtils
            .cleanPath(file.getOriginalFilename());
        String extension = originalName.substring(
            originalName.lastIndexOf("."));
        String storedName = UUID.randomUUID() + extension;

        // Dizin yoksa oluştur
        Files.createDirectories(uploadDir);

        // Dosyayı kaydet
        Path targetPath = uploadDir.resolve(storedName);
        file.transferTo(targetPath);

        FileResponse response = new FileResponse(
            storedName, originalName, file.getSize(), contentType);
        URI location = ServletUriComponentsBuilder
            .fromCurrentContextPath()
            .path("/api/files/{filename}")
            .buildAndExpand(storedName)
            .toUri();

        return ResponseEntity.created(location).body(response);
    }
}

record FileResponse(
    String storedName,
    String originalName,
    long size,
    String contentType
) {}

application.yml — Multipart Yapılandırması

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB          # tek dosya limiti
      max-request-size: 50MB       # toplam istek limiti
      file-size-threshold: 2KB     # bu boyuttan büyükse diske yaz
      location: /tmp/uploads       # geçici dizin

@RequestPart ile Dosya + JSON

Aynı istekte hem dosya hem JSON göndermek için @RequestPart kullanılır:

@PostMapping(value = "/products",
             consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Product> createProduct(
        @RequestPart("data") @Valid ProductDto productDto,
        @RequestPart("image") MultipartFile image)
        throws IOException {

    // image dosyasını kaydet
    String imagePath = fileService.store(image);

    // ürünü oluştur
    Product product = productService.create(productDto, imagePath);
    return ResponseEntity.status(HttpStatus.CREATED).body(product);
}

cURL ile test:

curl -X POST http://localhost:8080/api/products \
  -F 'data={"name":"Laptop","price":999.99};type=application/json' \
  -F 'image=@laptop.jpg'

Birden Fazla Dosya Yükleme

@PostMapping("/upload-multiple")
public ResponseEntity<List<FileResponse>> uploadMultiple(
        @RequestParam("files") List<MultipartFile> files)
        throws IOException {

    List<FileResponse> responses = new ArrayList<>();
    for (MultipartFile file : files) {
        // her dosyayı kaydet
        String stored = fileService.store(file);
        responses.add(new FileResponse(
            stored, file.getOriginalFilename(),
            file.getSize(), file.getContentType()));
    }
    return ResponseEntity.ok(responses);
}

Resource ile Dosya İndirme

Spring'in Resource arayüzü dosya indirme işlemlerini basitleştirir:

@GetMapping("/{filename}")
public ResponseEntity<Resource> downloadFile(
        @PathVariable String filename) throws IOException {

    Path filePath = uploadDir.resolve(filename).normalize();

    // Path traversal saldırısını engelle
    if (!filePath.startsWith(uploadDir)) {
        throw new BadRequestException("Geçersiz dosya yolu");
    }

    Resource resource = new UrlResource(filePath.toUri());
    if (!resource.exists()) {
        throw new ResourceNotFoundException("Dosya bulunamadı: "
            + filename);
    }

    String contentType = Files.probeContentType(filePath);
    if (contentType == null) {
        contentType = "application/octet-stream";
    }

    return ResponseEntity.ok()
        .contentType(MediaType.parseMediaType(contentType))
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + resource.getFilename() + "\"")
        .body(resource);
}

Content-Disposition header'ı tarayıcıya dosyanın indirilmesini (attachment) veya görüntülenmesini (inline) söyler:

Content-Disposition: attachment; filename="rapor.pdf"  → indir
Content-Disposition: inline; filename="foto.jpg"       → göster

StreamingResponseBody ile Büyük Dosyalar

Büyük dosyaları belleğe yüklemeden streaming ile göndermek için StreamingResponseBody kullanılır:

@GetMapping("/export/csv")
public ResponseEntity<StreamingResponseBody> exportCsv() {

    StreamingResponseBody stream = outputStream -> {
        try (Writer writer = new OutputStreamWriter(
                outputStream, StandardCharsets.UTF_8)) {

            writer.write("id,name,email\n");

            // Veritabanından sayfa sayfa oku ve yaz
            int page = 0;
            Page<User> users;
            do {
                users = userRepo.findAll(PageRequest.of(page, 1000));
                for (User u : users) {
                    writer.write(String.format("%d,%s,%s%n",
                        u.getId(), u.getName(), u.getEmail()));
                    writer.flush();
                }
                page++;
            } while (users.hasNext());
        }
    };

    return ResponseEntity.ok()
        .contentType(MediaType.parseMediaType("text/csv"))
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"users.csv\"")
        .body(stream);
}

StreamingResponseBody sayesinde milyonlarca satırlık veri bile bellek taşırması yapmadan aktarılabilir. Spring, bu response'u asenkron olarak işler ve Servlet output stream'ine doğrudan yazar.

Chunk Upload (Parçalı Yükleme)

Çok büyük dosyalar (yüzlerce MB, GB) için dosya parçalara bölünüp sırayla gönderilir:

@PostMapping("/upload/chunk")
public ResponseEntity<?> uploadChunk(
        @RequestParam("file") MultipartFile chunk,
        @RequestParam("uploadId") String uploadId,
        @RequestParam("chunkIndex") int chunkIndex,
        @RequestParam("totalChunks") int totalChunks)
        throws IOException {

    Path chunkDir = uploadDir.resolve("chunks").resolve(uploadId);
    Files.createDirectories(chunkDir);

    // Chunk'ı kaydet
    Path chunkPath = chunkDir.resolve("chunk-" + chunkIndex);
    chunk.transferTo(chunkPath);

    // Tüm chunk'lar geldi mi kontrol et
    long uploadedChunks = Files.list(chunkDir).count();
    if (uploadedChunks == totalChunks) {
        // Chunk'ları birleştir
        Path finalFile = uploadDir.resolve(uploadId + ".dat");
        try (OutputStream out = Files.newOutputStream(finalFile)) {
            for (int i = 0; i < totalChunks; i++) {
                Path part = chunkDir.resolve("chunk-" + i);
                Files.copy(part, out);
            }
        }
        // Chunk dizinini temizle
        FileSystemUtils.deleteRecursively(chunkDir);

        return ResponseEntity.ok(Map.of(
            "status", "completed",
            "filename", uploadId + ".dat"));
    }

    return ResponseEntity.ok(Map.of(
        "status", "partial",
        "uploaded", uploadedChunks,
        "total", totalChunks));
}

Dosya işlemleri, güvenlik açısından dikkatli ele alınmalıdır: path traversal saldırılarını engellemek, dosya tiplerini doğrulamak, boyut limitlerini uygulamak ve dosya adlarını sanitize etmek zorunlu güvenlik pratikleridir.