Skip to content

Commit

Permalink
Added base project
Browse files Browse the repository at this point in the history
  • Loading branch information
brendanlynn committed Aug 3, 2024
1 parent 8accac0 commit d29c95a
Show file tree
Hide file tree
Showing 16 changed files with 840 additions and 0 deletions.
66 changes: 66 additions & 0 deletions BackupFiles.cs
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);
}
}
82 changes: 82 additions & 0 deletions Commands/Backup.cs
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);
}
}
18 changes: 18 additions & 0 deletions Commands/Commands.cs
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);
}
}
37 changes: 37 additions & 0 deletions Commands/Gen.cs
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);
}
}
183 changes: 183 additions & 0 deletions Commands/Restore.cs
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);
}
}
Loading

0 comments on commit d29c95a

Please # to comment.