ソースコードはこちら
https://github.com/mi3w2a1/incremental-backup
前回は設定だけで実際にバックアップをする処理はしませんでした。今回は実際に指定されたフォルダにバックアップします。
仕様ですが、バックアップ先フォルダのなかに現在時刻を利用して新しいフォルダを作成します。そしてそこにバックアップ元フォルダ内のフォルダとファイルをバックアップします。
バックアップするだけなら以下のコードでよさそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void FullBackup(string sourceFolder, string targetFolder) { string[] filePaths = Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories); string[] folderPaths = Directory.GetDirectories(sourceFolder, "*.*", SearchOption.AllDirectories); // 現在時刻からフォルダを作成する string targetFolder1 = targetFolder + "\\" + DateTime.Now.ToString("yyyy-MMdd-HHmmss"); // フォルダを作成する if(!Directory.Exists(targetFolder1)) Directory.CreateDirectory(targetFolder1); // 先にフォルダをバックアップする(フォルダがないとファイルをコピーできない) foreach(string path in folderPaths) { string destfolderPath = path.Replace(sourceFolder, targetFolder1); Directory.CreateDirectory(destfolderPath); } // そのあとファイルをバックアップする foreach(string path in filePaths) { string destFilePath = path.Replace(sourceFolder, targetFolder1); File.Copy(path, destFilePath); } } |
しかし今回は増分バックアップなので初回はフルバックアップでよいのですが、次回以降は前回の実行時には存在しなかったファイル、更新されているファイルだけを選ぶ必要があります。そこで以下のようなコードはどうでしょうか?
GetSourceFolderPath()メソッドとGetTargetFolderPath()メソッドでバックアップに関するフォルダのパスを取得します。このときリストビューコントロールで選択されている項目が存在しないと処理はおこなわれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public partial class Form1 : Form { private void StartBackupMenuItem_Click(object sender, EventArgs e) { string sourceFolder = GetSourceFolderPath(); string targetFolder = GetTargetFolderPath(); if(Backup(sourceFolder, targetFolder)) MessageBox.Show("終了"); } string GetSourceFolderPath() { var indexes = listView1.SelectedIndices; if(indexes.Count == 1) return listView1.Items[indexes[0]].SubItems[0].Text; else return ""; } string GetTargetFolderPath() { var indexes = listView1.SelectedIndices; if(indexes.Count == 1) return listView1.Items[indexes[0]].SubItems[1].Text; else return ""; } } |
バックアップ先フォルダは現在時刻を利用して作成するので、そのためのメソッドを示します。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { string GetFolderPathFromTicks(long ticks) { DateTime dt = new DateTime(ticks); return GetTargetFolderPath() + "\\" + dt.ToString("yyyy-MMdd-HHmmss"); } } |
増分バックアップでは実際にファイルやフォルダをバックアップする必要があるかどうかを判断しなければなりません。そこでバックアップをしたらファイルのパスと更新時刻をログとして保存します。バックアップを実行するときは前回分のログがあるかどうかを確認します。そのためのメソッドがGetLastLogFilePath(string targetFolder)メソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public partial class Form1 : Form { string GetLastLogFilePath(string targetFolder) { if(!Directory.Exists(GetLogFolderPath(targetFolder))) return ""; string[] filePaths = Directory.GetFiles(GetLogFolderPath(targetFolder), "*.txt", SearchOption.TopDirectoryOnly); if(filePaths.Length == 0) return ""; return filePaths.Max(); } string GetLogFolderPath(string targetFolder) { return targetFolder + "\\log"; } } |
もし前回のバックアップのログファイルが存在する場合は、記録しておいたファイルパスと最終更新時刻のリストを取得します。
これはファイルパスと最終更新時刻を管理するためのクラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class FileAndTime { public FileAndTime(string path, long tick) { FilePath = path; Tick = tick; } public long Tick { get; protected set; } = 0; public string FilePath { get; protected set; } = ""; public bool IsChecked { get; set; } = false; } |
CreateFileAndTimes(string logFilePath)メソッドは、前回のバックアップのログファイルからバックアップされているファイル・フォルダのパスと最終更新時刻のリストを取得するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public partial class Form1 : Form { List<FileAndTime> CreateFileAndTimes(string logFilePath) { List<FileAndTime> fileAndTimes = new List<FileAndTime>(); StreamReader sr = new StreamReader(logFilePath); string str = sr.ReadToEnd(); sr.Close(); string[] vs1 = str.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); foreach(string line in vs1) { string[] vs2 = line.Split(new string[] { "::" }, StringSplitOptions.RemoveEmptyEntries); if(vs2.Length != 2) continue; FileAndTime fileAndTime = new FileAndTime(vs2[0], long.Parse(vs2[1])); fileAndTimes.Add(fileAndTime); } return fileAndTimes; } } |
ファイル・フォルダのパスと最終更新時刻のリストが取得できたら、ここからバックアップが必要なファイルとフォルダが存在するかどうかを調べます。DoesNeedBackup(List<FileAndTime> fileAndTimes, string[] folderPaths, string[] filePaths)メソッドはそのためのものです。
新しく作成されたり更新されていなくても削除されたファイルがあるかもしれません。そこでfolderPaths.Length と filePaths.Length の合計が fileAndTimes.Count と等しいか調べています。
異なっているのであればバックアップの処理が必要です。同じである場合はログファイルに記録されていた最終更新時刻とバックアップ元のファイルの最終更新時刻を比較します。後者のほうが時刻が後である場合はバックアップが必要です。
ここではバックアップするファイルまたはフォルダがひとつ以上存在するかどうかのみ調べています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public partial class Form1 : Form { bool DoesNeedBackup(List<FileAndTime> fileAndTimes, string[] folderPaths, string[] filePaths) { if(fileAndTimes.Count != folderPaths.Length + filePaths.Length) return true; bool doesNeed = false; foreach(string path in filePaths) { FileAndTime fileAndTime = fileAndTimes.FirstOrDefault(x => x.FilePath == path); if(fileAndTime == null) { doesNeed = true; break; } else { if(File.GetLastWriteTime(path).Ticks > fileAndTime.Tick) { doesNeed = true; break; } } } foreach(string path in folderPaths) { FileAndTime fileAndTime = fileAndTimes.FirstOrDefault(x => x.FilePath == path); if(fileAndTime == null) { doesNeed = true; break; } else { if(Directory.GetLastWriteTime(path).Ticks > fileAndTime.Tick) { doesNeed = true; break; } } } return doesNeed; } } |
あとは実際に新しく作成されたファイルや更新されていたファイルがあればバックアップ先フォルダ内にコピーします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
public partial class Form1 : Form { bool Backup(string sourceFolder, string targetFolder) { if(sourceFolder == "" || targetFolder == "") { MessageBox.Show("バックアップするフォルダが選択されていません!", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } if(!Directory.Exists(sourceFolder) || !Directory.Exists(targetFolder)) { MessageBox.Show("設定されているフォルダは存在しません!", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } string[] filePaths = Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories); string[] folderPaths = Directory.GetDirectories(sourceFolder, "*.*", SearchOption.AllDirectories); List<FileAndTime> fileAndTimes = null; string lastLogFilePath = GetLastLogFilePath(targetFolder); if(lastLogFilePath != "") { fileAndTimes = CreateFileAndTimes(lastLogFilePath); if(!DoesNeedBackup(fileAndTimes, folderPaths, filePaths)) { MessageBox.Show("バックアップをとる必要があるファイルは見つかりませんでした", "報告", MessageBoxButtons.OK, MessageBoxIcon.Information); return false; } } else fileAndTimes = new List<FileAndTime>(); long nowTicks = DateTime.Now.Ticks; string targetFolder1 = GetFolderPathFromTicks(nowTicks); if(!Directory.Exists(targetFolder1)) Directory.CreateDirectory(targetFolder1); List<string> newFiles = new List<string>(); List<string> newFolders = new List<string>(); List<string> modifiedFiles = new List<string>(); List<string> deletedFiles = new List<string>(); foreach(string path in folderPaths) { FileAndTime fileAndTime = fileAndTimes.FirstOrDefault(x => x.FilePath == path); if(fileAndTime == null || Directory.GetLastWriteTime(path).Ticks != fileAndTime.Tick) { // fileAndTime == null なら新しく作成されたフォルダ // Directory.GetLastWriteTime(path).Ticks != fileAndTime.Tick なら内容が変更されたフォルダ string destfolderPath = path.Replace(sourceFolder, targetFolder1); Directory.CreateDirectory(destfolderPath); } // fileAndTime != null のときはチェック済みにする // fileAndTime == null のときはそのパスを新しく作成されたフォルダとしてリストに格納する if(fileAndTime != null) fileAndTime.IsChecked = true; else newFolders.Add(path); } foreach(string path in filePaths) { string destFilePath = path.Replace(sourceFolder, targetFolder1); FileAndTime fileAndTime = fileAndTimes.FirstOrDefault(x => x.FilePath == path); if(fileAndTime == null) { // fileAndTime == null なら新しく作成されたファイル // 新しく作成されたファイルとして、そのパスをリストに格納する CopyFile(path, destFilePath); newFiles.Add(path); } else if(File.GetLastWriteTime(path).Ticks != fileAndTime.Tick) { // File.GetLastWriteTime(path).Ticks != fileAndTime.Tick なら更新されたファイル // 更新されたファイルとして、そのパスをリストに格納する CopyFile(path, destFilePath); modifiedFiles.Add(path); } // fileAndTime != null のときはチェック済みにする if(fileAndTime != null) fileAndTime.IsChecked = true; } // チェックされていない要素があればそれは削除されたファイルまたはフォルダである deletedFiles = fileAndTimes.Where(x => !x.IsChecked).Select(x => x.FilePath).ToList(); // ログを保存するためのファイルを保存するフォルダパスを取得する(必要ならそのためのフォルダをつくる) string logFolderPath = GetLogFolderPath(targetFolder); if(!Directory.Exists(logFolderPath)) Directory.CreateDirectory(logFolderPath); // ログを保存する string logFilePath = GetLogFilePathFromTicks(nowTicks); SaveLogFile(logFilePath, folderPaths, filePaths, targetFolder); // 差分を保存するためのファイルを保存するフォルダパスを取得する(必要ならそのためのフォルダをつくる) string differenceFolderPath = GetDifferenceFolderPath(); if(!Directory.Exists(differenceFolderPath)) Directory.CreateDirectory(differenceFolderPath); // 差分をファイルとして保存する string differenceFilePath = GetDifferenceFilePathFromTicks(nowTicks); SaveDifferenceFile(differenceFilePath, newFiles, newFolders, modifiedFiles, deletedFiles); return true; } void CopyFile(string sourceFilePath, string destFilePath) { // ファイルをコピーしたいフォルダが存在しない場合はつくる FileInfo info = new FileInfo(destFilePath); if(!Directory.Exists(info.DirectoryName)) Directory.CreateDirectory(info.DirectoryName); File.Copy(sourceFilePaht, destFilePath); } } |
以下はログと差分をファイルとして保存するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public partial class Form1 : Form { string GetLogFilePathFromTicks(long ticks) { DateTime dt = new DateTime(ticks); return GetTargetFolderPath() + "\\log\\" + dt.ToString("yyyy-MMdd-HHmmss") + ".txt"; } string GetDifferenceFolderPath() { return GetTargetFolderPath() + "\\difference"; } string GetDifferenceFilePathFromTicks(long ticks) { DateTime dt = new DateTime(ticks); return GetTargetFolderPath() + "\\difference\\" + dt.ToString("yyyy-MMdd-HHmmss") + ".txt"; } void SaveLogFile(string logFilePath, string[] folderPaths, string[] filePaths, string targetFolderPath) { StringBuilder sb = new StringBuilder(); foreach(string path in folderPaths) { DateTime dt = Directory.GetLastWriteTime(path); string time = dt.Ticks.ToString(); // ファイル名やフォルダ名として使えない文字でパスと更新時刻を区切る string str = String.Format("{0}::{1}\n", path, time); sb.Append(str); } foreach(string path in filePaths) { DateTime dt = File.GetLastWriteTime(path); string time = dt.Ticks.ToString(); string str = String.Format("{0}::{1}\n", path, time); sb.Append(str); } using(StreamWriter sw = new StreamWriter(logFilePath)) { sw.Write(sb.ToString()); } } void SaveDifferenceFile(string differenceFilePath, List<string> newFiles, List<string> newFolders, List<string> modifiedFiles, List<string> deletedFiles) { StringBuilder sb = new StringBuilder(); sb.Clear(); sb.Append("新しく生成されたフォルダ\n\n"); foreach(string path in newFolders) sb.Append(path + "\n"); sb.Append("\n新しく生成されたファイル\n\n"); foreach(string path in newFiles) sb.Append(path + "\n"); sb.Append("\n更新されたファイル\n\n"); foreach(string path in modifiedFiles) sb.Append(path + "\n"); sb.Append("\n削除されたフォルダとファイル\n\n"); foreach(string path in deletedFiles) sb.Append(path + "\n"); using(StreamWriter sw = new StreamWriter(differenceFilePath)) { sw.Write(sb.ToString()); } } } |
次回はバックアップ先からファイルを復元する処理を考えます。