Yükleniyor...

.NET'te Atomik Dosya Yazma: Yarım ve Bozuk Dosyaları Önleyin

İçeriği önce geçici dosyaya yazıp diske aktarın, ardından replace veya move kullanarak yarım ve bozuk dosya oluşma riskini azaltın.

Mevcut bir dosyanın üzerine doğrudan yazmak kolaydır; ancak uygulama çökerse, işlem sonlandırılırsa veya yazma sırasında bir giriş/çıkış hatası oluşursa dosya boş, eksik ya da bozuk kalabilir.

Daha güvenli yaklaşım atomik dosya yazma yöntemidir: İçeriğin tamamını önce geçici bir dosyaya yazın, tamponları diske aktarın ve hedef dosyayı yalnızca bu işlemler tamamlandıktan sonra değiştirin. Dosyayı okuyan uygulamalar yarım yazılmış bir sürüm yerine ya önceki eksiksiz dosyayı ya da yeni eksiksiz dosyayı görmelidir.


Amaç


Doğrudan Üzerine Yazmak Neden Risklidir?

Bir dosyaya doğrudan yazmak oldukça pratiktir:


await File.WriteAllTextAsync(
    "settings.json",
    json,
    cancellationToken);

Ancak mevcut bir dosyanın üzerine yazılırken dosya genellikle yeni içeriğin tamamı yazılmadan önce sıfırlanır. İşlem yazmanın ortasında durursa hedef dosya yalnızca yeni içeriğin bir bölümünü içerebilir veya tamamen kullanılamaz durumda kalabilir.

Bu durum özellikle yapılandırma dosyaları, dışa aktarılan raporlar, yerel veritabanları, uygulama durumu, önbelleğe alınmış API yanıtları ve her zaman geçerli bir söz dizimine sahip olması gereken JSON belgeleri için ciddi sorunlar oluşturabilir.


Atomik Yazma Akışı

  1. Hedef dosyanın yanında benzersiz isimli bir geçici dosya oluşturun.
  2. İçeriğin tamamını geçici dosyaya yazın.
  3. StreamWriter ve dosya sistemi tamponlarını boşaltın.
  4. Hedef dosya varsa File.Replace ile değiştirin.
  5. Hedef dosya yoksa geçici dosyayı son konumuna taşıyın.
  6. Yayımlama tamamlanmadan önce hata oluşursa geçici dosyayı silin.

Önemli: Geçici dosya bilinçli olarak hedef dosyanın bulunduğu klasörde oluşturulur. Her iki dosyanın aynı dosya sistemi biriminde bulunması, son değiştirme veya taşıma işleminin kopyala-sil yöntemine dönüşmek yerine dosya sisteminin rename/replace davranışını kullanmasını sağlar.


AtomicFileWriter Yardımcı Sınıfı

Aşağıdaki yardımcı sınıf, metni varsayılan olarak byte order mark içermeyen UTF-8 kodlamasıyla yazar. İptal desteği, isteğe bağlı yedekleme, diske flush işlemi ve geçici dosyaların temizlenmesi gibi özellikler sunar.


using System.Text;

public static class AtomicFileWriter
{
    public static async Task WriteAllTextAsync(
        string destinationPath,
        string content,
        Encoding? encoding = null,
        string? backupPath = null,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
        ArgumentNullException.ThrowIfNull(content);

        var fullDestinationPath = Path.GetFullPath(destinationPath);

        var directory = Path.GetDirectoryName(fullDestinationPath)
            ?? throw new InvalidOperationException(
                "Hedef klasör belirlenemedi.");

        Directory.CreateDirectory(directory);

        var destinationFileName = Path.GetFileName(fullDestinationPath);

        var temporaryPath = Path.Combine(
            directory,
            $".{destinationFileName}.{Guid.NewGuid():N}.tmp");

        var fullBackupPath = string.IsNullOrWhiteSpace(backupPath)
            ? null
            : Path.GetFullPath(backupPath);

        encoding ??= new UTF8Encoding(
            encoderShouldEmitUTF8Identifier: false);

        try
        {
            await using (var stream = new FileStream(
                temporaryPath,
                new FileStreamOptions
                {
                    Mode = FileMode.CreateNew,
                    Access = FileAccess.Write,
                    Share = FileShare.None,
                    BufferSize = 64 * 1024,
                    Options = FileOptions.Asynchronous
                }))
            {
                await using (var writer = new StreamWriter(
                    stream,
                    encoding,
                    bufferSize: 64 * 1024,
                    leaveOpen: true))
                {
                    await writer.WriteAsync(
                        content.AsMemory(),
                        cancellationToken);

                    await writer.FlushAsync();
                }

                cancellationToken.ThrowIfCancellationRequested();

                // Ara dosya sistemi tamponlarını diske aktarır.
                stream.Flush(flushToDisk: true);
            }

            cancellationToken.ThrowIfCancellationRequested();

            if (File.Exists(fullDestinationPath))
            {
                File.Replace(
                    sourceFileName: temporaryPath,
                    destinationFileName: fullDestinationPath,
                    destinationBackupFileName: fullBackupPath,
                    ignoreMetadataErrors: false);
            }
            else
            {
                File.Move(
                    sourceFileName: temporaryPath,
                    destFileName: fullDestinationPath);
            }
        }
        finally
        {
            // Başarılı bir replace veya move işleminden sonra
            // geçici dosya artık mevcut olmaz.
            // Yazma başarısız olursa veya iptal edilirse kalan dosya burada silinir.
            TryDelete(temporaryPath);
        }
    }

    private static void TryDelete(string path)
    {
        try
        {
            if (File.Exists(path))
                File.Delete(path);
        }
        catch
        {
            // Temizleme hatası asıl exception'ı gizlememelidir.
        }
    }
}

Neden Hem File.Replace Hem de File.Move Kullanılıyor?

File.Replace kullanılabilmesi için hedef dosyanın önceden mevcut olması gerekir. Bu metot, hedef dosyayı geçici dosyayla değiştirir ve istenirse önceki sürümü yedek dosya olarak saklar.

Hedef dosya ilk kez oluşturuluyorsa değiştirilecek mevcut bir dosya yoktur. Bu durumda File.Move, tamamlanan geçici dosyayı son adıyla yayımlar.


if (File.Exists(fullDestinationPath))
{
    File.Replace(
        temporaryPath,
        fullDestinationPath,
        fullBackupPath);
}
else
{
    File.Move(
        temporaryPath,
        fullDestinationPath);
}

Örnek: JSON Ayarlarını Güvenli Şekilde Kaydetme

Yardımcı sınıfı kullanarak JSON yapılandırma dosyasını, okuyucuların yarım yazılmış bir belgeyle karşılaşmasını önleyecek şekilde kaydedebilirsiniz.


using System.Text.Json;

public sealed record ApplicationSettings(
    string Theme,
    string Language,
    int PageSize);

var settings = new ApplicationSettings(
    Theme: "Dark",
    Language: "tr",
    PageSize: 25);

var json = JsonSerializer.Serialize(
    settings,
    new JsonSerializerOptions
    {
        WriteIndented = true
    });

await AtomicFileWriter.WriteAllTextAsync(
    destinationPath: "data/settings.json",
    content: json,
    backupPath: "data/settings.json.bak",
    cancellationToken: cancellationToken);

settings.json dosyası zaten varsa yeni dosya hedefin yerini almadan önce eski sürüm settings.json.bak adıyla saklanır.


Örnek: Yedek Oluşturmadan Kaydetme

Önceki sürümü saklamanız gerekmiyorsa yedek dosya yolu göndermeyebilirsiniz:


await AtomicFileWriter.WriteAllTextAsync(
    destinationPath: "exports/report.json",
    content: reportJson,
    cancellationToken: cancellationToken);

Atomiklik ve Kalıcılık Aynı Şey Değildir

Atomik değiştirme, dosyayı okuyanların ne göreceğine odaklanır: Okuyucular ya eski dosyanın tamamını ya da yeni dosyanın tamamını görür. Hedef dosyanın aşamalı olarak güncellenmesini engeller.

Kalıcılık ise verilerin işletim sistemi çökmesi veya ani elektrik kesintisi sonrasında korunup korunmayacağıyla ilgilidir. Flush(flushToDisk: true) çağrısı, işletim sisteminden ara tamponları diske yazmasını ister. Ancak nihai garantiler işletim sistemine, dosya sistemine, depolama aygıtına ve donanım yapılandırmasına bağlıdır.


Eşzamanlılık Ayrı Bir Konudur

Atomik değiştirme, birden fazla yazıcı işlemini otomatik olarak koordine etmez. İki işlem aynı hedef dosyaya aynı anda yazarsa her ikisi de kendi geçici dosyasını oluşturabilir ve sonuçlarını yayımlamak için yarışabilir.

Birden fazla yazıcının bulunduğu uygulamalarda ek bir koordinasyon mekanizması kullanılmalıdır:


Yaygın İyileştirmeler


TL;DR

  • Önemli dosyaların üzerine doğrudan yazmayın.
  • İçeriğin tamamını önce hedef klasördeki geçici bir dosyaya yazın.
  • Geçici dosyayı yayımlamadan önce tamponları diske aktarın.
  • Mevcut hedef için File.Replace, yeni dosya için File.Move kullanın.
  • Atomik değiştirme işleminin birden fazla yazıcı arasındaki koordinasyonu tek başına çözmediğini unutmayın.