はじめに 「世代管理バックアップ」は“時間を巻き戻すための仕組み”
バックアップを「とりあえずコピーしておく」だけで終わらせると、すぐにこうなります。
バックアップフォルダがファイルだらけでカオスになる。
どれが最新で、どれが古いのか分からない。
ディスクがいっぱいになってサーバーが止まる。
そこで出てくるのが「世代管理バックアップ」です。
これは一言でいうと、
「バックアップを世代(バージョン)として管理し、古いものから順に自動で捨てていく仕組み」
です。
例えば、「最新 5 世代だけ残す」「7 日分だけ残す」といったルールを決めて、
それをユーティリティとしてコードにしておくと、運用がものすごく楽になります。
ここでは、プログラミング初心者向けに、
世代管理バックアップの考え方
ファイル単位の世代管理
フォルダ単位の世代管理
日付・時刻と世代数の組み合わせ
実務での設計ポイント
を、例題付きでかみ砕いて説明していきます。
世代管理バックアップの基本発想
「残す数を決めて、古いものから捨てる」
世代管理のコアはとてもシンプルです。
バックアップを取るたびに新しい世代を追加する。
世代数が上限を超えたら、一番古いものから削除する。
例えば、「最大 5 世代」と決めた場合のイメージはこうです。
1 回目のバックアップ → 世代 1
2 回目 → 世代 1, 2
3 回目 → 世代 1, 2, 3
…
6 回目 → 世代 2, 3, 4, 5, 6(1 は削除)
この「古いものから順に消す」を、毎回のバックアップ処理の中に組み込んでおくのがポイントです。
「名前で管理する」か「フォルダで管理する」か
世代管理には大きく分けて二つのパターンがあります。
同じフォルダに、世代を表す名前のファイルを並べる。
専用のバックアップフォルダの中に、世代ごとのサブフォルダを作る。
ここではまず「ファイル名で世代を表す」パターンから入り、
次に「フォルダごと世代管理する」パターンを見ていきます。
ファイル単位の世代管理バックアップ
命名ルールを決める
まずは、バックアップファイルの名前のルールを決めます。
分かりやすいのは、次のような形式です。
config.json のバックアップを、同じフォルダに
config.json.bak.1config.json.bak.2config.json.bak.3
のように置いていくパターンです。
数字が大きいほど新しい世代、と決めておきます。
実装の全体像
やりたいことはこうです。
元ファイルをコピーして、新しい世代(例: .bak.1)を作る。
既存のバックアップファイルを列挙して、古い順に並べる。
上限世代数を超えている分を削除する。
これを 1 本のユーティリティメソッドにまとめます。
using System;
using System.IO;
using System.Linq;
public static class GenerationBackup
{
public static string CreateFileGenerationBackup(
string originalPath,
int maxGenerations = 5)
{
if (originalPath is null)
{
throw new ArgumentNullException(nameof(originalPath));
}
if (!File.Exists(originalPath))
{
throw new FileNotFoundException("バックアップ元のファイルが存在しません。", originalPath);
}
if (maxGenerations <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxGenerations), "1 以上を指定してください。");
}
string directory = Path.GetDirectoryName(originalPath)
?? throw new ArgumentException("ディレクトリを含まないパスです。", nameof(originalPath));
string fileName = Path.GetFileName(originalPath);
string pattern = fileName + ".bak.";
var existingBackups = 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();
int newGeneration = existingBackups.Any()
? existingBackups.Max(x => x.Generation!.Value) + 1
: 1;
string newBackupName = fileName + ".bak." + newGeneration;
string newBackupPath = Path.Combine(directory, newBackupName);
File.Copy(originalPath, newBackupPath, overwrite: false);
var toDelete = existingBackups
.Where(x => x.Generation <= newGeneration - maxGenerations)
.ToList();
foreach (var item in toDelete)
{
try
{
File.Delete(item.Path);
}
catch
{
}
}
return newBackupPath;
}
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#使い方の例
string original = @"C:\config\appsettings.json";
string backup1 = GenerationBackup.CreateFileGenerationBackup(original, maxGenerations: 3);
Console.WriteLine(backup1);
string backup2 = GenerationBackup.CreateFileGenerationBackup(original, maxGenerations: 3);
Console.WriteLine(backup2);
string backup3 = GenerationBackup.CreateFileGenerationBackup(original, maxGenerations: 3);
Console.WriteLine(backup3);
string backup4 = GenerationBackup.CreateFileGenerationBackup(original, maxGenerations: 3);
Console.WriteLine(backup4);
C#このとき、フォルダの中身は最終的に
appsettings.jsonappsettings.json.bak.2appsettings.json.bak.3appsettings.json.bak.4
のようになります。.bak.1 は、4 世代目を作るタイミングで削除されています。
重要ポイント① 既存バックアップの「世代番号」を解析する
ファイル名から世代番号を取り出す
ParseGenerationNumber がやっていることを丁寧に見ていきます。
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#例えば、元ファイルが appsettings.json の場合、pattern は
"appsettings.json.bak."
になります。
appsettings.json.bak.3 というファイル名なら、
StartsWith(pattern) が truepattern の長さ以降の文字列は "3"int.TryParse("3") に成功して、世代番号 3 が取れる
という流れです。
逆に、appsettings.json.bak.old のような名前は、"old" が数字に変換できないので、世代番号なし(null)として扱われます。
このように、「自分が決めた命名ルールに合うものだけを世代管理の対象にする」ことが大事です。
重要ポイント② 削除対象の判定ロジック
「新しい世代番号 − maxGenerations」より小さいものを消す
削除対象の判定はここです。
var toDelete = existingBackups
.Where(x => x.Generation <= newGeneration - maxGenerations)
.ToList();
C#例えば、maxGenerations = 3 で、新しい世代番号が 4 の場合、
newGeneration - maxGenerations = 1
となります。
つまり、「世代番号が 1 以下のものを削除する」という意味になります。
結果として、2, 3, 4 の 3 世代が残るわけです。
このロジックを変えれば、「最新だけ残す」「偶数世代だけ残す」などもできますが、
まずは「連番で古いものから消す」という素直な形を押さえておくのがよいです。
フォルダ単位の世代管理バックアップ
「バックアップフォルダの中に世代ごとのサブフォルダを作る」
設定一式やデータ一式をフォルダごとバックアップしたい場合は、
「バックアップルートフォルダの中に、世代ごとのサブフォルダを作る」パターンが分かりやすいです。
例えば、バックアップルートが C:\backup\data の場合、
C:\backup\data\001C:\backup\data\002C:\backup\data\003
のように、数字フォルダを世代として扱います。
実装例
フォルダコピー用のユーティリティを使って、世代管理を組み合わせます。
using System;
using System.IO;
using System.Linq;
public static class DirectoryGenerationBackup
{
public static string CreateDirectoryGenerationBackup(
string sourceDirectory,
string backupRootDirectory,
int maxGenerations = 5)
{
if (!Directory.Exists(sourceDirectory))
{
throw new DirectoryNotFoundException($"バックアップ元ディレクトリが存在しません: {sourceDirectory}");
}
if (maxGenerations <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxGenerations), "1 以上を指定してください。");
}
Directory.CreateDirectory(backupRootDirectory);
var existingGenerations = Directory
.GetDirectories(backupRootDirectory)
.Select(dir => new
{
Path = dir,
Generation = ParseGenerationFromDirectoryName(dir)
})
.Where(x => x.Generation.HasValue)
.OrderBy(x => x.Generation.Value)
.ToList();
int newGeneration = existingGenerations.Any()
? existingGenerations.Max(x => x.Generation!.Value) + 1
: 1;
string newDirName = newGeneration.ToString("D3");
string newBackupDir = Path.Combine(backupRootDirectory, newDirName);
CopyDirectoryRecursive(sourceDirectory, newBackupDir);
var toDelete = existingGenerations
.Where(x => x.Generation <= newGeneration - maxGenerations)
.ToList();
foreach (var item in toDelete)
{
try
{
Directory.Delete(item.Path, recursive: true);
}
catch
{
}
}
return newBackupDir;
}
private static int? ParseGenerationFromDirectoryName(string dirPath)
{
string name = Path.GetFileName(dirPath);
if (int.TryParse(name, out int gen))
{
return gen;
}
return null;
}
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);
foreach (string filePath in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(filePath);
string destFilePath = Path.Combine(destinationDir, fileName);
File.Copy(filePath, destFilePath, overwrite: true);
}
foreach (string subDir in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(subDir);
string destSubDir = Path.Combine(destinationDir, dirName);
CopyDirectoryRecursive(subDir, destSubDir);
}
}
}
C#使い方の例
string sourceDir = @"C:\app\data";
string backupRoot = @"C:\backup\data";
string backupDir = DirectoryGenerationBackup.CreateDirectoryGenerationBackup(
sourceDir,
backupRoot,
maxGenerations: 3);
Console.WriteLine($"バックアップ作成: {backupDir}");
C#この場合、C:\backup\data の中には常に最新 3 世代だけが残るようになります。
日付・時刻と世代数を組み合わせる
「フォルダ名は日付+連番」にする
もう一歩だけ発展させると、「フォルダ名に日付と連番を組み合わせる」というパターンもあります。
例えば、
20250128_00120250128_00220250129_001
のような形です。
こうしておくと、「いつのバックアップか」と「その日の何回目か」が一目で分かります。
実装としては、フォルダ名の生成ロジックを
newGeneration.ToString("D3")
の代わりに
DateTime.Now.ToString("yyyyMMdd") + "_" + dailyIndex.ToString("D3")
のように変えるイメージです。
ここは少し複雑になるので、まずは「単純な連番世代」をしっかり押さえてからで十分です。
実務での設計ポイント
「何世代残すか」はビジネス側と決める
世代数は、技術的な問題というより「ビジネス上の要件」です。
どこまで遡って復元できれば安心か。
ディスク容量はどれくらい使えるか。
バックアップ 1 世代あたりのサイズはどれくらいか。
例えば、「設定ファイルなら 5 世代で十分」「重要データなら 30 世代欲しい」など、
対象によって変えるのもよくあります。
コード側では、「世代数を引数や設定で変えられるようにしておく」ことが大事です。
「失敗したときにどうするか」を決めておく
世代管理バックアップは、次のような理由で失敗することがあります。
ディスク容量不足
権限不足
ファイルロック
そのときに、
バックアップ失敗をログに残して本処理は続行するのか。
バックアップが取れないなら本処理も中止するのか。
を決めておく必要があります。
特に「上書き前バックアップ」と組み合わせる場合は、
「バックアップが取れないなら上書きしない」という方針のほうが安全です。
まとめ 「世代管理バックアップ」は“過去の自分を守るための保険”
世代管理バックアップは、単なるコピーではなく、
「いつでも、少し前の状態に戻れるようにしておく」
ための仕組みです。
押さえておきたいポイントを整理すると、こうなります。
バックアップを「世代」として扱い、上限数を超えたら古いものから自動で削除する。
ファイル単位なら、元ファイル名 + ".bak.n" のような命名ルールで世代番号を管理できる。
フォルダ単位なら、バックアップルートの中に世代ごとのサブフォルダを作り、連番や日付で管理する。
世代数はコードに固定せず、引数や設定で変えられるようにしておくと、ビジネス要件に合わせやすい。
失敗時の扱い(ログだけか、中止か)まで含めて設計すると、“業務で本当に使える世代管理バックアップ”になる。
ここまで理解できれば、「とりあえずコピーしておく」段階から一歩進んで、
「時間を巻き戻せる前提で安心して変更できる」環境を、自分の C# ユーティリティで作れるようになります。
