REST API ile Dosya Yükleme: Multipart Form Data Kullanımı
Dosya yükleme işlemleri, modern web uygulamalarının vazgeçilmez bir parçası haline geldi. Kullanıcı avatarları, dökümanlar, medya dosyaları, log arşivleri… Bunların hepsini güvenli ve verimli bir şekilde API üzerinden aktarmak, çoğu sysadmin’in ya da backend geliştiricinin eninde sonunda karşılaştığı bir ihtiyaç. Multipart form data tam da bu noktada devreye giriyor ve HTTP protokolünün sunduğu en pratik çözümlerden biri olmaya devam ediyor.
Multipart Form Data Nedir?
HTTP isteğinin gövdesini birden fazla parçaya bölmeyi sağlayan bir içerik türüdür. Content-Type: multipart/form-data başlığıyla birlikte gönderilir ve her bir parça kendi başlıklarına, içerik türüne sahip olabilir. Bu sayede aynı istekte hem metin verisi hem de binary dosya içeriği bir arada taşınabilir.
Normal bir JSON isteğinde binary veriyi doğrudan göndermek için ya Base64 kodlaması yaparsınız (bu dosya boyutunu %33 artırır) ya da başka bir yol düşünürsünüz. Multipart ile bu sorun ortadan kalkar. Dosya olduğu gibi, binary formatında taşınır.
Bir multipart isteğinin ham hali şöyle görünür:
POST /api/upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
john_doe
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg
<binary dosya içeriği>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
boundary değeri her parçayı birbirinden ayıran sınırlayıcıdır. HTTP istemcisi bunu otomatik olarak üretir.
curl ile Dosya Yükleme
Sysadmin’lerin en yakın dostu curl, multipart yüklemeyi gayet güzel destekler. Test senaryolarında ve otomasyon scriptlerinde sıkça kullanırsınız.
# Tek dosya yükleme
curl -X POST https://api.example.com/upload
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
-F "file=@/home/john/documents/report.pdf"
-F "description=Aylik rapor"
-F "category=finance"
# Birden fazla dosya yükleme
curl -X POST https://api.example.com/upload/bulk
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
-F "files[]=@/tmp/log1.gz"
-F "files[]=@/tmp/log2.gz"
-F "files[]=@/tmp/log3.gz"
-F "archive_date=2024-01-15"
# Dosya adını override etmek
curl -X POST https://api.example.com/upload
-F "file=@/tmp/tmpfile123;filename=gercek_ad.pdf;type=application/pdf"
# Progress bar ile büyük dosya yükleme
curl -X POST https://api.example.com/upload/large
-H "Authorization: Bearer TOKEN"
-F "file=@/var/backups/database.sql.gz"
--progress-bar
-o /dev/null
-F parametresi curl’e multipart/form-data kullanmasını söyler. @ işareti dosya içeriğini okuyacağını belirtir. Bunu bir bash scriptiyle otomatize ettiğinizde, belirli dizinlerdeki dosyaları API’ye düzenli olarak gönderebilirsiniz.
Python ile Multipart Yükleme
Python, özellikle requests kütüphanesiyle bu işi son derece temiz yapar. Monitoring scriptleri veya veri aktarım araçları yazarken tercih edilen dil genellikle Python oluyor.
# requests kütüphanesini yükle
pip install requests
import requests
import os
from pathlib import Path
def dosya_yukle(api_url, dosya_yolu, token, meta_data=None):
"""
Tek dosya yükleme fonksiyonu
"""
dosya_yolu = Path(dosya_yolu)
if not dosya_yolu.exists():
raise FileNotFoundError(f"Dosya bulunamadi: {dosya_yolu}")
headers = {
"Authorization": f"Bearer {token}"
}
# files parametresi multipart/form-data'yı tetikler
with open(dosya_yolu, "rb") as f:
files = {
"file": (dosya_yolu.name, f, "application/octet-stream")
}
# Ek form alanları
data = meta_data or {}
response = requests.post(
api_url,
headers=headers,
files=files,
data=data,
timeout=300 # Büyük dosyalar için timeout artırılmalı
)
response.raise_for_status()
return response.json()
def toplu_yukle(api_url, dizin, uzanti=".log", token=None):
"""
Bir dizindeki tüm dosyaları yükle
"""
basarili = []
basarisiz = []
for dosya in Path(dizin).glob(f"*{uzanti}"):
try:
sonuc = dosya_yukle(
api_url,
dosya,
token,
meta_data={"boyut": str(dosya.stat().st_size)}
)
basarili.append(dosya.name)
print(f"[OK] {dosya.name} yuklendi. ID: {sonuc.get('id')}")
except Exception as e:
basarisiz.append(dosya.name)
print(f"[HATA] {dosya.name}: {e}")
print(f"nToplam: {len(basarili)} basarili, {len(basarisiz)} basarisiz")
return basarili, basarisiz
if __name__ == "__main__":
API_URL = "https://api.example.com/v1/files/upload"
TOKEN = os.environ.get("API_TOKEN")
toplu_yukle(API_URL, "/var/log/nginx", ".log.gz", TOKEN)
files parametresine verilen tuple yapısına dikkat edin: (dosya_adı, dosya_nesnesi, content_type). Bu üçlü yapı sayesinde her şeyi tam kontrol altında tutabilirsiniz.
Node.js ile Dosya Yükleme API’si Yazmak
Sunucu tarafında multipart isteği nasıl karşılanır? Node.js ekosisteminde multer kütüphanesi bu iş için standart haline gelmiştir.
# Gerekli paketleri kur
npm install express multer uuid
const express = require('express');
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const app = express();
// Depolama yapılandırması
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, 'uploads', req.body.category || 'general');
// Dizin yoksa oluştur
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
// Orijinal adı koru ama çakışmayı önle
const uniquePrefix = uuidv4().split('-')[0];
const safeFilename = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
cb(null, `${uniquePrefix}_${safeFilename}`);
}
});
// Dosya filtresi - güvenlik için kritik
const fileFilter = (req, file, cb) => {
const izinliTurler = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain',
'application/gzip'
];
if (izinliTurler.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`Desteklenmeyen dosya türü: ${file.mimetype}`), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 100 * 1024 * 1024, // 100MB limit
files: 10 // Aynı anda max 10 dosya
}
});
// Tek dosya endpoint'i
app.post('/api/v1/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Dosya bulunamadi' });
}
res.json({
id: uuidv4(),
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
path: req.file.path,
uploadedAt: new Date().toISOString()
});
});
// Çoklu dosya endpoint'i
app.post('/api/v1/upload/bulk', upload.array('files', 10), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Hic dosya gönderilmedi' });
}
const yuklenenler = req.files.map(file => ({
id: uuidv4(),
filename: file.filename,
originalName: file.originalname,
size: file.size,
mimetype: file.mimetype
}));
res.json({
count: yuklenenler.length,
files: yuklenenler
});
});
// Hata yönetimi middleware'i
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: 'Dosya boyutu cok büyük (max 100MB)' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: 'Cok fazla dosya (max 10)' });
}
}
res.status(500).json({ error: err.message });
});
app.listen(3000, () => console.log('API sunucusu 3000 portunda calisiyor'));
Güvenlik: En Kritik Konu
Dosya yükleme endpoint’leri, saldırganların gözde hedefleri arasında yer alır. Content-type başlığına güvenmek büyük bir hata olur çünkü bu başlık kolayca manipüle edilebilir. Gerçek güvenlik, dosya içeriğine bakarak tür doğrulaması yapmakla başlar.
# Linux üzerinde dosya türü doğrulama scripti
#!/bin/bash
# validate_upload.sh
UPLOAD_DIR="/var/www/uploads/pending"
SAFE_DIR="/var/www/uploads/approved"
LOG_FILE="/var/log/upload_validation.log"
# İzin verilen MIME türleri (file komutu çıktısına göre)
IZINLI_TURLER=(
"image/jpeg"
"image/png"
"image/gif"
"application/pdf"
"application/gzip"
"text/plain"
)
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}
kontrol_et() {
local dosya="$1"
local dosya_adi=$(basename "$dosya")
# file komutu ile gerçek MIME türünü öğren
gercek_tur=$(file --mime-type -b "$dosya")
# Uzantı ve içerik tutarlılığını kontrol et
uzanti="${dosya_adi##*.}"
# PHP, Python, shell script içeriği var mı?
if file "$dosya" | grep -qiE "PHP|Python|shell|executable"; then
log "[TEHLIKE] Yürütülebilir içerik tespit edildi: $dosya_adi"
rm -f "$dosya"
return 1
fi
# MIME türü izinli mi?
for izinli in "${IZINLI_TURLER[@]}"; do
if [[ "$gercek_tur" == "$izinli" ]]; then
mv "$dosya" "$SAFE_DIR/"
log "[OK] Dosya onaylandi: $dosya_adi ($gercek_tur)"
return 0
fi
done
log "[RED] Izinsiz tur: $dosya_adi ($gercek_tur)"
rm -f "$dosya"
return 1
}
# Bekleyen tüm dosyaları işle
for dosya in "$UPLOAD_DIR"/*; do
[ -f "$dosya" ] && kontrol_et "$dosya"
done
Güvenlik konusunda akılda tutulması gereken başlıca noktalar:
- Dosya adını asla doğrudan kullanmayın: Path traversal saldırılarına karşı
../../../etc/passwdgibi girdileri temizleyin - Web kökü dışında saklayın: Yüklenen dosyaları web sunucusunun erişemeyeceği bir dizine koyun
- Boyut limiti zorunludur: Disk dolu bırakma saldırılarına karşı mutlaka uygulayın
- İçerik türünü magic bytes ile doğrulayın: Uzantı ve Content-Type başlığına güvenmeyin
- Antivirüs taraması yapın: ClamAV gibi araçlarla üretim ortamında dosyaları tarayın
- Çalıştırma iznini kaldırın:
chmod 644ile yüklenen dosyaların çalıştırılmasını engelleyin
Büyük Dosyalar için Chunked Upload
100MB’ı aşan dosyalar için tek parça yükleme güvenilir değildir. Ağ kesintileri, timeout sorunları büyük baş ağrıları yaratır. Bu noktada chunked (parçalı) yükleme devreye girer.
#!/bin/bash
# chunked_upload.sh - Büyük dosyaları parçalara bölerek yükler
API_URL="https://api.example.com/v1/upload/chunked"
TOKEN="${API_TOKEN}"
DOSYA="$1"
PARCA_BOYUTU=$((10 * 1024 * 1024)) # 10MB parçalar
if [ -z "$DOSYA" ] || [ ! -f "$DOSYA" ]; then
echo "Kullanim: $0 <dosya_yolu>"
exit 1
fi
DOSYA_ADI=$(basename "$DOSYA")
DOSYA_BOYUTU=$(stat -c%s "$DOSYA")
UPLOAD_ID=$(curl -s -X POST "${API_URL}/init"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d "{"filename": "${DOSYA_ADI}", "total_size": ${DOSYA_BOYUTU}}"
| python3 -c "import sys,json; print(json.load(sys.stdin)['upload_id'])")
echo "Upload ID: ${UPLOAD_ID}"
echo "Dosya boyutu: ${DOSYA_BOYUTU} byte"
PARCA_NO=0
OFFSET=0
while [ $OFFSET -lt $DOSYA_BOYUTU ]; do
PARCA_NO=$((PARCA_NO + 1))
KALAN=$((DOSYA_BOYUTU - OFFSET))
if [ $KALAN -lt $PARCA_BOYUTU ]; then
GONDERILECEK=$KALAN
else
GONDERILECEK=$PARCA_BOYUTU
fi
echo -n "Parca ${PARCA_NO} gonderiliyor (offset: ${OFFSET})... "
# dd ile ilgili parçayı kes ve gönder
HTTP_STATUS=$(dd if="$DOSYA" bs=1 skip="$OFFSET" count="$GONDERILECEK" 2>/dev/null |
curl -s -o /dev/null -w "%{http_code}"
-X POST "${API_URL}/chunk"
-H "Authorization: Bearer ${TOKEN}"
-F "upload_id=${UPLOAD_ID}"
-F "chunk_number=${PARCA_NO}"
-F "offset=${OFFSET}"
-F "chunk=@-")
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "OK"
else
echo "HATA (HTTP: ${HTTP_STATUS})"
echo "Yuklemeden vazgeciliyor..."
exit 1
fi
OFFSET=$((OFFSET + GONDERILECEK))
done
# Yüklemeyi tamamla
echo "Yukleme tamamlaniyor..."
curl -s -X POST "${API_URL}/complete"
-H "Authorization: Bearer ${TOKEN}"
-H "Content-Type: application/json"
-d "{"upload_id": "${UPLOAD_ID}", "total_chunks": ${PARCA_NO}}"
echo "Tamamlandi!"
Nginx Yapılandırması
Uygulama önünde Nginx varsa (ki çoğu üretim ortamında vardır) bazı ayarları doğrudan Nginx’te yapmanız gerekir.
# /etc/nginx/sites-available/api-upload.conf
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL ayarları...
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Maksimum yükleme boyutu - uygulama limitiyle uyumlu olmalı
client_max_body_size 110M;
# Buffer ayarları - büyük yüklemeler için
client_body_buffer_size 128k;
client_body_timeout 300s;
# Geçici dosya dizini - /tmp yerine SSD üzerinde bir yer
client_body_temp_path /var/nginx/tmp 1 2;
location /api/v1/upload {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off; # Büyük dosyalar için streaming
# Gerçek IP'yi ilet
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Rate limiting - upload endpoint'lerine karşı
limit_req_zone $binary_remote_addr zone=upload_limit:10m rate=5r/m;
location /api/v1/upload/bulk {
limit_req zone=upload_limit burst=2 nodelay;
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 600s;
client_max_body_size 500M;
}
}
proxy_request_buffering off ayarı kritik öneme sahiptir. Bu ayar olmadan Nginx, büyük dosyayı önce kendi diskine yazar sonra uygulamaya iletir. Bu hem gecikme yaratır hem de disk üzerinde iki kez yer kaplar.
Gerçek Dünya Senaryosu: Log Toplama Sistemi
Elimde onlarca sunucu var ve hepsinin günlük log arşivlerini merkezi bir depolama sistemine göndermem gerekiyor. İşte bu iş için kurduğum yapı:
#!/bin/bash
# /usr/local/bin/log_gonder.sh
# cron ile her gece 02:00'de çalışır
# 0 2 * * * /usr/local/bin/log_gonder.sh >> /var/log/log_gonderici.log 2>&1
API_URL="https://logstore.internal.company.com/api/v1/upload"
API_TOKEN=$(cat /etc/log-agent/token)
SUNUCU_ADI=$(hostname -f)
TARIH=$(date +%Y%m%d)
LOG_DIZIN="/var/log"
ARSIV_DIZIN="/tmp/log_arsiv_${TARIH}"
BASARILI=0
BASARISIZ=0
mkdir -p "$ARSIV_DIZIN"
# Log dosyalarını sıkıştır ve gönder
for servis in nginx mysql syslog auth; do
LOG_DOSYA="${LOG_DIZIN}/${servis}.log.1" # Önceki günün logu
[ ! -f "$LOG_DOSYA" ] && continue
ARSIV="${ARSIV_DIZIN}/${SUNUCU_ADI}_${servis}_${TARIH}.log.gz"
# Sıkıştır
gzip -c "$LOG_DOSYA" > "$ARSIV"
# Yükle
HTTP_DURUM=$(curl -s -o /dev/null -w "%{http_code}"
-X POST "$API_URL"
-H "Authorization: Bearer ${API_TOKEN}"
-F "file=@${ARSIV}"
-F "server=${SUNUCU_ADI}"
-F "service=${servis}"
-F "date=${TARIH}"
-F "environment=production"
--max-time 120)
if [ "$HTTP_DURUM" -eq 200 ] || [ "$HTTP_DURUM" -eq 201 ]; then
echo "$(date): [OK] ${servis} logu yuklendi"
BASARILI=$((BASARILI + 1))
rm -f "$ARSIV" # Geçici arşivi temizle
else
echo "$(date): [HATA] ${servis} log yuklemesi basarisiz (HTTP: ${HTTP_DURUM})"
BASARISIZ=$((BASARISIZ + 1))
fi
done
# Özet
echo "$(date): Tamamlandi - Basarili: ${BASARILI}, Basarisiz: ${BASARISIZ}"
# Başarısız varsa monitoring sistemine bildir
if [ $BASARISIZ -gt 0 ]; then
curl -s -X POST https://monitoring.internal.company.com/alert
-H "Content-Type: application/json"
-d "{"severity": "warning", "message": "${SUNUCU_ADI}: ${BASARISIZ} log yuklemesi basarisiz"}"
fi
# Geçici dizini temizle
rm -rf "$ARSIV_DIZIN"
Bu script’i her sunucuya dağıtmak için Ansible kullanıyorum. Token’ları /etc/log-agent/token dosyasında saklıyorum ve dosya izinlerini chmod 600 ile kısıtlıyorum.
Sık Karşılaşılan Sorunlar ve Çözümleri
413 Request Entity Too Large hatası aldığınızda genellikle sorun Nginx veya Apache’nin boyut limitindedir. client_max_body_size değerini artırmanız gerekir. Uygulama katmanının da kendi limiti olduğunu unutmayın.
Timeout hataları büyük dosyalarda kaçınılmazdır. proxy_read_timeout, proxy_send_timeout ve uygulama katmanındaki timeout değerlerini birlikte artırın.
CORS sorunları browser’dan yükleme yapıyorsanız dikkat edin. Access-Control-Allow-Origin başlığının yanı sıra Access-Control-Allow-Headers: Content-Type de gereklidir.
Geçici dosya dolması çok sık yükleme olan sistemlerde /tmp dolabilir. Nginx’in client_body_temp_path ve uygulamanın geçici dizinini ayrı bir partition’a alın.
Memory kullanımı sunucu tarafında stream kullanmayan implementasyonlar dosyayı tüm hafızaya yükler. Node.js’te multer varsayılan olarak disk storage kullanır, memory storage’a geçerseniz RAM’e dikkat edin.
Sonuç
Multipart form data, REST API üzerinden dosya aktarımının en pratik ve yaygın yöntemi olmaya devam ediyor. curl ile hızlı test edebilir, Python ile otomasyon scriptleri yazabilir, Node.js ile güçlü bir upload endpoint’i kurabilirsiniz.
Ancak işin güvenlik kısmını asla hafife almayın. Dosya yükleme endpoint’leri doğru yapılandırılmadığında sunucunuza açılan bir kapıya dönüşür. İçerik türü doğrulaması, boyut limitleri, path traversal koruması ve web kökü dışı depolama bu işin dört temel taşıdır.
Chunked upload ihtiyacı hissettirdiğinde erken önlem alın. 50-100MB sınırında uygulamayı yeniden yazmak zorunda kalmamak için bu yapıyı baştan düşünmek çok daha akıllıca. Üretim ortamında rate limiting ve monitoring mutlaka olsun. Kaç dosya yüklendiğini, ne kadar disk harcandığını takip etmezseniz sürprizlerle karşılaşırsınız.
