Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Parallel.Cli/Commands/CleanCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using Parallel.Cli.Utils;
Expand Down
6 changes: 3 additions & 3 deletions Parallel.Cli/Commands/DuplicatesCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using Parallel.Cli.Utils;
Expand Down Expand Up @@ -36,10 +36,10 @@ private async Task ScanForDuplicateFiles(string path)
lines.Add($"{key} ({paths.Length:N0} items):");
lines.AddRange(paths.Select(value => $" - {value}"));
}

string fileName = PathBuilder.TempFile;
await File.WriteAllLinesAsync(fileName, lines);

CommandLine.WriteLine($"Scan found {duplicates.Count(kv => kv.Value.Length > 1):N0} duplicate files. ({Formatter.FromBytes(length)})", ConsoleColor.Green);
CommandLine.WriteLine($"A detailed list can be found here: {fileName}", ConsoleColor.DarkGray);
}
Expand Down
6 changes: 3 additions & 3 deletions Parallel.Cli/Commands/FetchCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using Parallel.Cli.Utils;
Expand All @@ -25,7 +25,7 @@ private async Task HandleFetchAsync(string config)
await FetchVaultAsync(localVault);
return;
}

await Program.Settings.ForEachVaultAsync(FetchVaultAsync);
}

Expand All @@ -38,7 +38,7 @@ private async Task FetchVaultAsync(LocalVaultConfig localVault)
CommandLine.WriteLine(localVault, "Failed to connect to vault!", ConsoleColor.Red);
return;
}

CommandLine.WriteLine(syncManager.LocalVault, $"Successfully fetched vault data for: '{localVault.Name}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
Expand Down
2 changes: 1 addition & 1 deletion Parallel.Cli/Commands/HistoryCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using Parallel.Cli.Utils;
Expand Down
2 changes: 1 addition & 1 deletion Parallel.Cli/Commands/IgnoreCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using Parallel.Cli.Utils;
Expand Down
4 changes: 2 additions & 2 deletions Parallel.Cli/Commands/PruneCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using System.Diagnostics;
Expand Down Expand Up @@ -135,7 +135,7 @@ private async Task PruneInternalAsync(ISyncManager syncManager, string path, Dat
{
files = await (syncManager.Database?.GetFilesAsync(path, timestamp, true) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
}

if (files.Count == 0)
{
CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow);
Expand Down
148 changes: 120 additions & 28 deletions Parallel.Cli/Commands/RestoreCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.Collections.Concurrent;
using System.CommandLine;
Expand All @@ -19,27 +19,45 @@ public class RestoreCommand : Command
{
private Stopwatch _sw = new Stopwatch();

private readonly Option<string> _sourceOpt = new(["--path", "-p"], "The source path to restore.");
private readonly Argument<string> _sourceArg = new("path", "The path to add or remove.");
private readonly Option<string> _sourceOpt = new(["--path", "-p"], "The source path to pull.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");
private readonly Option<DateTime> _beforeOpt = new(["--before"], "Restores files before a certain timestamp.");
private readonly Option<string> _remapOpt = new(["--remap"], "The new directory to map restored files to.");
private readonly Option<bool> _forceOpt = new(["--force", "-f"], "Forces restoring, bypassing safe guards.");
private readonly Option<DateTime> _beforeOpt = new(["--before"], "Pulls files before a certain timestamp.");
private readonly Option<string> _destOpt = new(["--destination"], "The new directory to map pulled files to.");
private readonly Option<bool> _archiveOpt = new(["--archive", "-a"], "Pulls only archived files.");
private readonly Option<bool> _forceOpt = new(["--force", "-f"], "Forces pulling, bypassing safe guards.");
private readonly Option<bool> _dryRunOpt = new(["--dry-run"], "Previews the command without executing it.");
private readonly Option<bool> _verboseOpt = new(["--verbose", "-v"], "Shows verbose output.");

private readonly Command addCmd = new("add", "Adds a new directory to the backup list.");
private readonly Command listCmd = new("list", "Shows all directories in the backup list.");
private readonly Command removeCmd = new("remove", "Removes a directory from the backup list.");

public RestoreCommand() : base("restore", "Restores files from the backup.")
public RestoreCommand() : base("restore", "Pulls files a vault.")
{
this.AddOption(_sourceOpt);
this.AddOption(_configOpt);
this.AddOption(_beforeOpt);
this.AddOption(_remapOpt);
this.AddOption(_destOpt);
this.AddOption(_archiveOpt);
this.AddOption(_forceOpt);
this.AddOption(_dryRunOpt);
this.AddOption(_verboseOpt);
this.SetHandler(HandleRestoreAsync, _sourceOpt, _configOpt, _beforeOpt, _remapOpt, _forceOpt, _verboseOpt, _dryRunOpt);
this.SetHandler(HandlePullAsync, _sourceOpt, _configOpt, _beforeOpt, _destOpt, _archiveOpt, _forceOpt, _verboseOpt, _dryRunOpt);

this.AddCommand(addCmd);
addCmd.AddArgument(_sourceArg);
addCmd.AddOption(_configOpt);
addCmd.AddOption(_destOpt);
addCmd.SetHandler(HandleAddAsync, _sourceArg, _configOpt, _destOpt);

this.AddCommand(removeCmd);
removeCmd.AddArgument(_sourceArg);
removeCmd.AddOption(_configOpt);
removeCmd.SetHandler(HandleRemoveAsync, _sourceArg, _configOpt);
}

private async Task HandleRestoreAsync(string? path, string? config, DateTime before, string? remap, bool force, bool verbose, bool dryRun)
private async Task HandlePullAsync(string? path, string? config, DateTime before, string? destination, bool archive, bool force, bool verbose, bool dryRun)
{
_sw = Stopwatch.StartNew();
DateTime timestamp = before != DateTime.MinValue ? before.AddMinutes(1).AddTicks(-1) : DateTime.Now;
Expand All @@ -48,27 +66,27 @@ private async Task HandleRestoreAsync(string? path, string? config, DateTime bef
{
if (!string.IsNullOrEmpty(path))
{
await RestorePathAsync(localVault, path, timestamp, remap, force, verbose, dryRun);
await PullPathAsync(localVault, path, timestamp, destination, archive, force, verbose, dryRun);
}
else
{
await RestoreSystemAsync(localVault, timestamp, remap, force, verbose, dryRun);
await PullSystemAsync(localVault, timestamp, destination, archive, force, verbose, dryRun);
}
}
else
{
if (!string.IsNullOrEmpty(path))
{
await Program.Settings.ForEachVaultAsync(vault => RestorePathAsync(vault, path, timestamp, remap, force, verbose, dryRun));
await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, destination, archive, force, verbose, dryRun));
}
else
{
await Program.Settings.ForEachVaultAsync(vault => RestoreSystemAsync(vault, timestamp, remap, force, verbose, dryRun));
await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, destination, archive, force, verbose, dryRun));
}
}
}

private async Task RestoreSystemAsync(LocalVaultConfig vault, DateTime timestamp, string? output, bool force, bool verbose, bool dryRun)
private async Task PullSystemAsync(LocalVaultConfig vault, DateTime timestamp, string? output, bool archive, bool force, bool verbose, bool dryRun)
{
ISyncManager? syncManager = SyncManager.CreateNew(vault);
if (syncManager == null || !await syncManager.ConnectAsync())
Expand All @@ -77,13 +95,13 @@ private async Task RestoreSystemAsync(LocalVaultConfig vault, DateTime timestamp
return;
}

foreach (string path in syncManager.RemoteVault.BackupDirectories)
foreach (PullRecord record in syncManager.RemoteVault.PullDirectories.Where(r => r.Machine == Environment.MachineName))
{
await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun);
await PullInternalAsync(syncManager, record.Source, timestamp, (output ?? record.Destination), archive, force, verbose, dryRun);
}
}

private async Task RestorePathAsync(LocalVaultConfig vault, string path, DateTime timestamp, string? output, bool force, bool verbose, bool dryRun)
private async Task PullPathAsync(LocalVaultConfig vault, string path, DateTime timestamp, string? destination, bool archive, bool force, bool verbose, bool dryRun)
{
ISyncManager? syncManager = SyncManager.CreateNew(vault);
if (syncManager == null || !await syncManager.ConnectAsync())
Expand All @@ -92,23 +110,23 @@ private async Task RestorePathAsync(LocalVaultConfig vault, string path, DateTim
return;
}

await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun);
await PullInternalAsync(syncManager, path, timestamp, destination, archive, force, verbose, dryRun);
}

private async Task RestoreInternalAsync(ISyncManager syncManager, string path, DateTime timestamp, string? output, bool force, bool verbose, bool dryRun)
private async Task PullInternalAsync(ISyncManager syncManager, string path, DateTime timestamp, string? destination, bool archive, bool force, bool verbose, bool dryRun)
{
CommandLine.WriteLine(syncManager.RemoteVault, $"Scanning for files in {path}...", ConsoleColor.DarkGray);
IReadOnlyList<LocalFile> files = await (syncManager.Database?.GetLatestFilesAsync(path, timestamp) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
IReadOnlyList<LocalFile> files = await (syncManager.Database?.GetLatestFilesAsync(path, timestamp, archive) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
Log.Debug($"GetLatestFilesAsync returned {files.Count} files for path '{path}'");

ConcurrentBag<LocalFile> restoreFiles = new();
System.Threading.Tasks.Parallel.ForEach(files, ParallelConfig.Options, (file) =>
{
//string sourcePath = file.Fullname;
//string outputPath = string.IsNullOrEmpty(output) ? sourcePath : PathBuilder.ReplacePath(sourcePath, path, output);
string outputPath = PathBuilder.ReplacePath(file.Fullname, path, output);
string outputPath = PathBuilder.ReplacePath(file.Fullname, path, destination);
if (File.Exists(outputPath) && !FileScanner.HasChanged(file, new LocalFile(outputPath)) && !force) return;

file.Fullname = outputPath;
restoreFiles.Add(file);
});
Expand All @@ -123,18 +141,92 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D
{
string fileName = PathBuilder.TempFile;
await File.WriteAllLinesAsync(fileName, restoreFiles.Select(f => f.Fullname).OrderBy(f => f));
CommandLine.WriteLine($"This operation will restore {restoreFiles.Count:N0} files into: {(string.IsNullOrEmpty(output) ? path : output)}", ConsoleColor.Green);
CommandLine.WriteLine($"This operation will pull {restoreFiles.Count:N0} files into: {(string.IsNullOrEmpty(destination) ? path : destination)}", ConsoleColor.Green);
CommandLine.WriteLine($"A detailed list can be found here: {fileName}", ConsoleColor.DarkGray);
}
else
{
CommandLine.WriteLine(syncManager.RemoteVault, $"Restoring {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
int restoredFiles = await syncManager.RestoreFilesAsync(restoreFiles.ToArray(), progressReporter);
CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully restored {restoredFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
CommandLine.WriteLine(syncManager.RemoteVault, $"Pulling {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
int pulledFiles = await syncManager.PullFilesAsync(restoreFiles.ToArray(), progressReporter);

CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pulled {pulledFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
}

#region Add

private async Task HandleAddAsync(string path, string? config, string? destination)
{
LocalVaultConfig? localVault = string.IsNullOrEmpty(config) ? ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled) : ParallelConfig.GetVault(config);
if (!string.IsNullOrEmpty(config) && localVault != null)
{
await AddPathAsync(localVault, path, destination);
}
else
{
await Program.Settings.ForEachVaultAsync(vault => AddPathAsync(vault, path, destination));
}
}

private async Task AddPathAsync(LocalVaultConfig vault, string path, string? destination)
{
ISyncManager? syncManager = SyncManager.CreateNew(vault);
if (syncManager == null || !await syncManager.ConnectAsync())
{
CommandLine.WriteLine(vault, "Failed to connect to vault!", ConsoleColor.Red);
return;
}

PullRecord record = new(path, destination);
if (!syncManager.RemoteVault.PullDirectories.Add(record))
{
CommandLine.WriteLine(vault, $"Unable to add path: '{path}'", ConsoleColor.Yellow);
return;
}

CommandLine.WriteLine(vault, $"Successfully added '{path}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}

#endregion

#region Remove

private async Task HandleRemoveAsync(string path, string? config)
{
LocalVaultConfig? localVault = string.IsNullOrEmpty(config) ? ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled) : ParallelConfig.GetVault(config);
if (!string.IsNullOrEmpty(config) && localVault != null)
{
await RemovePathAsync(localVault, path);
}
else
{
await Program.Settings.ForEachVaultAsync(vault => RemovePathAsync(vault, path));
}
}

private async Task RemovePathAsync(LocalVaultConfig vault, string path)
{
ISyncManager? syncManager = SyncManager.CreateNew(vault);
if (syncManager == null || !await syncManager.ConnectAsync())
{
CommandLine.WriteLine(vault, "Failed to connect to vault!", ConsoleColor.Red);
return;
}

IEnumerable<PullRecord> records = syncManager.RemoteVault.PullDirectories.Where(r => r.Machine == Environment.MachineName && r.Source == path);
foreach (PullRecord record in records)
{
if (syncManager.RemoteVault.PullDirectories.Remove(record)) continue;
CommandLine.WriteLine(vault, $"Unable to remove path: '{path}'", ConsoleColor.Yellow);
}

CommandLine.WriteLine(vault, $"Successfully removed '{path}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}

#endregion
}
}
10 changes: 5 additions & 5 deletions Parallel.Cli/Commands/ScrubCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2026 Kyle Ebbinga
// Copyright 2026 Entex Interactive

using System.CommandLine;
using System.Diagnostics;
Expand All @@ -24,7 +24,7 @@ public ScrubCommand() : base("scrub", "Verifies file integrity within a vault.")
this.AddOption(_sourceOpt);
this.AddOption(_configOpt);
this.AddOption(_verboseOpt);
this.SetHandler(HandleScrubAsync, _sourceOpt, _configOpt, _verboseOpt);
this.SetHandler(HandleScrubAsync, _sourceOpt, _configOpt, _verboseOpt);
}

private async Task HandleScrubAsync(string? path, string? config, bool verbose)
Expand Down Expand Up @@ -64,7 +64,7 @@ private async Task ScrubSystemAsync(LocalVaultConfig vault, bool verbose)
return;
}

foreach (string path in syncManager.RemoteVault.BackupDirectories)
foreach (string path in syncManager.RemoteVault.PushDirectories)
{
await ScrubInternalAsync(syncManager, path, verbose);
}
Expand All @@ -91,11 +91,11 @@ private async Task ScrubInternalAsync(ISyncManager syncManager, string path, boo
CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow);
return;
}

CommandLine.WriteLine(syncManager.RemoteVault, $"Scrubbing {files.Count:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, files.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
int scrubbedFiles = await syncManager.ScrubFilesAsync(files, progressReporter);

CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully scrubbed {scrubbedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
Expand Down
Loading
Loading