【C#】ファイルを別の場所に書き出してから保存する

既存のファイルに内容を書きこむ時に、直接対象のファイルを開いて書き込みを行うとアプリが強制終了するなどでストリームが異常終了するとファイルの内容が破損する場合があります。この問題を避けるためには以下のアプローチが必要です。

  • 直接ファイルを開かない
  • 別の場所に書き出してからファイルを保存先に移動する

ですが、この処理を毎回書くのは多少面倒なので、簡単に上記動作を実行できる Utility を作成してみました。

using System;
using System.IO;

public static class FileUtility
{
    // ファイルをいったん一時領域に出力してから保存する
    public static void SaveNew(string savePath, Action<string> saveAction)
    {
        string tempPath = "";
        try
        {
            tempPath = GetUniquePath();
            saveAction(tempPath);
            Microsoft.VisualBasic.FileIO.FileSystem.MoveFile(tempPath, savePath, true);
        }
        finally
        {
            if (File.Exists(tempPath)) // ゴミが残らないようにする
            {
                File.Delete(tempPath);
            }
        }
    }

    // 既存のファイルに内容を追記する
    // ** 何百MBもあるファイルに対してこのメソッドを使うとかなり重い
    public static void Append(string filePath, Action<string> appendAction)
    {
        if (!File.Exists(filePath))
        {
            SaveNew(filePath, appendAction);
        }
        else
        {
            string tempPath = "";
            try
            {
                tempPath = GetUniquePath();
                File.Copy(filePath, tempPath, true);
                appendAction(tempPath);
                Microsoft.VisualBasic.FileIO.FileSystem.MoveFile(tempPath, filePath, true);
            }
            finally
            {
                if (File.Exists(tempPath))
                {
                    File.Delete(tempPath);
                }
            }
        }
    }

    // ユニークな一時ファイルパスを取得する
    public static string GetUniquePath()
    {
        string filePath;
        do
        {
            filePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".tmp");
        }
        while (File.Exists(filePath)); // 使用可能なパスを取得できるまで繰り返す
        return filePath;
    }
}

使い方は以下の通りです。

// Program.cs

using System;
using System.IO;
using System.Text;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");

        // ラムダ式で処理を指定
        FileUtility.Save(@"d:\sample1.txt",
            tmpPath =>
            {
                var sw = new StreamWriter(tmpPath, false, Encoding.UTF8);
                sw.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
            });

        // メソッドを指定(★推奨)
        FileUtility.Save(@"d:\sample2.txt", Save);
    }

    // ファイルパスを受け取ってそこに内容を保存する処理
    private static void Save(string filePath)
    {
        var sw = new StreamWriter(filePath, false, Encoding.UTF8);
        sw.WriteLine(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
    }
}

これでファイルを開きっぱなしでアプリがクラッシュしてデータが全部消えるという状態を少しは避けることができると思います。