C# Tips | ファイル・ディレクトリ操作:ファイル復元

C sharp C#
スポンサーリンク

はじめに 「ファイル復元」は“やらかした後に助けてくれる最後の味方”

バックアップを取るだけでは、まだ片手落ちです。
本当に大事なのは、「壊したあとに、元に戻せること」です。

設定ファイルを上書きしてアプリが起動しなくなった。
誤って最新データで古いファイルを潰してしまった。
テスト用の変更を本番ファイルに書き込んでしまった。

こういうときに、「バックアップから元に戻す」=「ファイル復元」ができるかどうかで、
その日のダメージが“冷や汗で済むか”“地獄になるか”が変わります。

ここでは、プログラミング初心者向けに、

単純なバックアップからの復元
タイムスタンプ付きバックアップからの復元
世代管理バックアップからの復元
復元時に絶対に意識すべき安全ポイント

を、例題付きでかみ砕いて説明していきます。


一番シンプルな「.bak から元に戻す」復元

単一ファイルの単純バックアップを前提にする

まずは、こんなバックアップを取っている前提から始めます。

元ファイル
C:\config\appsettings.json

バックアップ
C:\config\appsettings.bak

この場合、「復元」とは

appsettings.bakappsettings.json にコピーし直す

ことです。

単純復元ユーティリティの実装

using System;
using System.IO;

public static class RestoreUtil
{
    public static void RestoreFromSimpleBackup(string originalPath)
    {
        if (originalPath is null)
        {
            throw new ArgumentNullException(nameof(originalPath));
        }

        string directory = Path.GetDirectoryName(originalPath)
            ?? throw new ArgumentException("ディレクトリを含まないパスです。", nameof(originalPath));

        string fileNameWithoutExt = Path.GetFileNameWithoutExtension(originalPath);
        string backupPath = Path.Combine(directory, fileNameWithoutExt + ".bak");

        if (!File.Exists(backupPath))
        {
            throw new FileNotFoundException("バックアップファイルが見つかりません。", backupPath);
        }

        string backupCopyPath = originalPath + ".before_restore";

        if (File.Exists(originalPath))
        {
            File.Copy(originalPath, backupCopyPath, overwrite: true);
        }

        File.Copy(backupPath, originalPath, overwrite: true);
    }
}
C#

ここでの重要ポイントを丁寧に見ていきます。

まず、バックアップファイルのパスを「規約から計算」しています。
元ファイル名の拡張子を取って .bak を付ける というルールを、バックアップ作成側と復元側で共有しているイメージです。

次に、「復元前の元ファイル」をさらに退避しています。
originalPath + ".before_restore" にコピーしてから上書きしているのは、
「復元自体が間違っていたときに、復元前の状態に戻せるようにする」ためです。
復元処理も“壊す操作”なので、その前にもう一段バックアップを取っておく、という発想です。

使い方の例は次の通りです。

string original = @"C:\config\appsettings.json";

RestoreUtil.RestoreFromSimpleBackup(original);

Console.WriteLine("復元完了");
C#

タイムスタンプ付きバックアップからの復元

「どのバックアップから戻すか」を選ぶ必要がある

タイムスタンプ付きでバックアップを取っている場合を考えます。

例として、次のようなファイルがあるとします。

appsettings_20250128_200000.json.bak
appsettings_20250128_210000.json.bak
appsettings_20250128_220000.json.bak

この場合、「どれを使って復元するか」を決める必要があります。

一番よくあるのは、

最新のバックアップから復元する

というパターンです。

最新バックアップから復元するユーティリティ

using System;
using System.IO;
using System.Linq;

public static class TimestampRestoreUtil
{
    public static void RestoreFromLatestTimestampBackup(
        string originalPath,
        string? backupDirectory = null)
    {
        if (originalPath is null)
        {
            throw new ArgumentNullException(nameof(originalPath));
        }

        string sourceDirectory = backupDirectory
            ?? Path.GetDirectoryName(originalPath)
            ?? throw new ArgumentException("ディレクトリを含まないパスです。", nameof(originalPath));

        if (!Directory.Exists(sourceDirectory))
        {
            throw new DirectoryNotFoundException($"バックアップディレクトリが存在しません: {sourceDirectory}");
        }

        string fileNameWithoutExt = Path.GetFileNameWithoutExtension(originalPath);
        string extension = Path.GetExtension(originalPath);

        string searchPattern = $"{fileNameWithoutExt}_*{extension}.bak";

        var candidates = Directory.GetFiles(sourceDirectory, searchPattern, SearchOption.TopDirectoryOnly);

        if (candidates.Length == 0)
        {
            throw new FileNotFoundException("タイムスタンプ付きバックアップが見つかりません。", searchPattern);
        }

        string latestBackup = candidates
            .OrderBy(path => path)
            .Last();

        string backupCopyPath = originalPath + ".before_restore";

        if (File.Exists(originalPath))
        {
            File.Copy(originalPath, backupCopyPath, overwrite: true);
        }

        File.Copy(latestBackup, originalPath, overwrite: true);
    }
}
C#

ここでの重要ポイントは二つあります。

一つ目は、「バックアップファイルの検索パターン」です。
元ファイル名_*.拡張子.bak という規約でバックアップを作っている前提なので、
Directory.GetFiles にそのパターンを渡して候補を集めています。

二つ目は、「最新のバックアップの決め方」です。
タイムスタンプを yyyyMMdd_HHmmss 形式で付けているなら、
ファイル名を文字列としてソートしたときに、最後のものが最新になります。
その性質を利用して、OrderBy(path => path).Last() で最新を選んでいます。

使い方の例です。

string original = @"C:\config\appsettings.json";

TimestampRestoreUtil.RestoreFromLatestTimestampBackup(
    original,
    backupDirectory: @"C:\backup\config");

Console.WriteLine("最新バックアップから復元完了");
C#

世代管理バックアップからの復元

「世代番号」で管理している場合

世代管理バックアップで、例えば次のようなファイルを持っているとします。

appsettings.json.bak.1
appsettings.json.bak.2
appsettings.json.bak.3

この場合、「最新世代から戻す」「特定世代から戻す」の二つのパターンが考えられます。

最新世代から復元する

using System;
using System.IO;
using System.Linq;

public static class GenerationRestoreUtil
{
    public static void RestoreFromLatestGeneration(string originalPath)
    {
        if (originalPath is null)
        {
            throw new ArgumentNullException(nameof(originalPath));
        }

        string directory = Path.GetDirectoryName(originalPath)
            ?? throw new ArgumentException("ディレクトリを含まないパスです。", nameof(originalPath));

        string fileName = Path.GetFileName(originalPath);
        string pattern = fileName + ".bak.";

        var backups = Directory
            .GetFiles(directory, fileName + ".bak.*", SearchOption.TopDirectoryOnly)
            .Select(path => new
            {
                Path = path,
                Generation = ParseGenerationNumber(path, pattern)
            })
            .Where(x => x.Generation.HasValue)
            .OrderBy(x => x.Generation.Value)
            .ToList();

        if (!backups.Any())
        {
            throw new FileNotFoundException("世代管理バックアップが見つかりません。", fileName + ".bak.*");
        }

        var latest = backups.Last();

        string backupCopyPath = originalPath + ".before_restore";

        if (File.Exists(originalPath))
        {
            File.Copy(originalPath, backupCopyPath, overwrite: true);
        }

        File.Copy(latest.Path, originalPath, overwrite: true);
    }

    private static int? ParseGenerationNumber(string path, string pattern)
    {
        string fileName = Path.GetFileName(path);

        if (!fileName.StartsWith(pattern, StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        string suffix = fileName.Substring(pattern.Length);

        if (int.TryParse(suffix, out int gen))
        {
            return gen;
        }

        return null;
    }
}
C#

ここでは、「世代番号の最大値」を最新とみなして復元しています。
世代管理バックアップの作成側と同じロジック(ParseGenerationNumber)を使うことで、
命名ルールの解釈がズレないようにしています。

使い方の例です。

string original = @"C:\config\appsettings.json";

GenerationRestoreUtil.RestoreFromLatestGeneration(original);

Console.WriteLine("最新世代から復元完了");
C#

復元処理で絶対に意識してほしい安全ポイント

復元前に「今の状態」を必ず退避する

ここまでの例で共通してやっているのが、

復元前に、現在の元ファイルを別名でコピーしている

という点です。

originalPath + ".before_restore" のような名前で退避しているのは、
「復元自体が間違っていたときに、復元前の状態に戻せるようにする」ためです。

例えば、こういうケースがあります。

間違ったバックアップから復元してしまった。
バックアップファイル自体が壊れていた。
復元後にアプリが起動しなくなった。

このとき、「復元前の状態」が残っていれば、
そこに戻してから原因を調査できます。

復元処理は、「過去に戻す」という意味で強力な操作なので、
その前にもう一段“保険”をかけておく、という感覚を持っておくと安全です。

復元対象を間違えないための工夫

復元処理で怖いのは、「違うファイルに上書きしてしまう」ことです。

例えば、テスト環境と本番環境でパスが似ている場合、

C:\app\test\config\appsettings.json
C:\app\prod\config\appsettings.json

などを間違えると、取り返しがつかなくなります。

コード側でできる工夫としては、

復元対象のパスをログに必ず出す
バックアップ元と復元先のパスを画面やログで確認できるようにする
環境ごとにバックアップディレクトリを分ける

といったものがあります。

「人間が見て確認できる情報」を残しておくことが、
復元系の処理ではとても大事です。


まとめ 「ファイル復元ユーティリティ」は“バックアップとセットで初めて意味を持つ”

バックアップ作成とファイル復元は、必ずセットで考えるべきペアです。
片方だけあっても、実務では片手落ちになります。

押さえておきたいポイントを整理すると、こうなります。

単純な .bak バックアップなら、「規約からバックアップパスを計算してコピーし直す」だけで復元できる。
タイムスタンプ付きバックアップでは、「命名ルールに沿って候補を列挙し、文字列ソートで最新を選ぶ」というパターンが使える。
世代管理バックアップでは、「世代番号の最大値を最新とみなす」「必要なら特定世代を選べるようにする」といった設計ができる。
復元前に現在のファイルを別名で退避しておくことで、「復元ミスからさらに戻る」ための保険をかけられる。
復元対象のパスや使ったバックアップファイルをログに残し、「何をどう戻したか」が後から追えるようにしておくと、運用が格段に楽になる。

ここまで理解できれば、「バックアップはあるけど戻し方が分からない」状態から卒業して、
「壊しても、ちゃんと戻せる」前提で安心して変更できる C# ユーティリティを書けるようになっていきます。

タイトルとURLをコピーしました