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österStreamingResponseBody 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.
AI Asistan
Sorularını yanıtlamaya hazır