PHP dosya indirme işlemi ilk bakışta basit görünür: dosyayı oku, ekrana bas, bitti. Ama birkaç yüz megabaytlık bir log arşivini ya da bir video dosyasını bu yöntemle sunmaya çalıştığınızda sunucu belleği bir anda dolar, PHP süreci Allowed memory size exhausted hatasıyla çöker ve kullanıcı yarım kalmış bir indirme ile kalır. Bu yazıda dosyaları doğru başlıklarla nasıl sunacağınızı ve büyük dosyaları belleği taşırmadan nasıl stream edeceğinizi adım adım anlatıyorum.
Neden basit yöntem bellek taşırır
En sık görülen hata, dosyanın tamamını tek seferde belleğe almaktır:
// KÖTÜ: 500 MB'lık dosya = 500 MB RAM
$data = file_get_contents('/path/to/big.zip');
echo $data;
file_get_contents ve readfile öncesi yapılan echo kombinasyonları, dosyanın tamamını PHP'nin bellek alanına kopyalar. memory_limit değeriniz 256 MB ise, 300 MB'lık bir dosya daha indirme başlamadan süreci öldürür. Çözüm, dosyayı parça parça okuyup her parçayı anında çıktıya yazmak, yani stream etmektir. Böylece bellekte aynı anda yalnızca küçük bir tampon (buffer) bulunur.
Doğru indirme başlıkları
Tarayıcının dosyayı görüntülemek yerine indirmesini sağlamak için doğru HTTP başlıklarını göndermeniz gerekir. En kritik olanlar şunlardır:
Content-Type— dosyanın MIME türü (bilinmiyorsaapplication/octet-stream).Content-Disposition: attachment; filename="..."— tarayıcıya indirmesini ve hangi adla kaydedeceğini söyler.Content-Length— dosya boyutu; ilerleme çubuğunun doğru çalışması için gereklidir.
$file = '/path/to/big.zip';
$name = 'arsiv.zip';
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . filesize($file));
header('X-Content-Type-Options: nosniff');
Content-Disposition içindeki dosya adında Türkçe karakter veya boşluk varsa, eski tarayıcılarla uyum için filename*=UTF-8'' sözdizimini de eklemek iyi olur.
readfile ile basit stream
Orta boy dosyalar için PHP'nin readfile fonksiyonu yeterlidir. readfile, dosyayı PHP belleğine almadan doğrudan çıktı tamponuna aktarır; ancak çıktı tamponlaması (output buffering) açıksa avantajı kaybolur. Bu yüzden önce tamponu temizleyin:
// Açık çıktı tamponlarını kapat
while (ob_get_level() > 0) {
ob_end_clean();
}
readfile($file);
exit;
readfile tek satırda işi halleder ama akış üzerinde kontrol vermez. Hız sınırlama, devam eden indirmeler (resume) veya parça başına işlem yapmak isterseniz manuel okumaya geçmeniz gerekir.
fopen/fread ile tam kontrollü stream
Gerçekten büyük dosyalar ve tam kontrol için dosyayı sabit boyutlu parçalar halinde okuyun. Aşağıdaki döngü, her seferinde yalnızca 8 KB belleği kullanır:
$handle = fopen($file, 'rb');
if ($handle === false) {
http_response_code(404);
exit;
}
while (!feof($handle)) {
echo fread($handle, 8192); // 8 KB'lık parçalar
flush(); // tamponu hemen istemciye gönder
}
fclose($handle);
exit;
Dosyayı 'rb' (binary) kipinde açmak Windows sunucularda zip ve resim gibi ikili dosyaların bozulmasını önler. flush() çağrısı veriyi bekletmeden gönderir. Çok büyük indirmelerde, kullanıcı sekmeyi kapatırsa boşuna okumaya devam etmemek için döngü başına if (connection_aborted()) break; kontrolü ekleyebilirsiniz.
Yürütme süresi ve güvenlik
Yavaş bir bağlantıda büyük bir dosya, varsayılan max_execution_time (genelde 30 sn) sınırını aşabilir. Stream başlamadan önce süreyi sıfırlayın; set_time_limit(0) her parçada zamanlayıcıyı yenileyebilir ama daha güvenli yöntem stream döngüsü içinde periyodik set_time_limit çağırmaktır.
Güvenlik tarafında en kritik nokta, kullanıcı girdisini doğrudan dosya yoluna koymamaktır:
- Path traversal'i engelleyin:
../../etc/passwdgibi girdileribasename()ile temizleyin ve dosyanın izin verilen dizinde olduğunurealpath()ile doğrulayın. - Beyaz liste kullanın: İndirilebilir dosyaları veritabanında bir ID ile eşleştirin; istemciye ham yol göstermeyin.
- Yetki kontrolü yapın: Stream'i başlatmadan önce kullanıcının o dosyaya erişim hakkı olduğunu doğrulayın.
$base = '/var/www/downloads';
$path = realpath($base . '/' . basename($_GET['file'] ?? ''));
if ($path === false || strpos($path, $base) !== 0) {
http_response_code(403);
exit('Erişim reddedildi');
}
Laravel ve Symfony'de pratik yol
Bir framework kullanıyorsanız bu işi elle yapmanıza gerek yok. Laravel'de response()->download() dosyayı indirme olarak sunar, response()->streamDownload() ise üretilen içeriği (örneğin anlık CSV) bellek doldurmadan akıtır. Symfony tarafında BinaryFileResponse aynı işi yapar ve X-Sendfile başlığını destekler.
// Laravel
return response()->download('/path/to/big.zip', 'arsiv.zip');
// Akış halinde üretilen içerik
return response()->streamDownload(function () {
$out = fopen('php://output', 'w');
foreach ($rows as $row) {
fputcsv($out, $row);
}
fclose($out);
}, 'rapor.csv');
En yüksek performans için, statik dosyaları PHP üzerinden hiç geçirmeyip web sunucusuna devretmek en iyisidir: Nginx'te X-Accel-Redirect, Apache'de X-Sendfile başlığıyla PHP yetki kontrolünü yapar, dosyayı ise sunucu doğrudan gönderir. Bu yöntem PHP işçisini saniyelerce meşgul etmez.
Sık Sorulan Sorular
readfile mi yoksa fread döngüsü mü kullanmalıyım?
Orta boy dosyalar için ve çıktı tamponunu temizlediyseniz readfile yeterli ve en basit yoldur. Hız sınırlama, devam eden indirme desteği ya da bağlantı kopması kontrolü gibi ince ayara ihtiyacınız varsa fopen/fread döngüsü size tam kontrol verir.
İndirme yarıda kesilip "bozuk dosya" hatası alıyorum, neden?
En sık sebep, dosyadan önce çıktıya sızan boşluk veya BOM karakteridir. Header'lardan önce hiçbir şey yazdırmadığınızdan emin olun, açık ob_* tamponlarını kapatın ve Content-Length değerinin gerçek boyutla aynı olduğundan emin olun.
Çok kullanıcı aynı anda indirince sunucu neden yavaşlıyor?
Her aktif indirme bir PHP işçisini meşgul tutar. Büyük statik dosyalarda X-Accel-Redirect (Nginx) veya X-Sendfile (Apache) kullanarak teslimi web sunucusuna devredin; böylece PHP işçileri hızla serbest kalır.
Dosya indirme altyapınız mı sorun çıkarıyor? Bellek taşıran indirmeleri stream'e çevirip güvenli ve hızlı hale getirebilirim. Benimle iletişime geçin, projenizi birlikte değerlendirelim.