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/passwd gibi 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 644 ile 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir