-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8accac0
commit d29c95a
Showing
16 changed files
with
840 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
using System.Text.Json; | ||
|
||
namespace BackupHash; | ||
static class BackupFiles { | ||
private static char _ByteToChar(int Data) { | ||
if (Data < 10) | ||
return (char)(Data + 0x30); | ||
else | ||
return (char)(Data + 0x41); | ||
} | ||
private static string _BytesToString(byte[] Bytes) { | ||
StringBuilder sb = new(Bytes.Length << 1) { | ||
Length = Bytes.Length << 1 | ||
}; | ||
for (int i = 0; i < Bytes.Length; i++) { | ||
int i2 = i << 1; | ||
sb[i2] = _ByteToChar(Bytes[i] & 0x0F); | ||
sb[i2 | 1] = _ByteToChar((Bytes[i] & 0xF0) >> 4); | ||
} | ||
return sb.ToString(); | ||
} | ||
public static string GetFileHash(string Path, HashAlgorithm HashAlgorithm) { | ||
byte[] bytes; | ||
using (FileStream fs = new(Path, FileMode.Open)) | ||
bytes = HashAlgorithm.ComputeHash(fs); | ||
return _BytesToString(bytes); | ||
} | ||
public static (string Hash, bool IsNew) BackupFileNoUpdate(string Path, string BackupDir, HashAlgorithm HashAlgorithm, string Timestamp) { | ||
string hash = GetFileHash(Path, HashAlgorithm); | ||
FileInfo file = new(Path); | ||
string filePath = System.IO.Path.Combine(BackupDir, hash); | ||
bool isNew = !File.Exists(filePath); | ||
if (isNew) _ = file.CopyTo(filePath, false); | ||
return (hash, isNew); | ||
} | ||
public static (int Added, int Preexisting) TakeSnapshot(List<string> Files, string BackupDir, DateTime Time, string Timestamp) { | ||
if (!Directory.Exists(BackupDir)) | ||
_ = Directory.CreateDirectory(BackupDir); | ||
|
||
string snapshotPath = Path.Combine(BackupDir, $"snapshot_{Timestamp}.json"); | ||
if (File.Exists(snapshotPath)) | ||
throw new Exception($"Snapshot '{Timestamp}' already exists."); | ||
|
||
int added = 0; | ||
int preexisting = 0; | ||
List<SnapshotFileMeta> backedHashes = []; | ||
using HashAlgorithm hashAlg = SHA1.Create(); | ||
foreach (string file in Files) { | ||
(string hash, bool isNew) = BackupFileNoUpdate(file, BackupDir, hashAlg, Timestamp); | ||
if (isNew) | ||
++added; | ||
else | ||
++preexisting; | ||
backedHashes.Add(new SnapshotFileMeta(file, hash)); | ||
UserInteraction.Info($"Backed up file '{file}' with hash '{hash}'."); | ||
} | ||
|
||
SnapshotMeta snapshotMeta = new(backedHashes, Time); | ||
string snapshotJson = JsonSerializer.Serialize(snapshotMeta); | ||
File.WriteAllText(snapshotPath, snapshotJson); | ||
|
||
return (added, preexisting); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.NamingConventionBinder; | ||
using static System.Console; | ||
|
||
namespace BackupHash; | ||
static partial class Commands { | ||
private static void _Backup(string? i, string? b, bool nc) { | ||
i = Shared.MakePathFull(i); | ||
b = Shared.MakePathFull(b); | ||
|
||
if (Path.TrimEndingDirectorySeparator(i) == Path.TrimEndingDirectorySeparator(b)) { | ||
UserInteraction.Fatal("Backup directory cannot be the same as source file/directory."); | ||
return; | ||
} | ||
|
||
if (!Directory.Exists(b)) { | ||
try { | ||
_ = Directory.CreateDirectory(b); | ||
} | ||
catch { | ||
UserInteraction.Fatal($"Directory '{b}' does not exist, and could not be created."); | ||
return; | ||
} | ||
} | ||
|
||
List<string> files; | ||
if (File.Exists(i)) { | ||
files = [i]; | ||
} | ||
else if (Directory.Exists(i)) { | ||
try { | ||
files = FindAllFiles.Find(i); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
} | ||
else { | ||
UserInteraction.Fatal($"Input path '{i}' does not exist."); | ||
return; | ||
} | ||
|
||
if (files.Count == 0) { | ||
UserInteraction.Info("The program found no files applicable to backup."); | ||
return; | ||
} | ||
|
||
UserInteraction.Info("Backing up all of the following:"); | ||
foreach (string file in files) { | ||
WriteLine(" " + file); | ||
} | ||
|
||
DateTime time = DateTime.UtcNow; | ||
string timestamp = Shared.ToTimestamp(time); | ||
UserInteraction.Info($"Using timestamp {timestamp} ({time})."); | ||
|
||
if (!nc && !UserInteraction.Warning_PromptContinue("This operation may take time.")) { | ||
UserInteraction.Fatal("Operation cancelled by user."); | ||
return; | ||
} | ||
|
||
int added; | ||
int preexisting; | ||
try { | ||
(added, preexisting) = BackupFiles.TakeSnapshot(files, b, time, timestamp); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
UserInteraction.Info($"Operation succeeded with {added + preexisting} files backed up: {added} not seen before; and {preexisting} duplicate(s)."); | ||
} | ||
public static Command Backup = new("backup", "Backs up the provided input file/directory (working directory if unspecified) to the provided output directory (working directory if unspecified).") { | ||
new Option<string>(["--input", "-i"], "The input file/directory. Defaults to the working directory."), | ||
new Option<string>(["--backup-dir", "-b"], "The backup directory. Defaults to the working directory."), | ||
new Option<bool>(["--no-confirm", "-nc"], "Whether or not the program confirms the backup before executing it.") | ||
}; | ||
private static void _Init_Backup() { | ||
Backup.Handler = CommandHandler.Create<string?, string?, bool>(_Backup); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace BackupHash; | ||
static partial class Commands { | ||
static Commands() { | ||
_Init_Root(); | ||
|
||
_Init_Gen(); | ||
Root.AddCommand(Gen); | ||
|
||
_Init_Backup(); | ||
Root.AddCommand(Backup); | ||
|
||
_Init_View(); | ||
Root.AddCommand(View); | ||
|
||
_Init_Restore(); | ||
Root.AddCommand(Restore); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.NamingConventionBinder; | ||
|
||
namespace BackupHash; | ||
static partial class Commands { | ||
private static void _Gen(bool ow) { | ||
string filePath = Path.Combine(Shared.WorkingDirectory, Shared.ConfigFilename); | ||
bool exists = File.Exists(filePath); | ||
if (!ow) { | ||
if (exists) | ||
{ | ||
if (!UserInteraction.Warning_PromptContinue($"Config file '{Shared.ConfigFilename}' already exists.")) | ||
{ | ||
UserInteraction.Fatal("Operation cancelled by user."); | ||
return; | ||
} | ||
} | ||
} | ||
try { | ||
File.WriteAllText(filePath, Shared.DefaultConfig); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
if (exists) | ||
UserInteraction.Info($"Successfully replaced config file '{filePath}' with default."); | ||
else | ||
UserInteraction.Info($"Successfully added config file '{filePath}'."); | ||
} | ||
public static Command Gen = new("gen", $"Generates a default '{Shared.ConfigFilename}' file in the working directory.") { | ||
new Option<bool>(["--overwrite", "-ow"], "Overwrites the file if it already exists, without asking the user.") | ||
}; | ||
private static void _Init_Gen() { | ||
Gen.Handler = CommandHandler.Create<bool>(_Gen); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
using System.CommandLine; | ||
using System.CommandLine.NamingConventionBinder; | ||
using System.IO; | ||
using System.Text.Json; | ||
|
||
namespace BackupHash; | ||
static partial class Commands { | ||
private static void _Restore(string? o, string? b, string? i, long? ts) | ||
{ | ||
o = Shared.MakePathFull(o); | ||
b = Shared.MakePathFull(b); | ||
if (i is null) | ||
i = o; | ||
else | ||
i = Shared.MakePathFull(i); | ||
if (o == b || i == b) { | ||
UserInteraction.Fatal("Neither argument --output/-o nor --input/-i should equal --backup-dir/-b. This is not the case."); | ||
return; | ||
} | ||
string snapshotFilepath; | ||
if (ts.HasValue) | ||
snapshotFilepath = Shared.GetSnapshotFilepathFromTimestamp(b, ts.Value); | ||
else | ||
snapshotFilepath = Directory.GetFiles(b, Shared.SnapshotSearchString) | ||
.Where(F => Shared.SnapshotMetaFileRegex.IsMatch(F)) | ||
.Order() | ||
.Last(); | ||
|
||
string snapshotJson; | ||
try { | ||
snapshotJson = File.ReadAllText(snapshotFilepath); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
|
||
SnapshotMeta? snapshotMeta; | ||
try { | ||
snapshotMeta = JsonSerializer.Deserialize<SnapshotMeta>(snapshotJson); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
if (snapshotMeta is null) { | ||
UserInteraction.Fatal($"JSON file '{snapshotFilepath}' is invalid."); | ||
return; | ||
} | ||
|
||
List<string> deletingFiles; | ||
if (Directory.Exists(o)) { | ||
try { | ||
deletingFiles = FindAllFiles.Find(o); | ||
} | ||
catch (Exception e) { | ||
UserInteraction.Fatal(e.Message); | ||
return; | ||
} | ||
} | ||
else | ||
deletingFiles = []; | ||
|
||
Dictionary<string, ConfigInfo?> configs = []; | ||
ConfigInfo? GetConfigForDir(string Dir) | ||
{ | ||
if (configs.TryGetValue(Dir, out ConfigInfo? config)) | ||
return config; | ||
_ = ConfigInfo.ConditionalFromDirectory(Dir, out config); | ||
configs.Add(Dir, config); | ||
return config; | ||
} | ||
bool DestinationIncluded(string Filepath) { | ||
string? directory = Path.GetDirectoryName(Filepath); | ||
if (directory is null) | ||
return false; | ||
List<string> upperDirectories = [directory]; | ||
string? currentDirectory = directory; | ||
while (true) { | ||
currentDirectory = Shared.GetParentDir(currentDirectory); | ||
if (string.IsNullOrEmpty(currentDirectory)) | ||
break; | ||
upperDirectories.Add(currentDirectory); | ||
if (Path.GetRelativePath(b, currentDirectory) == currentDirectory) | ||
break; | ||
} | ||
string workingDirectory = b; | ||
ConfigInfo? currentConfig = GetConfigForDir(workingDirectory); | ||
for (int idx = upperDirectories.Count - 1; idx > 0; --idx) { | ||
string thisDir = upperDirectories[idx]; | ||
ConfigInfo? thisConfig = GetConfigForDir(thisDir); | ||
if (thisConfig is not null) { | ||
currentConfig = thisConfig; | ||
workingDirectory = thisDir; | ||
} | ||
if (currentConfig is null) | ||
continue; | ||
string relativeNext = Path.GetRelativePath(workingDirectory, upperDirectories[idx - 1]); | ||
if (!currentConfig.IncludedDirectory(relativeNext)) | ||
return false; | ||
} | ||
return currentConfig?.IncludedFile(Filepath) ?? true; | ||
} | ||
|
||
List<(string Hash, string Destination)> pairs = []; | ||
foreach (SnapshotFileMeta fileMeta in snapshotMeta.Files) { | ||
string relative = Path.GetRelativePath(i, fileMeta.Filepath); | ||
if (relative == fileMeta.Filepath) | ||
continue; | ||
string newPath = Path.Combine(o, relative); | ||
if (!DestinationIncluded(newPath)) | ||
continue; | ||
pairs.Add((fileMeta.Hash, newPath)); | ||
} | ||
|
||
HashSet<string> set_deleted = [..deletingFiles]; | ||
HashSet<string> set_added = [.. pairs.Select(P => P.Destination)]; | ||
HashSet<string> set_modified = [..deletingFiles]; | ||
set_modified.IntersectWith(set_added); | ||
set_deleted.ExceptWith(set_modified); | ||
set_added.ExceptWith(set_modified); | ||
|
||
if (set_added.Count > 0) { | ||
UserInteraction.Info("The following file(s) will be added:"); | ||
foreach (string file in set_added) | ||
Console.WriteLine(" " + file); | ||
} | ||
else | ||
UserInteraction.Info("No files will be added."); | ||
if (set_modified.Count > 0) { | ||
UserInteraction.Warning("The following file(s) will be overwritten:"); | ||
foreach (string file in set_modified) | ||
Console.WriteLine(" " + file); | ||
} | ||
else | ||
UserInteraction.Info("No files will be overwritten."); | ||
if (set_deleted.Count > 0) { | ||
UserInteraction.Warning("The following file(s) will be deleted:"); | ||
foreach (string file in set_deleted) | ||
Console.WriteLine(" " + file); | ||
} | ||
else | ||
UserInteraction.Info("No files will be deleted."); | ||
|
||
if (!UserInteraction.Warning_PromptContinue("Please reread all input. There is no undo button.")) { | ||
UserInteraction.Fatal("Operation cancelled by user."); | ||
return; | ||
} | ||
|
||
if (Directory.Exists(o)) | ||
foreach (string file in set_deleted) | ||
try { | ||
File.Delete(file); | ||
} | ||
catch { | ||
UserInteraction.Warning($"Could not delete existing file '{file}'."); | ||
} | ||
|
||
foreach ((string hash, string destination) in pairs) { | ||
UserInteraction.Info($"Copying to file {destination}"); | ||
string? directory = Path.GetDirectoryName(destination); | ||
if (directory is null) { | ||
UserInteraction.Fatal(null); | ||
return; | ||
} | ||
if (!Directory.Exists(directory)) | ||
_ = Directory.CreateDirectory(directory); | ||
|
||
FileInfo fileInfo = new(Path.Combine(b, hash)); | ||
_ = fileInfo.CopyTo(destination, true); | ||
} | ||
UserInteraction.Info($"Operation succeeded with {pairs.Count} file(s) restored: {set_added.Count} added; {set_modified.Count} modified; and {set_deleted.Count} deleted."); | ||
} | ||
public static Command Restore = new("restore", "Backs up the provided input file/directory (working directory if unspecified) to the provided output directory (working directory if unspecified).") { | ||
new Option<string>(["--output", "-o"], "The output file/directory. Defaults to the working directory."), | ||
new Option<string>(["--backup-dir", "-b"], "The backup directory. Defaults to the working directory."), | ||
new Option<string>(["--input", "-i"], "The directory in the backups. Defaults to the value of --output/-o. Intended to be used if the directory has moved since the backup took place."), | ||
new Option<long>(["--timestamp", "-ts"], "The timestamp of the backup used. Defaults to the most recent backup."), | ||
}; | ||
private static void _Init_Restore() { | ||
Restore.Handler = CommandHandler.Create<string?, string?, string?, long?>(_Restore); | ||
} | ||
} |
Oops, something went wrong.