Skip to content

Commit ebbad4e

Browse files
Merge pull request #103 from EntexInteractive/feature/service
Added its own service host for running background tasks
2 parents 2b7f601 + d05ae09 commit ebbad4e

80 files changed

Lines changed: 701 additions & 660 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Parallel.Cli/Commands/CleanCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using Parallel.Cli.Utils;

Parallel.Cli/Commands/DuplicatesCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using Parallel.Cli.Utils;
@@ -36,10 +36,10 @@ private async Task ScanForDuplicateFiles(string path)
3636
lines.Add($"{key} ({paths.Length:N0} items):");
3737
lines.AddRange(paths.Select(value => $" - {value}"));
3838
}
39-
39+
4040
string fileName = PathBuilder.TempFile;
4141
await File.WriteAllLinesAsync(fileName, lines);
42-
42+
4343
CommandLine.WriteLine($"Scan found {duplicates.Count(kv => kv.Value.Length > 1):N0} duplicate files. ({Formatter.FromBytes(length)})", ConsoleColor.Green);
4444
CommandLine.WriteLine($"A detailed list can be found here: {fileName}", ConsoleColor.DarkGray);
4545
}

Parallel.Cli/Commands/FetchCommand.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using Parallel.Cli.Utils;
@@ -25,7 +25,7 @@ private async Task HandleFetchAsync(string config)
2525
await FetchVaultAsync(localVault);
2626
return;
2727
}
28-
28+
2929
await Program.Settings.ForEachVaultAsync(FetchVaultAsync);
3030
}
3131

@@ -38,7 +38,7 @@ private async Task FetchVaultAsync(LocalVaultConfig localVault)
3838
CommandLine.WriteLine(localVault, "Failed to connect to vault!", ConsoleColor.Red);
3939
return;
4040
}
41-
41+
4242
CommandLine.WriteLine(syncManager.LocalVault, $"Successfully fetched vault data for: '{localVault.Name}'", ConsoleColor.Green);
4343
await syncManager.DisconnectAsync();
4444
}

Parallel.Cli/Commands/HistoryCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using Parallel.Cli.Utils;

Parallel.Cli/Commands/IgnoreCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using Parallel.Cli.Utils;

Parallel.Cli/Commands/PruneCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using System.Diagnostics;
@@ -135,7 +135,7 @@ private async Task PruneInternalAsync(ISyncManager syncManager, string path, Dat
135135
{
136136
files = await (syncManager.Database?.GetFilesAsync(path, timestamp, true) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
137137
}
138-
138+
139139
if (files.Count == 0)
140140
{
141141
CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow);
Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.Collections.Concurrent;
44
using System.CommandLine;
@@ -19,27 +19,45 @@ public class RestoreCommand : Command
1919
{
2020
private Stopwatch _sw = new Stopwatch();
2121

22-
private readonly Option<string> _sourceOpt = new(["--path", "-p"], "The source path to restore.");
22+
private readonly Argument<string> _sourceArg = new("path", "The path to add or remove.");
23+
private readonly Option<string> _sourceOpt = new(["--path", "-p"], "The source path to pull.");
2324
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");
24-
private readonly Option<DateTime> _beforeOpt = new(["--before"], "Restores files before a certain timestamp.");
25-
private readonly Option<string> _remapOpt = new(["--remap"], "The new directory to map restored files to.");
26-
private readonly Option<bool> _forceOpt = new(["--force", "-f"], "Forces restoring, bypassing safe guards.");
25+
private readonly Option<DateTime> _beforeOpt = new(["--before"], "Pulls files before a certain timestamp.");
26+
private readonly Option<string> _destOpt = new(["--destination"], "The new directory to map pulled files to.");
27+
private readonly Option<bool> _archiveOpt = new(["--archive", "-a"], "Pulls only archived files.");
28+
private readonly Option<bool> _forceOpt = new(["--force", "-f"], "Forces pulling, bypassing safe guards.");
2729
private readonly Option<bool> _dryRunOpt = new(["--dry-run"], "Previews the command without executing it.");
2830
private readonly Option<bool> _verboseOpt = new(["--verbose", "-v"], "Shows verbose output.");
31+
32+
private readonly Command addCmd = new("add", "Adds a new directory to the backup list.");
33+
private readonly Command listCmd = new("list", "Shows all directories in the backup list.");
34+
private readonly Command removeCmd = new("remove", "Removes a directory from the backup list.");
2935

30-
public RestoreCommand() : base("restore", "Restores files from the backup.")
36+
public RestoreCommand() : base("restore", "Pulls files a vault.")
3137
{
3238
this.AddOption(_sourceOpt);
3339
this.AddOption(_configOpt);
3440
this.AddOption(_beforeOpt);
35-
this.AddOption(_remapOpt);
41+
this.AddOption(_destOpt);
42+
this.AddOption(_archiveOpt);
3643
this.AddOption(_forceOpt);
3744
this.AddOption(_dryRunOpt);
3845
this.AddOption(_verboseOpt);
39-
this.SetHandler(HandleRestoreAsync, _sourceOpt, _configOpt, _beforeOpt, _remapOpt, _forceOpt, _verboseOpt, _dryRunOpt);
46+
this.SetHandler(HandlePullAsync, _sourceOpt, _configOpt, _beforeOpt, _destOpt, _archiveOpt, _forceOpt, _verboseOpt, _dryRunOpt);
47+
48+
this.AddCommand(addCmd);
49+
addCmd.AddArgument(_sourceArg);
50+
addCmd.AddOption(_configOpt);
51+
addCmd.AddOption(_destOpt);
52+
addCmd.SetHandler(HandleAddAsync, _sourceArg, _configOpt, _destOpt);
53+
54+
this.AddCommand(removeCmd);
55+
removeCmd.AddArgument(_sourceArg);
56+
removeCmd.AddOption(_configOpt);
57+
removeCmd.SetHandler(HandleRemoveAsync, _sourceArg, _configOpt);
4058
}
4159

42-
private async Task HandleRestoreAsync(string? path, string? config, DateTime before, string? remap, bool force, bool verbose, bool dryRun)
60+
private async Task HandlePullAsync(string? path, string? config, DateTime before, string? destination, bool archive, bool force, bool verbose, bool dryRun)
4361
{
4462
_sw = Stopwatch.StartNew();
4563
DateTime timestamp = before != DateTime.MinValue ? before.AddMinutes(1).AddTicks(-1) : DateTime.Now;
@@ -48,27 +66,27 @@ private async Task HandleRestoreAsync(string? path, string? config, DateTime bef
4866
{
4967
if (!string.IsNullOrEmpty(path))
5068
{
51-
await RestorePathAsync(localVault, path, timestamp, remap, force, verbose, dryRun);
69+
await PullPathAsync(localVault, path, timestamp, destination, archive, force, verbose, dryRun);
5270
}
5371
else
5472
{
55-
await RestoreSystemAsync(localVault, timestamp, remap, force, verbose, dryRun);
73+
await PullSystemAsync(localVault, timestamp, destination, archive, force, verbose, dryRun);
5674
}
5775
}
5876
else
5977
{
6078
if (!string.IsNullOrEmpty(path))
6179
{
62-
await Program.Settings.ForEachVaultAsync(vault => RestorePathAsync(vault, path, timestamp, remap, force, verbose, dryRun));
80+
await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, destination, archive, force, verbose, dryRun));
6381
}
6482
else
6583
{
66-
await Program.Settings.ForEachVaultAsync(vault => RestoreSystemAsync(vault, timestamp, remap, force, verbose, dryRun));
84+
await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, destination, archive, force, verbose, dryRun));
6785
}
6886
}
6987
}
7088

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

80-
foreach (string path in syncManager.RemoteVault.BackupDirectories)
98+
foreach (PullRecord record in syncManager.RemoteVault.PullDirectories.Where(r => r.Machine == Environment.MachineName))
8199
{
82-
await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun);
100+
await PullInternalAsync(syncManager, record.Source, timestamp, (output ?? record.Destination), archive, force, verbose, dryRun);
83101
}
84102
}
85103

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

95-
await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun);
113+
await PullInternalAsync(syncManager, path, timestamp, destination, archive, force, verbose, dryRun);
96114
}
97115

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

104122
ConcurrentBag<LocalFile> restoreFiles = new();
105123
System.Threading.Tasks.Parallel.ForEach(files, ParallelConfig.Options, (file) =>
106124
{
107125
//string sourcePath = file.Fullname;
108126
//string outputPath = string.IsNullOrEmpty(output) ? sourcePath : PathBuilder.ReplacePath(sourcePath, path, output);
109-
string outputPath = PathBuilder.ReplacePath(file.Fullname, path, output);
127+
string outputPath = PathBuilder.ReplacePath(file.Fullname, path, destination);
110128
if (File.Exists(outputPath) && !FileScanner.HasChanged(file, new LocalFile(outputPath)) && !force) return;
111-
129+
112130
file.Fullname = outputPath;
113131
restoreFiles.Add(file);
114132
});
@@ -123,18 +141,92 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D
123141
{
124142
string fileName = PathBuilder.TempFile;
125143
await File.WriteAllLinesAsync(fileName, restoreFiles.Select(f => f.Fullname).OrderBy(f => f));
126-
CommandLine.WriteLine($"This operation will restore {restoreFiles.Count:N0} files into: {(string.IsNullOrEmpty(output) ? path : output)}", ConsoleColor.Green);
144+
CommandLine.WriteLine($"This operation will pull {restoreFiles.Count:N0} files into: {(string.IsNullOrEmpty(destination) ? path : destination)}", ConsoleColor.Green);
127145
CommandLine.WriteLine($"A detailed list can be found here: {fileName}", ConsoleColor.DarkGray);
128146
}
129147
else
130148
{
131-
CommandLine.WriteLine(syncManager.RemoteVault, $"Restoring {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray);
132-
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
133-
int restoredFiles = await syncManager.RestoreFilesAsync(restoreFiles.ToArray(), progressReporter);
134-
135-
CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully restored {restoredFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
149+
CommandLine.WriteLine(syncManager.RemoteVault, $"Pulling {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray);
150+
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
151+
int pulledFiles = await syncManager.PullFilesAsync(restoreFiles.ToArray(), progressReporter);
152+
153+
CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pulled {pulledFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
136154
await syncManager.DisconnectAsync();
137155
}
138156
}
157+
158+
#region Add
159+
160+
private async Task HandleAddAsync(string path, string? config, string? destination)
161+
{
162+
LocalVaultConfig? localVault = string.IsNullOrEmpty(config) ? ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled) : ParallelConfig.GetVault(config);
163+
if (!string.IsNullOrEmpty(config) && localVault != null)
164+
{
165+
await AddPathAsync(localVault, path, destination);
166+
}
167+
else
168+
{
169+
await Program.Settings.ForEachVaultAsync(vault => AddPathAsync(vault, path, destination));
170+
}
171+
}
172+
173+
private async Task AddPathAsync(LocalVaultConfig vault, string path, string? destination)
174+
{
175+
ISyncManager? syncManager = SyncManager.CreateNew(vault);
176+
if (syncManager == null || !await syncManager.ConnectAsync())
177+
{
178+
CommandLine.WriteLine(vault, "Failed to connect to vault!", ConsoleColor.Red);
179+
return;
180+
}
181+
182+
PullRecord record = new(path, destination);
183+
if (!syncManager.RemoteVault.PullDirectories.Add(record))
184+
{
185+
CommandLine.WriteLine(vault, $"Unable to add path: '{path}'", ConsoleColor.Yellow);
186+
return;
187+
}
188+
189+
CommandLine.WriteLine(vault, $"Successfully added '{path}'", ConsoleColor.Green);
190+
await syncManager.DisconnectAsync();
191+
}
192+
193+
#endregion
194+
195+
#region Remove
196+
197+
private async Task HandleRemoveAsync(string path, string? config)
198+
{
199+
LocalVaultConfig? localVault = string.IsNullOrEmpty(config) ? ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled) : ParallelConfig.GetVault(config);
200+
if (!string.IsNullOrEmpty(config) && localVault != null)
201+
{
202+
await RemovePathAsync(localVault, path);
203+
}
204+
else
205+
{
206+
await Program.Settings.ForEachVaultAsync(vault => RemovePathAsync(vault, path));
207+
}
208+
}
209+
210+
private async Task RemovePathAsync(LocalVaultConfig vault, string path)
211+
{
212+
ISyncManager? syncManager = SyncManager.CreateNew(vault);
213+
if (syncManager == null || !await syncManager.ConnectAsync())
214+
{
215+
CommandLine.WriteLine(vault, "Failed to connect to vault!", ConsoleColor.Red);
216+
return;
217+
}
218+
219+
IEnumerable<PullRecord> records = syncManager.RemoteVault.PullDirectories.Where(r => r.Machine == Environment.MachineName && r.Source == path);
220+
foreach (PullRecord record in records)
221+
{
222+
if (syncManager.RemoteVault.PullDirectories.Remove(record)) continue;
223+
CommandLine.WriteLine(vault, $"Unable to remove path: '{path}'", ConsoleColor.Yellow);
224+
}
225+
226+
CommandLine.WriteLine(vault, $"Successfully removed '{path}'", ConsoleColor.Green);
227+
await syncManager.DisconnectAsync();
228+
}
229+
230+
#endregion
139231
}
140232
}

Parallel.Cli/Commands/ScrubCommand.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2026 Kyle Ebbinga
1+
// Copyright 2026 Entex Interactive
22

33
using System.CommandLine;
44
using System.Diagnostics;
@@ -24,7 +24,7 @@ public ScrubCommand() : base("scrub", "Verifies file integrity within a vault.")
2424
this.AddOption(_sourceOpt);
2525
this.AddOption(_configOpt);
2626
this.AddOption(_verboseOpt);
27-
this.SetHandler(HandleScrubAsync, _sourceOpt, _configOpt, _verboseOpt);
27+
this.SetHandler(HandleScrubAsync, _sourceOpt, _configOpt, _verboseOpt);
2828
}
2929

3030
private async Task HandleScrubAsync(string? path, string? config, bool verbose)
@@ -64,7 +64,7 @@ private async Task ScrubSystemAsync(LocalVaultConfig vault, bool verbose)
6464
return;
6565
}
6666

67-
foreach (string path in syncManager.RemoteVault.BackupDirectories)
67+
foreach (string path in syncManager.RemoteVault.PushDirectories)
6868
{
6969
await ScrubInternalAsync(syncManager, path, verbose);
7070
}
@@ -91,11 +91,11 @@ private async Task ScrubInternalAsync(ISyncManager syncManager, string path, boo
9191
CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow);
9292
return;
9393
}
94-
94+
9595
CommandLine.WriteLine(syncManager.RemoteVault, $"Scrubbing {files.Count:N0} files...", ConsoleColor.DarkGray);
9696
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, files.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
9797
int scrubbedFiles = await syncManager.ScrubFilesAsync(files, progressReporter);
98-
98+
9999
CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully scrubbed {scrubbedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
100100
await syncManager.DisconnectAsync();
101101
}

0 commit comments

Comments
 (0)