İçeriğe geç
academia.sh

Ders 15 / 16

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.

İçindekiler

Ö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.

// 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.

// 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");
#!/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?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat