Node.js fs Modülü
Node.js'in dosya sistemi işlemlerini gerçekleştirmek için temel aracı, yerleşik (built-in) `fs` modülüdür. Bu modül, dosya okuma, yazma, silme, yeniden adlandırma gibi temel operasyonlardan, dizin oluşturma ve dosya meta verilerini sorgulamaya kadar geniş bir yelpazede senkron ve asenkron API'ler sunar. Kullanmak için herhangi bir ek kuruluma gerek yoktur; doğrudan `require` ifadesi ile uygulamaya dahil edilebilir. Bu, geliştiriciye güçlü ve doğrudan bir erişim sağlar.
Modülün yapısı, hem Callback tabanlı hem de Promise tabanlı yöntemleri destekleyecek şekilde evrim geçirmiştir. Özellikle Node.js sürüm 10'dan itibaren geln `fs/promises` API'si, modern asenkron kod yazımını kolaylaştırmaktadır. Bu gelişmeler, kodun okunabilirliğini ve bakımını önemli ölçüde iyileştirmiştir.
| API Stili | Metod Örneği | Avantajı |
|---|---|---|
| Callback | fs.readFile('dosya.txt', callback) |
Tüm Node.js sürümlerinde uyumlu |
| Promise (fs/promises) | fsPromises.readFile('dosya.txt') |
Async/await ile modern, temiz kod |
| Senkron | fs.readFileSync('dosya.txt') |
Basit script'ler için uygun |
`fs` modülünün sağladığı fonksiyonlar, genellikle işletim sisteminin sunduğu temel dosya işlemlerini yansıtır. Bu nedenle, platformlar arasında tutarlı bir davranış beklenir, ancak bazı gelişmiş izinler veya sembolik linkler gibi konularda farklılıklar olabilir. Modülün dokümantasyonu bu tür incelikleri detaylıca açıklar.
Modülün kullanımı, genellikle bir dosya yolu belirterek başlar. Yolların belirlenmesinde `path` modülü ile birlikte kullanım oldukça yaygındır. Bu kombinasyon, platform bağımsız ve güvenli dosya erişimi için kritik bir öneme sahiptir.
Senkron vs Asenkron
Node.js'de dosya işlemlerinde en kritik tasarım kararlarından biri, senkron (bloklayıcı) ve asenkron (bloklamayan) yöntemler arasındaki seçimdir. Asenkron işlemler, Node.js'in olay döngüsü (event loop) mimarisinin kalbinde yer alır. Bir dosya okuma işlemi başlatıldığında, sistem çağrısı işletim sistemine devredilir ve Node.js iş parçacığı (thread) bir sonraki işleme geçebilir. Bu, yüksek eşzamanlılık (concurrency) sağlar.
Senkron yöntemler ise, adından da anlaşılacağı gibi, işlem tamamlanana kadar ana iş parçacığını bloke eder. Bu, sunucu uygulamalarında tüm gelen isteklerin donmasına neden olabileceğinden genellikle kaçınılması gereken bir yaklaşımdır. Ancak, uygulamanın başlangıç aşamasında yapılandırma dosyalarını yüklemek veya basit komut satırı araçları (CLI) geliştirmek gibi senaryolarda kullanışlı olabilir.
- Asenkron Kullanım:
fs.readFile('veri.json', 'utf8', (err, data) => { ... }) - Senkron Kullanım:
const data = fs.readFileSync('veri.json', 'utf8'); - Modern Asenkron Kullanım (Promise):
const data = await fsPromises.readFile('veri.json', 'utf8');
| Kriter | Asenkron İşlemler | Senkron İşlemler |
|---|---|---|
| Performans | Yüksek eşzamanlılık, ölçeklenebilirlik | Düşük, ana thread'i bloklar |
| Kod Karmaşıklığı | Callback hell riski (Promise/async ile çözülür) | Doğrusal, sade akış |
| Kullanım Yeri | Web sunucuları, API'ler, gerçek zamanlı uygulamalar | Başlangıç script'leri, CLI araçları, basit batch işler |
| Hata Yönetimi | Callback'de ilk parametre veya try/catch (async/await) | Try/catch bloğu içinde |
Asenkron programlamanın ilk versiyonları callback fonksiyonlarına dayanıyordu, ancak bu, "callback hell" olarak bilinen iç içe geçmiş ve okunması zor kod yapılarına yol açabiliyordu. Promise API'si ve `async/await` sözdiziminin gelişi, asenkron kodun sanki senkronmuş gibi yazılabilmesine olanak tanıdı. Bu, hata yönetimini (try...catch ile) büyük ölçüde basitleştirdi ve kodun anlaşılırlığını artırdı. Bu nedenle, modern Node.js uygulamalarında fs/promises API'sinin tercih edilmesi önemli bir best practice'dir.
Dosya Okuma ve Yazma
Dosya okuma ve yazma işlemleri, `fs` modülünün en temel fonksiyonlarını oluşturur. Okuma işlemleri için `readFile` (asenkron) ve `readFileSync` (senkron) metodları yaygın olarak kullanılır. Bu metodlar, dosyanın tamamını tek seferde belleğe yükler, bu da orta büyüklükteki dosyalar için pratiktir. Ancak, çok büyük dosyalarla çalışırken bu yaklaşım bellek tükenmesine neden olabilir. Bu gibi durumlarda, dosyayı daha küçük parçalar halinde okumaya olanak tanıyan akış (stream) tabanlı yöntemler veya `open`, `read`, `close` metodlarıyla düşük seviyeli okuma tercih edilmelidir.
Yazma işlemleri de benzer şekilde `writeFile` ve `appendFile` metodları ile gerçekleştirilir. `writeFile` hedef dosyayı tamamen yeniden yazar veya oluştururken, `appendFile` mevcut içeriğin sonuna yeni veri ekler. Her iki metod da varsayılan olarak dosyayı oluşturur veya üzerine yazar. Dosyanın önce var olup olmadığını kontrol etmek genellikle gereksizdir, çünkü bu işlemler zaten bu durumu halleder. Ancak, kaybı kabul edilemez veriler için dikkatli olunmalıdır.
Dosya kodlamaları (encoding), özellikle metin dosyaları ile çalışırken hayati öneme sahiptir. `'utf8'` parametresi, Türkçe karakterler de dahil olmak üzere Unicode metinleri doğru şekilde işlemek için standart seçimdir. İkili (binary) dosyalarla (resim, video) çalışırken ise encoding belirtilmez, böylece metod bir Buffer nesnesi döndürür. Buffer, Node.js'de ham ikili veriyi temsil etmenin temel yoludur ve yüksek performanslı veri işleme sağlar.
const fs = require('fs').promises;
async function dosyaIslemleri() {
try {
// Dosya yazma
await fs.writeFile('log.txt', 'Yeni giriş\n', { flag: 'a' }); // Ekleme modu
console.log('Dosyaya yazıldı.');
// Dosya okuma
const icerik = await fs.readFile('log.txt', 'utf8');
console.log('Okunan içerik:', icerik);
} catch (err) {
console.error('Dosya işleminde hata:', err);
}
}
dosyaIslemleri();
Promise tabanlı API ile yazılan yukarıdaki örnek, modern dosya işlemlerinin ne kadar temiz ve okunabilir olduğunu göstermektedir. `async/await` yapısı, asenkron operasyonları senkron bir akış gibi yönetmemizi sağlar. Hata yönetimi için tek bir `try...catch` bloğu yeterlidir. Bu yaklaşım, güvenilir ve sürdürülebilir uygulamalar geliştirmenin anahtarıdır.
Büyük veri kümeleri ile çalışırken, dosya akışları (streams) kullanmak performans ve bellek verimliliği açısından çok daha iyidir. `fs.createReadStream` ve `fs.createWriteStream` metodları, dosyaları parça parça işleyerek tüm dosyanın belleğe yüklenmesini engeller. Bu, log dosylarını işlemek, büyük medya dosyalarını aktarmak veya gerçek zamanlı veri işleme pipeline'ları oluşturmak için idealdir.
Dosya ve Dizin Yönetimi
Modern uygulamalar sadece dosya içeriği ile değil, aynı zamanda dosya ve dizinlerin kendileriyle de etkileşime girer. `fs` modülü, bu nesneleri yönetmek için kapsamlı bir araç seti sunar. Dizin oluşturmak için `mkdir` veya `mkdirSync`, bir dizinin içeriğini listelemek için `readdir` fonksiyonları kullanılır. `readdir` ile alınan liste, sadece dosya ve dizin isimlerini içerebileceği gibi, `withFileTypes: true` seçeneği ile daha detaylı `Dirent` nesneleri de döndürebilir.
Dosya ve dizinlerin meta verilerine erişim, `stat` veya `lstat` fonksiyonları ile sağlanır. Bu fonksiyonlar, boyut, oluşturulma ve değiştirilme tarihleri (mtime, ctime), ve önemlisi dosya türü (dosya mı, dizin mi, sembolik link mi) gibi bilgileri içeren bir `Stats` nesnesi döndürür. `lstat` ise sembolik link'in kendisi hakkında bilgi verirken, `stat` link'in hedefi hakkında bilgi verir. Bu ayrım, sembolik link'lerin doğru yönetilmesi için kritiktir.
- Dizin Oluşturma:
fs.mkdir('yeni-klasor', { recursive: true }) - İçerik Listeleme:
fs.readdir('./', { withFileTypes: true }) - Meta Veri Alma:
const stats = await fs.stat('dosya.js') - Yeniden Adlandırma/Taşıma:
fs.rename('eski.txt', 'yeni.txt') - Silme:
fs.unlink('dosya.txt')(dosya),fs.rmdir('bos-klasor')(dizin)
Node.js sürüm 14.14.0'dan itibaren, özyinelemeli (recursive) dizin silme işlemi için çok daha güvenli ve kullanışlı bir metod olan `fs.rm` eklenmiştir. `{ recursive: true, force: true }` seçenekleri ile, içi dolu dizinleri ve var olmayan yolları hatasız bir şekilde silmeye olanak tanır. Bu, eski `rimraf` gibi üçüncü parti paketlere olan bağımlılığı büyük ölçüde azaltmıştır ve resmi, standart bir çözüm sunar.
Dosya yolları ile çalışırken mutlak (absolute) ve göreli (relative) yol kavramları önemlidir. `__dirname` ve `__filename` global değişkenleri, mevcut modülün bulunduğu dizin ve dosya yolunu verir. Bu değerler, göreli yollar oluşturmak için sıklıkla `path.join()` ile birlikte kullanılır. Bu kombinasyon, farklı işletim sistemlarında (Windows'taki `\` ve Unix'teki `/` ayrımı) sorunsuz çalışan platform-bağımsız kod yazmayı garanti eder. Yol birleştirme işlemlerini asla string concatenation (`+`) ile yapmamak gerekir.
İzinler (permissions) ve kullanıcı/group ID'leri gibi gelişmiş dosya sistemi özellikleri de `fs` modülü tarafından desteklenir. `chmod`, `chown` gibi fonksiyonlar bu işlemler için kullanılabilir. Ancak, bu işlemlerin çoğu platforma özgü davranışlar sergileyebilir ve genellikle uygulamanın çalıştığı prosesin yeterli sistem izinlerine sahip olmasını gerektirir. Sunucu güvenliği açısından, dosya işlemlerinin çalıştığı ortamın izin yapılandırması dikkatle incenmelidir.
İzleme ve Performans
Node.js, dosya sistemi üzerindeki değişiklikleri gerçek zamanlı olarak izlemek için güçlü bir mekanizma sunar: `fs.watch()` ve `fs.watchFile()` API'leri. Bu fonksiyonlar, bir dosya veya dizindeki değişiklikleri (oluşturma, düzenleme, silme, yeniden adlandırma) dinleyerek olay tabanlı (event-driven) programlamanın gücünü dosya sistmine taşır. `fs.watch()` tercih edilen yöntemdir, çünkü işletim sisteminin yerel dosya değişim bildirim sistemlerini (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) kullanır ve daha verimli, daha düşük gecikmeli olaylar sağlar.
Ancak, `fs.watch()` API'si platformlar arasında tutarsız davranışlar sergileyebilir ve bazı durumlarda beklenmeyen olaylar (`rename`) üretebilir. Bu nedenle, üretim ortamlarında bu API'yi sarmalayan ve tutarlılık sağlayan `chokidar` gibi üçüncü parti modüller sıklıkla tercih edilir. Performans açısından, özellikle çok sayıda dosyayı izlerken, izleyici başına belirli bir kaynak tüketimi olduğu unutulmamalıdır. Gereksiz izleyicilerin kapatılması (`watcher.close()`) bellek sızıntılarını önlemek için kritik bir uygulama yönetimi adımıdır.
Dosya sistemi performansı, Node.js uygulamalarının ölçeklenebilirliğini doğrudan etkiler. Senkron (bloklayıcı) çağrıların kullanımı, yukarıda belirtildiği gibi, olay döngüsünü engelleyerek uygulamanın tüm yanıt verebilirliğini düşürebilir. Büyük dosya işlemleri için akışların (streams) kullanılması, bellek baskısını azaltır ve işlemi daha küçük, yönetilebilir parçalara böler. Ek olarak, sık erişilen dosya yollarının önbelleğe alınması (cache'lenmesi) ve tekrarlanan `stat` çağrılarından kaçınılması performansı artıracaktır.
Dosya işlemlerinde gecikme (latency), temelde altta yatan depolama aygıtının (HDD, SSD) hızına ve dosya sistemine bağlıdır. Node.js'in asenkron yapısı, bu gecikmeler sırasında CPU'nun diğer işleri yapmasına izin vererek genel verimi maksimize eder. Ancak, yoğun I/O iş yüklerinde, Worker Threads veya işi birden fazla proses arasında dağıtmak için cluster modülü kullanılabilir. Bu, özellikle CPU yoğun dosya şifreleme/sıkıştırma işlemleri ile birlikte kullanıldığında performans kazancı sağlayabilir.
const fs = require('fs');
const path = './izlenecek-dizin';
// fs.watch ile dizin izleme
const watcher = fs.watch(path, { recursive: true }, (eventType, filename) => {
if (filename) {
console.log(`Olay Türü: ${eventType}, Dosya: ${filename}`);
// Dosya silindi ise izleyiciyi kapatabiliriz.
if (eventType === 'rename') {
// İşlemler...
}
}
});
// Uygulama kapanırken izleyiciyi kapat
process.on('SIGINT', () => {
console.log('İzleyici kapatılıyor...');
watcher.close();
});
Güvenlik ve Hata Yönetimi
Dosya sistemi işlemleri, uygulama güvenliğinde en hassas noktalardan biridir. Yol traversal (Directory Traversal) saldırıları, kullanıcı girdisinden alınan dosya yolu parçalarının güvenilmez bir şekilde birleştirilmesi sonucu ortaya çıkar. Saldırgan, `../../etc/passwd` gibi göreli yollar kullanarak uygulamanın erişmemesi gereken dizinlere sızabilir. Bu riski önlemek için, kullanıcıdan gelen tüm yol girdileri mutlaka normalize edilmeli ve sanitize edilmelidir. `path.normalize()` fonksiyonu temel koruma sağlar, ancak güvenli bir kök dizin (`process.cwd()` veya `__dirname` gibi) içine hapsetmek için `path.resolve()` veya `path.join()` ile birlikte kullanmak ve sonucun bu kök dizin altında olduğunu kontrol etmek daha sağlam bir çözümdür.
Hata yönetimi, dosya işlemlerinin güvenilirliğinin temel taşıdır. Asenkron callback yönteminde hatalar ilk parametre olarak gelirken, Promise tabanlı yöntemlerde `try...catch` bloğu ile yakalanır. Yaygın hatalar arasında `ENOENT` (dosya veya dizin bulunamadı), `EACCES` (izin reddedildi), `EEXIST` (dosya/dizin zaten var) ve `ENOSPC` (disk dolu) bulunur. Bu hatalar sadece loglanmamalı, aynı zamanda uygulama mantığına uygun şekilde ele alınmalıdır. Örneğin, bir dosya bulnamadığında varsayılan bir dosya oluşturmak veya kullanıcıya anlamlı bir mesaj döndürmek gerekebilir.
Race condition (yarış durumu) riski de göz ardı edilmemelidir. Bir dosyanın varlığını `fs.exists` veya `fs.access` ile kontrol edip hemen ardından işlem yapmak, kontrol ile işlem arasında geçen mikro saniyelerde dosyanın durumunun değişebileceği riskini taşır. Bu nedenle, `fs.exists` metodunun kullanımı artık tavsiye edilmez. Bunun yerine, yapılacak işlemi (okuma, yazma) doğrudan gerçekleştirip ortaya çıkabilecek `ENOENT` hatasını yakalamak daha güvenli bir paradigmadır. Bu yaklaşım, dosya sisteminin durumu ile ilgili varsayımlardan kaçınarak daha sağlam bir uygulama akışı oluşturur. Tüm bu ilkeler, Node.js ile dosya sistemi etkileşimlerinin güvenli, verimli ve bakımı kolay olmasını sağlamak için esastır.