---
title: 'Dosya Yükleme ve Depolama'
source: 'https://academia.sh/tr/kurslar/sunucu-temelleri/dosya-yukleme-ve-depolama'
course: 'Sunucu Tarafı Temelleri'
language: tr
updated: '2026-08-17T18:06:52+00:00'
license: 'CC BY-SA 4.0'
---

# Dosya Yükleme ve Depolama

Dışarıdan gelen dosyanın sınırlarla kabul edilmesi: bildirilen uzunluk ile akış sayacının iki katmanlı boyut sınırı, reddin karşı tarafa ulaştırılması, türün uzantı yerine içerikten saptanması, depo dizininin sunulan dizinden ayrılması ve dosya adının sunucuda üretilmesi.

Önceki iki ders sunucudan dışarı akan veriyi ele aldı: diskteki dosya, şablondan üretilen
belge. Kütüphane servisinin işleyişinde ters yönde de veri akar. Bağışlanan kitabın kapak
görüntüsü, üye kaydına eklenen fotoğraf, şubeden gelen sayım dosyası sunucuya dışarıdan
gelir.

Dışarıdan gelen içeriğin üç özelliği, sunucudan çıkan içerikten ayrılır: boyutu önceden
bilinmez, türü bildirildiği gibi olmayabilir ve nereye yazılacağı sunucunun kararıdır. Bu
ders yükleme ucunu bu üç sorunun cevabı olarak kurar. Ele alınan her kalem bir
**savunmadır**: sunucunun kabul etmeden önce uyguladığı bir kural.

## Boyut Sınırı İki Katmanlıdır

Bir yükleme gövdesinin boyutu iki ayrı yerden öğrenilebilir ve ikisi de tek başına
yetmez.

Birincisi **bildirilen uzunluktur**: istek başlığındaki içerik uzunluğu değeri. Bu değer
gövde hiç okunmadan görülür, dolayısıyla sınırın üstündeki bir yükleme daha ilk baytı
belleğe alınmadan reddedilebilir. Ucuz ve erken bir savunmadır.

İkincisi **akış sayacıdır**: gövde okunurken gerçekte gelen baytların sayılması.
Bildirilen uzunluk isteğin bir parçasıdır ve isteği kuran taraf onu göndermeyebilir ya da
yanlış gönderebilir; parçalı aktarımda başlık hiç bulunmaz. Sayaç, bildirime bakmaksızın
sınırı uygular.

İki katman birlikte çalışır: bildirim erken reddi mümkün kılar, sayaç reddin
atlanamamasını sağlar. Yalnız birincisi varsa sınır bildirilmediğinde kaybolur; yalnız
ikincisi varsa büyük bir yük her seferinde sınıra kadar okunur.

## Reddi Karşı Tarafa Ulaştırmak

Sınırı aşan bir isteğin bağlantısını anında kapatmak akla yakın gelir ama sonucu
beklenmediktir: karşı taraf gövdeyi yazmayı sürdürdüğü için bağlantı hata ile kopar ve
sunucunun yazdığı yanıt hiç okunmaz. Gönderen tarafta görünen şey "bağlantı sıfırlandı"
olur; sınırın aşıldığı, sınırın ne olduğu ve isteğin nasıl düzeltileceği kaybolur.

Reddin okunabilmesi için sunucunun gelen baytları **boşaltmaya** (draining) devam etmesi
gerekir: biriktirmeden, ama okuyarak. Boşaltmanın da bir üst sınırı olur; o sınır da
aşıldığında bağlantı düşürülür, çünkü sonsuza kadar okumak reddin anlamını ortadan
kaldırır.

Bu, yükleme ucunun en kolay atlanan ayrıntısıdır ve ölçümde doğrudan görünür: boşaltma
yoksa istemci 413 yanıtı yerine bir bağlantı hatası görür.

## Tür Uzantıdan Değil İçerikten Saptanır

Yüklenen dosyanın türü hakkında sunucuya iki bilgi ulaşır: istekteki içerik türü başlığı
ve dosya adının uzantısı. İkisi de gönderen tarafın yazdığı metinlerdir; içerikle
uyuşmaları zorunlu değildir. Uyuşmadıklarında ortaya çıkan durum bir saldırıdan önce bir
tutarsızlıktır: kayıt görüntü olarak işaretlenir, görüntü olarak sunulur, görüntü
değildir.

Güvenilir kaynak içeriğin kendisidir. Yaygın ikili biçimler dosyanın başında sabit bir
bayt dizisi taşır; bu diziye **imza** (magic number) denir. Sunucu kabul ettiği türlerin
imzalarını bilir ve gövdenin ilk baytlarını bu listeyle karşılaştırır. Eşleşme yoksa
yükleme reddedilir.

İmza denetimi bir izin listesidir: sunucu neyi kabul ettiğini sayar, neyi reddettiğini
değil. Liste dışındaki her şey, tanınmadığı için reddedilir. Ters yönde kurulan bir
kural — belli türleri yasaklamak — listeye girmeyen her yeni biçimi sessizce kabul eder.

İmza denetiminin sınırı da açıkça bilinmelidir: imza dosyanın biçimini gösterir, geri
kalanının geçerli olduğunu göstermez. Görüntülerin boyutu ve içeriği ayrıca sınanacaksa
bu iş biçimi çözen bir adımın işidir.

## Depolama Yeri ve Üretilen Ad

Yüklenen dosya nereye yazılır sorusunun cevabı, ilk dersteki kök dizin kararının
devamıdır. Statik sunucu kök dizinin altındaki her dosyayı sunar. Yüklenen içerik o
dizine yazılırsa, dışarıdan gelen bayt dizileri sunucunun sunum alanına doğrudan eklenmiş
olur. Bu yüzden **depo dizini sunulan dizinden ayrı tutulur** ve yüklenen içerik ancak
uygulamanın kendi ucundan, kaydı bulunarak sunulur.

Dosya adı da dışarıdan alınmaz. Gönderilen ad yalnızca kullanıcıya gösterilecek bir üstveri
olarak saklanır; diske yazılan ad sunucuda **üretilir**. Üretilen adın üç özelliği vardır:
çakışmaz, dizin yapısında anlam taşımaz ve uzantısı gönderilen addan değil **saptanan
türden** gelir. Böylece hem eşzamanlı iki yüklemenin birbirini ezmesi, hem de adın dosya
yolu olarak yorumlanması engellenmiş olur.

Sunum tarafında iki başlık daha eklenir. İçerik türü kayıttan okunur, bildirilenden değil;
ve tür tahmini kapatılır, böylece alıcı taraf bildirilen türün dışına çıkarak içeriği
başka bir şey sanmaz.

## Uç Noktanın Tamamı

Aşağıdaki uç, tarayıcı formlarının kullandığı **çok parçalı form verisi**
(multipart/form-data) kodlamasını çözmek yerine gövdeyi doğrudan dosya baytları olarak
alır. Bu, sınır mantığını parça ayrıştırmasının arkasına saklamamak içindir; çok parçalı
kodlamada aynı üç savunma her parça için ayrı ayrı uygulanır.

```js
// yukleme.mjs — kapak gorseli yukleme ucu: boyut siniri, icerik denetimi, ayri depo
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { join } from "node:path";

const SINIR = 64 * 1024;          // kabul edilen en buyuk kapak gorseli
const BOSALTMA = 1024 * 1024;     // reddedilen govdeden okunmaya devam edilecek en fazla bayt
const DEPO = join(process.cwd(), "veri", "kapaklar"); // sunulan dizinin disinda
const kayitlar = new Map();       // uretilen ad -> { tur, bayt }

// Tur, bildirilen basliktan ya da dosya adindan degil, govdenin ilk baytlarindan belirlenir.
const IMZALAR = [
  { uzanti: "png", tur: "image/png", imza: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
  { uzanti: "jpg", tur: "image/jpeg", imza: [0xff, 0xd8, 0xff] },
  { uzanti: "gif", tur: "image/gif", imza: [0x47, 0x49, 0x46, 0x38] },
];
const turBelirle = (govde) =>
  IMZALAR.find((k) => k.imza.every((b, i) => govde[i] === b)) ?? null;

// Govdeyi okur. Sinir asilirsa baytlar biriktirilmez ama akis bosaltma sinirina kadar
// okunmaya devam eder: bosaltilmadan yazilan yanit istemciye ulasmadan baglanti kirilir.
const govdeyiOku = (istek, pesinenRed) =>
  new Promise((coz) => {
    const parcalar = [];
    let toplam = 0;
    let asildi = pesinenRed;
    istek.on("data", (parca) => {
      toplam += parca.length;
      if (!asildi && toplam > SINIR) { asildi = true; parcalar.length = 0; }
      if (asildi) { if (toplam > BOSALTMA) istek.destroy(); return; }
      parcalar.push(parca);
    });
    istek.on("end", () => coz(asildi ? null : Buffer.concat(parcalar)));
    istek.on("error", () => coz(null));
    istek.on("close", () => coz(null));
  });

const json = (yanit, kod, govde) => {
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(kod).end(metin);
};

await mkdir(DEPO, { recursive: true });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;

  if (istek.method === "POST" && istek.url === "/kapak") {
    // 1. Savunma: bildirilen uzunluk sinirin ustundeyse govde hic biriktirilmez.
    // 2. Savunma: gercekte gelen bayt sayilir; bildirim yanlissa da sinir uygulanir.
    const bildirilen = Number(istek.headers["content-length"] ?? 0);
    const asama = bildirilen > SINIR ? "bildirilen_uzunluk" : "akis_sayaci";
    const govde = await govdeyiOku(istek, bildirilen > SINIR);
    if (govde === null)
      return json(yanit, 413, { hata: "sinir_asildi", asama, sinir: SINIR });

    // 3. Savunma: tur, gonderilen baslik ve dosya adi degil, icerik belirler.
    const tur = turBelirle(govde);
    if (!tur)
      return json(yanit, 415, {
        hata: "tur_kabul_edilmiyor",
        bildirilen: istek.headers["content-type"] ?? null,
      });

    // 4. Savunma: ad sunucuda uretilir; uzanti saptanan turden gelir.
    const ad = `${randomUUID()}.${tur.uzanti}`;
    await writeFile(join(DEPO, ad), govde);
    kayitlar.set(ad, { tur: tur.tur, bayt: govde.length });

    yanit.setHeader("Location", `/kapak/${ad}`);
    return json(yanit, 201, { ad, tur: tur.tur, bayt: govde.length });
  }

  // 5. Savunma: sunum kayitli adlar uzerinden yapilir; istek yolu dosya yoluna donusmez.
  if (istek.method === "GET" && istek.url.startsWith("/kapak/")) {
    const ad = istek.url.slice("/kapak/".length);
    const kayit = kayitlar.get(ad);
    if (!kayit) return json(yanit, 404, { hata: "bulunamadi" });
    const govde = await readFile(join(DEPO, ad));
    yanit.setHeader("Content-Type", kayit.tur);
    yanit.setHeader("Content-Length", govde.length);
    yanit.setHeader("Content-Disposition", "inline");
    yanit.setHeader("X-Content-Type-Options", "nosniff");
    return yanit.writeHead(200).end(govde);
  }

  return json(yanit, 404, { hata: "bulunamadi" });
}).listen(8312, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8312"));
```

Kayıtların bellekte tutulması yalnızca bu dersin kapsamı içindir; sunucu yeniden
başladığında kayıtlar kaybolur ve diskteki dosyalar erişilemez kalır. Üstverinin kalıcı
bir depoya yazılması, veri erişimi kursunun konusudur.

## Ölçüm

Ölçüm üç dosya kullanır: geçerli bir görüntü, adı görüntü uzantısıyla biten bir metin
dosyası ve sınırın üstünde bir dosya. Aşağıdaki blok üçünü de üretir.

```js
// ornek-uret.mjs — deneme icin uc dosya uretir: gecerli PNG, PNG uzantili metin, buyuk PNG
import { crc32, deflateSync } from "node:zlib";
import { statSync, writeFileSync } from "node:fs";

const parca = (tip, veri) => {
  const uzunluk = Buffer.alloc(4);
  uzunluk.writeUInt32BE(veri.length);
  const govde = Buffer.concat([Buffer.from(tip, "ascii"), veri]);
  const sag = Buffer.alloc(4);
  sag.writeUInt32BE(crc32(govde) >>> 0);
  return Buffer.concat([uzunluk, govde, sag]);
};

const png = (en, boy) => {
  const ihdr = Buffer.alloc(13);
  ihdr.writeUInt32BE(en, 0); ihdr.writeUInt32BE(boy, 4);
  ihdr[8] = 8; ihdr[9] = 2; // 8 bit derinlik, dogru renk
  const satir = Buffer.alloc(1 + en * 3); // suzgec bayti + RGB
  const ham = Buffer.concat(Array.from({ length: boy }, () => satir));
  return Buffer.concat([
    Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
    parca("IHDR", ihdr),
    parca("IDAT", deflateSync(ham)),
    parca("IEND", Buffer.alloc(0)),
  ]);
};

writeFileSync("kapak.png", png(8, 8));
writeFileSync("sahte.png", Buffer.from("Bu bir metin dosyasidir, adi .png ile bitiyor.\n"));
writeFileSync("buyuk.png", Buffer.concat([png(1200, 1200), Buffer.alloc(120 * 1024, 0x41)]));
for (const a of ["kapak.png", "sahte.png", "buyuk.png"])
  console.log(a, statSync(a).size, "B");
```

```bash
#!/usr/bin/env bash
# yukleme.mjs'yi baslatir, dort yukleme denemesini sinar, depoyu listeler, durdurur.
rm -rf veri
node ornek-uret.mjs
node yukleme.mjs > /dev/null &
sunucu=$!
sleep 1
A=http://127.0.0.1:8312

echo "--- 1) gecerli kapak (69 B) ---"
AD=$(curl -sS -X POST --data-binary @kapak.png -H 'Content-Type: image/png' "$A/kapak" \
     | tee /dev/stderr | sed 's/.*"ad":"\([^"]*\)".*/\1/')
echo
echo "--- 2) yuklenen kapagin sunulmasi ---"
curl -sS -D - -o /dev/null "$A/kapak/$AD"

echo "--- 3) .png adli metin dosyasi, image/png bildirilerek ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @sahte.png \
     -H 'Content-Type: image/png' "$A/kapak"

echo "--- 4) sinirin ustunde, uzunluk bildirerek ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @buyuk.png \
     -H 'Content-Type: image/png' "$A/kapak"

echo "--- 5) sinirin ustunde, uzunluk bildirmeden (parcali aktarim) ---"
curl -sS -o - -w '  <- HTTP %{http_code}\n' -X POST --data-binary @buyuk.png \
     -H 'Content-Type: image/png' -H 'Transfer-Encoding: chunked' "$A/kapak"

echo "--- depoya yazilanlar ---"
ls veri/kapaklar | sed 's/^/  /'
echo "  toplam dosya: $(ls veri/kapaklar | wc -l | tr -d ' ')"

kill "$sunucu"
```

```
kapak.png 69 B
sahte.png 47 B
buyuk.png 127159 B
--- 1) gecerli kapak (69 B) ---
{"ad":"158bb07e-446a-49bb-b053-4cd61d4500f6.png","tur":"image/png","bayt":69}
--- 2) yuklenen kapagin sunulmasi ---
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 69
Content-Disposition: inline
X-Content-Type-Options: nosniff
Connection: keep-alive
Keep-Alive: timeout=5

--- 3) .png adli metin dosyasi, image/png bildirilerek ---
{"hata":"tur_kabul_edilmiyor","bildirilen":"image/png"}  <- HTTP 415
--- 4) sinirin ustunde, uzunluk bildirerek ---
{"hata":"sinir_asildi","asama":"bildirilen_uzunluk","sinir":65536}  <- HTTP 413
--- 5) sinirin ustunde, uzunluk bildirmeden (parcali aktarim) ---
{"hata":"sinir_asildi","asama":"akis_sayaci","sinir":65536}  <- HTTP 413
--- depoya yazilanlar ---
  158bb07e-446a-49bb-b053-4cd61d4500f6.png
  toplam dosya: 1
```

Üretilen ad her çalıştırmada değişir; bağlantı başlıkları çalışma zamanının varsayılan
ayarına bağlıdır. Bağlantı noktası 8312 keyfîdir ve boş olmalıdır.

Dört sonuç okunur. **Tür denetimi bildirime bakmamıştır**: üçüncü denemede istek türü
görüntü olarak bildirilmiş, içerik metin olduğu için yükleme 415 ile reddedilmiştir.
**İki sınır katmanı da ayrı ayrı çalışmıştır**: dördüncü denemede ret bildirilen
uzunlukta, beşinci denemede — uzunluk hiç bildirilmediği için — akış sayacında olmuştur.
Yanıttaki aşama alanı hangi katmanın devreye girdiğini söyler. **Reddedilen hiçbir yükleme
diske yazılmamıştır**: iki yüz kilobayta yakın veri gönderilmiş olmasına karşın depoda tek
dosya vardır ve o da geçerli kapaktır.

## Özet

- Boyut sınırı iki katmandan oluşur: bildirilen uzunluk erken ve ucuz reddi, akış sayacı
  ise bildirimin yanlış ya da eksik olduğu durumda sınırın uygulanmasını sağlar.
- Sınırı aşan istek anında kesilirse yazılan yanıt karşı tarafa ulaşmaz; ret, gelen
  baytlar kendi üst sınırına kadar boşaltılarak bildirilir.
- Dosya türü bildirilen başlıktan ya da uzantıdan değil, gövdenin baş kısmındaki imzadan
  saptanır ve kabul edilen türler bir izin listesiyle sayılır.
- Depo dizini statik sunumun kök dizininden ayrı tutulur; yüklenen içerik ancak kaydı
  bulunarak, uygulamanın kendi ucundan sunulur.
- Diske yazılan ad sunucuda üretilir ve uzantısı saptanan türden gelir; gönderilen ad
  yalnızca gösterilecek bir üstveri olarak saklanır.

## Sonraki Adım

Bu ders sunucunun diske yazdığı ilk kalıcı veriyi ortaya çıkardı ve bir eksiği de görünür
kıldı: kayıtlar bellekte durduğu için sunucu yeniden başladığında kaybolmakta. Gerçek
kurulumda o kayıtlar bir veritabanında, kapak dosyaları bir nesne deposunda, arama ise
ayrı bir serviste durur. Uygulama artık tek başına çalışan bir süreç değildir; yanında
başka servisler ister. Kursun son dersi bu bağımlılıkları geliştiricinin makinesinde
ayağa kaldırma sorusunu ele alır: servisler hangi sırayla başlar, uygulama onların hazır
olduğunu nasıl anlar ve aynı ortam iki makinede nasıl birebir aynı kurulur?
