diff --git a/Parallel.Cli/Commands/CleanCommand.cs b/Parallel.Cli/Commands/CleanCommand.cs index 7828562..9355249 100644 --- a/Parallel.Cli/Commands/CleanCommand.cs +++ b/Parallel.Cli/Commands/CleanCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; diff --git a/Parallel.Cli/Commands/DuplicatesCommand.cs b/Parallel.Cli/Commands/DuplicatesCommand.cs index f033a52..363f891 100644 --- a/Parallel.Cli/Commands/DuplicatesCommand.cs +++ b/Parallel.Cli/Commands/DuplicatesCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; @@ -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); } diff --git a/Parallel.Cli/Commands/FetchCommand.cs b/Parallel.Cli/Commands/FetchCommand.cs index 2e7d906..a5a8305 100644 --- a/Parallel.Cli/Commands/FetchCommand.cs +++ b/Parallel.Cli/Commands/FetchCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; @@ -25,7 +25,7 @@ private async Task HandleFetchAsync(string config) await FetchVaultAsync(localVault); return; } - + await Program.Settings.ForEachVaultAsync(FetchVaultAsync); } @@ -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(); } diff --git a/Parallel.Cli/Commands/HistoryCommand.cs b/Parallel.Cli/Commands/HistoryCommand.cs index 8541582..0a3cfc9 100644 --- a/Parallel.Cli/Commands/HistoryCommand.cs +++ b/Parallel.Cli/Commands/HistoryCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; diff --git a/Parallel.Cli/Commands/IgnoreCommand.cs b/Parallel.Cli/Commands/IgnoreCommand.cs index d450792..7c9a1b1 100644 --- a/Parallel.Cli/Commands/IgnoreCommand.cs +++ b/Parallel.Cli/Commands/IgnoreCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; diff --git a/Parallel.Cli/Commands/PruneCommand.cs b/Parallel.Cli/Commands/PruneCommand.cs index 784e9e0..e328c21 100644 --- a/Parallel.Cli/Commands/PruneCommand.cs +++ b/Parallel.Cli/Commands/PruneCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Diagnostics; @@ -135,7 +135,7 @@ private async Task PruneInternalAsync(ISyncManager syncManager, string path, Dat { files = await (syncManager.Database?.GetFilesAsync(path, timestamp, true) ?? Task.FromResult>([])); } - + if (files.Count == 0) { CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow); diff --git a/Parallel.Cli/Commands/RestoreCommand.cs b/Parallel.Cli/Commands/RestoreCommand.cs index 317664c..c2415b4 100644 --- a/Parallel.Cli/Commands/RestoreCommand.cs +++ b/Parallel.Cli/Commands/RestoreCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using System.CommandLine; @@ -19,27 +19,45 @@ public class RestoreCommand : Command { private Stopwatch _sw = new Stopwatch(); - private readonly Option _sourceOpt = new(["--path", "-p"], "The source path to restore."); + private readonly Argument _sourceArg = new("path", "The path to add or remove."); + private readonly Option _sourceOpt = new(["--path", "-p"], "The source path to pull."); private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); - private readonly Option _beforeOpt = new(["--before"], "Restores files before a certain timestamp."); - private readonly Option _remapOpt = new(["--remap"], "The new directory to map restored files to."); - private readonly Option _forceOpt = new(["--force", "-f"], "Forces restoring, bypassing safe guards."); + private readonly Option _beforeOpt = new(["--before"], "Pulls files before a certain timestamp."); + private readonly Option _destOpt = new(["--destination"], "The new directory to map pulled files to."); + private readonly Option _archiveOpt = new(["--archive", "-a"], "Pulls only archived files."); + private readonly Option _forceOpt = new(["--force", "-f"], "Forces pulling, bypassing safe guards."); private readonly Option _dryRunOpt = new(["--dry-run"], "Previews the command without executing it."); private readonly Option _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; @@ -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()) @@ -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()) @@ -92,13 +110,13 @@ 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 files = await (syncManager.Database?.GetLatestFilesAsync(path, timestamp) ?? Task.FromResult>([])); + IReadOnlyList files = await (syncManager.Database?.GetLatestFilesAsync(path, timestamp, archive) ?? Task.FromResult>([])); Log.Debug($"GetLatestFilesAsync returned {files.Count} files for path '{path}'"); ConcurrentBag restoreFiles = new(); @@ -106,9 +124,9 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D { //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); }); @@ -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 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 } } \ No newline at end of file diff --git a/Parallel.Cli/Commands/ScrubCommand.cs b/Parallel.Cli/Commands/ScrubCommand.cs index 84b10f8..3ee375d 100644 --- a/Parallel.Cli/Commands/ScrubCommand.cs +++ b/Parallel.Cli/Commands/ScrubCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Diagnostics; @@ -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) @@ -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); } @@ -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(); } diff --git a/Parallel.Cli/Commands/SnapshotsCommand.cs b/Parallel.Cli/Commands/SnapshotsCommand.cs index 23d2a00..1abe2c6 100644 --- a/Parallel.Cli/Commands/SnapshotsCommand.cs +++ b/Parallel.Cli/Commands/SnapshotsCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using System.CommandLine; @@ -16,26 +16,26 @@ namespace Parallel.Cli.Commands public class SnapshotsCommand : Command { private Stopwatch _sw = new Stopwatch(); - + private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); private readonly Option _nameOpt = new(["--name", "-n"], "The name of the snapshot."); private readonly Option _jsonOpt = new(["--json", "-j"], "The json path of the snapshot."); private readonly Option _verboseOpt = new(["--verbose", "-v"], "Shows verbose output."); - + private readonly Command createCmd = new("create", "Creates a new system snapshot."); private readonly Command listCmd = new("list", "Lists available system snapshots."); private readonly Command restoreCmd = new("restore", "Restores a system snapshot."); - + public SnapshotsCommand() : base("snapshots", "Manages system snapshots.") { this.AddCommand(createCmd); createCmd.AddOption(_configOpt); createCmd.SetHandler(HandleCreateSnapshotAsync, _configOpt); - + this.AddCommand(listCmd); listCmd.AddOption(_configOpt); listCmd.SetHandler(HandleListSnapshotsAsync, _configOpt); - + this.AddCommand(restoreCmd); restoreCmd.AddOption(_configOpt); restoreCmd.AddOption(_nameOpt); @@ -57,7 +57,7 @@ private async Task HandleCreateSnapshotAsync(string? config) await Program.Settings.ForEachVaultAsync(CreateSnapshotAsync); } } - + private async Task HandleListSnapshotsAsync(string? config) { LocalVaultConfig? localVault = string.IsNullOrEmpty(config) ? ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled) : ParallelConfig.GetVault(config); @@ -66,7 +66,7 @@ private async Task HandleListSnapshotsAsync(string? config) CommandLine.WriteLine($"No vault was found!", ConsoleColor.Yellow); return; } - + CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray); ISyncManager? syncManager = SyncManager.CreateNew(localVault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -81,10 +81,10 @@ private async Task HandleListSnapshotsAsync(string? config) CommandLine.WriteLine($"No snapshots were found!", ConsoleColor.Yellow); return; } - + CommandLine.WriteArray("Available snapshots", snapshots); } - + private async Task HandleRestoreSnapshotAsync(string? config, string? name, string? jsonPath, bool verbose) { _sw = Stopwatch.StartNew(); @@ -98,7 +98,7 @@ private async Task HandleRestoreSnapshotAsync(string? config, string? name, stri await Program.Settings.ForEachVaultAsync(vault => RestoreSnapshotAsync(vault, name, jsonPath, verbose)); } } - + private async Task CreateSnapshotAsync(LocalVaultConfig vault) { ISyncManager? syncManager = SyncManager.CreateNew(vault); @@ -109,24 +109,24 @@ private async Task CreateSnapshotAsync(LocalVaultConfig vault) } ConcurrentBag snapshots = new(); - await System.Threading.Tasks.Parallel.ForEachAsync(syncManager.RemoteVault.BackupDirectories, ParallelConfig.Options, async (path, ct) => + await System.Threading.Tasks.Parallel.ForEachAsync(syncManager.RemoteVault.PushDirectories, ParallelConfig.Options, async (path, ct) => { IReadOnlyList files = await (syncManager.Database?.GetLatestFilesAsync(path, DateTime.UtcNow, false) ?? Task.FromResult>([])); foreach (LocalFile file in files) snapshots.Add(new SnapshotRecord(file)); }); - + Log.Debug($"Found {snapshots.Count} files"); string snapshotFilename = $"snapshot_{UnixTime.Now.TotalMilliseconds}"; string localSnapshotFile = Path.Combine(PathBuilder.TempDirectory, snapshotFilename + ".json"); string remoteSnapshotFile = PathBuilder.GetSnapshotFile(vault, snapshotFilename); - + await File.WriteAllTextAsync(localSnapshotFile, JsonConvert.SerializeObject(snapshots)); await syncManager.StorageProvider.UploadFileAsync(new LocalFile(localSnapshotFile), remoteSnapshotFile); if (!await (syncManager.Database?.AddSnapshotAsync(snapshotFilename) ?? Task.FromResult(false))) Log.Error($"Failed to add snapshot: {snapshotFilename}"); CommandLine.WriteLine(syncManager.LocalVault, $"Successfully created snapshot: {snapshotFilename}", ConsoleColor.Green); await syncManager.DisconnectAsync(); } - + private async Task RestoreSnapshotAsync(LocalVaultConfig vault, string? name, string? jsonPath, bool verbose) { ISyncManager? syncManager = SyncManager.CreateNew(vault); @@ -135,7 +135,7 @@ private async Task RestoreSnapshotAsync(LocalVaultConfig vault, string? name, st CommandLine.WriteLine(vault, "Failed to connect to vault!", ConsoleColor.Red); return; } - + string? snapshotFile = File.Exists(jsonPath) ? jsonPath : null; if (string.IsNullOrEmpty(snapshotFile)) { @@ -150,7 +150,7 @@ private async Task RestoreSnapshotAsync(LocalVaultConfig vault, string? name, st } CommandLine.WriteLine($"Loading snapshot {snapshotFile}"); - + //string remoteSnapshotFile = PathBuilder.GetSnapshotFile(vault, snapshotFilename); //string localSnapshotFile = Path.Combine(PathBuilder.TempDirectory, snapshotFilename + ".json"); } diff --git a/Parallel.Cli/Commands/StatsCommand.cs b/Parallel.Cli/Commands/StatsCommand.cs deleted file mode 100644 index 6ce8b6a..0000000 --- a/Parallel.Cli/Commands/StatsCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2026 Kyle Ebbinga - -using System.CommandLine; -using Parallel.Cli.Utils; -using Parallel.Core.Database; -using Parallel.Core.IO.Syncing; -using Parallel.Core.Settings; -using Parallel.Core.Storage; -using Parallel.Core.Utils; - -namespace Parallel.Cli.Commands -{ - public class StatsCommand : Command - { - private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); - - public StatsCommand() : base("stats", "Displays various vault statistics.") - { - this.AddOption(_configOpt); - this.SetHandler(async (config) => - { - LocalVaultConfig? vault = ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled); - if (!string.IsNullOrEmpty(config)) vault = ParallelConfig.GetVault(config); - if (vault == null) - { - CommandLine.WriteLine($"No vault was found!", ConsoleColor.Yellow); - return; - } - - await DisplayDiskInformationAsync(vault); - }, _configOpt); - } - - private async Task DisplayDiskInformationAsync(LocalVaultConfig vault) - { - CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray); - ISyncManager? syncManager = SyncManager.CreateNew(vault); - if (syncManager == null || !await syncManager.ConnectAsync()) - { - CommandLine.WriteLine(vault, $"Failed to connect to vault!", ConsoleColor.Red); - return; - } - - IDatabase? db = syncManager.Database; - long localSize = await (db?.GetCurrentSizeAsync() ?? Task.FromResult(0L)); - long totalSize = await (db?.GetTotalSizeAsync() ?? Task.FromResult(0L)); - long totalFiles = await (db?.GetTotalFilesAsync() ?? Task.FromResult(0L)); - long totalLocalFiles = await (db?.GetTotalFilesAsync(false) ?? Task.FromResult(0L)); - long totalDeletedFiles = await (db?.GetTotalFilesAsync(true) ?? Task.FromResult(0L)); - long totalRevisedFiles = await (db?.GetTotalRevisedFilesAsync() ?? Task.FromResult(0L)); - - CommandLine.WriteLine($"Using vault '{syncManager.RemoteVault.Name}' ({vault.Id}):"); - CommandLine.WriteLine($"Service Type: {vault.Credentials.Service}"); - CommandLine.WriteLine($"Root Directory: {vault.Credentials.RootDirectory}"); - CommandLine.WriteLine($"Managed Files: {totalFiles:N0}"); - CommandLine.WriteLine($"Local Files: {totalLocalFiles:N0}"); - CommandLine.WriteLine($"Deleted Files: {totalDeletedFiles:N0}"); - CommandLine.WriteLine($"Revisions: {totalRevisedFiles:N0}"); - CommandLine.WriteLine($"Local Size: {Formatter.FromBytes(localSize)}"); - CommandLine.WriteLine($"Remote Size: {Formatter.FromBytes(totalSize)}"); - - if (vault.Credentials.Service.Equals(FileService.Local)) - { - DriveInfo drive = new(vault.Credentials.RootDirectory); - long diskUsage = drive.TotalSize - drive.TotalFreeSpace; - CommandLine.WriteLine($"Total Usage: {Formatter.FromBytes(diskUsage)} ({Math.Round(diskUsage / (double)drive.TotalSize * 100, 1)}%)"); - CommandLine.WriteLine($"Disk Usage: {Formatter.FromBytes(diskUsage - totalSize)} ({Math.Round((diskUsage - totalSize) / (double)drive.TotalSize * 100, 1)}%)"); - CommandLine.WriteLine($"Disk Free: {Formatter.FromBytes(drive.TotalFreeSpace)} ({Math.Round(drive.TotalFreeSpace / (double)drive.TotalSize * 100, 1)}%)"); - CommandLine.WriteLine($"Disk Total: {Formatter.FromBytes(drive.TotalSize)}"); - } - } - } -} \ No newline at end of file diff --git a/Parallel.Cli/Commands/SyncCommand.cs b/Parallel.Cli/Commands/SyncCommand.cs index a189404..1084fcf 100644 --- a/Parallel.Cli/Commands/SyncCommand.cs +++ b/Parallel.Cli/Commands/SyncCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Diagnostics; @@ -27,13 +27,13 @@ public class SyncCommand : Command 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 SyncCommand() : base("sync", "Syncs the system with the vaults.") + public SyncCommand() : base("sync", "Syncs system files with vaults.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); this.AddOption(_forceOpt); this.AddOption(_verboseOpt); - this.SetHandler(HandleSyncAsync, _sourceOpt, _configOpt, _forceOpt, _verboseOpt); + this.SetHandler(HandlePushAsync, _sourceOpt, _configOpt, _forceOpt, _verboseOpt); this.AddCommand(addCmd); addCmd.AddArgument(_sourceArg); @@ -46,9 +46,9 @@ public SyncCommand() : base("sync", "Syncs the system with the vaults.") removeCmd.SetHandler(HandleRemoveAsync, _sourceArg, _configOpt); } - #region Sync + #region Push - private async Task HandleSyncAsync(string? path, string? config, bool force, bool verbose) + private async Task HandlePushAsync(string? path, string? config, bool force, bool verbose) { _sw = Stopwatch.StartNew(); LocalVaultConfig? localVault = ParallelConfig.GetVault(config); @@ -56,27 +56,27 @@ private async Task HandleSyncAsync(string? path, string? config, bool force, boo { if (!string.IsNullOrEmpty(path)) { - await SyncPathAsync(localVault, path, force, verbose); + await PushPathAsync(localVault, path, force, verbose); } else { - await SyncSystemAsync(localVault, force, verbose); + await PushSystemAsync(localVault, force, verbose); } } else { if (!string.IsNullOrEmpty(path)) { - await Program.Settings.ForEachVaultAsync(vault => SyncPathAsync(vault, path, force, verbose)); + await Program.Settings.ForEachVaultAsync(vault => PushPathAsync(vault, path, force, verbose)); } else { - await Program.Settings.ForEachVaultAsync(vault => SyncSystemAsync(vault, force, verbose)); + await Program.Settings.ForEachVaultAsync(vault => PushSystemAsync(vault, force, verbose)); } } } - private async Task SyncSystemAsync(LocalVaultConfig vault, bool force, bool verbose) + private async Task PushSystemAsync(LocalVaultConfig vault, bool force, bool verbose) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -85,13 +85,15 @@ private async Task SyncSystemAsync(LocalVaultConfig vault, bool force, bool verb return; } - foreach (string path in syncManager.RemoteVault.BackupDirectories) + foreach (string path in syncManager.RemoteVault.PushDirectories) { - await SyncInternalAsync(syncManager, path, force, verbose); + await PushInternalAsync(syncManager, path, force, verbose); } + + await syncManager.DisconnectAsync(); } - private async Task SyncPathAsync(LocalVaultConfig vault, string path, bool force, bool verbose) + private async Task PushPathAsync(LocalVaultConfig vault, string path, bool force, bool verbose) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -100,13 +102,14 @@ private async Task SyncPathAsync(LocalVaultConfig vault, string path, bool force return; } - await SyncInternalAsync(syncManager, path, force, verbose); + await PushInternalAsync(syncManager, path, force, verbose); + await syncManager.DisconnectAsync(); } - private async Task SyncInternalAsync(ISyncManager syncManager, string path, bool force, bool verbose) + private async Task PushInternalAsync(ISyncManager syncManager, string path, bool force, bool verbose) { // Normalize paths for safe comparison - string[] backupFolders = syncManager.RemoteVault.BackupDirectories.ToArray(); + string[] backupFolders = syncManager.RemoteVault.PushDirectories.ToArray(); string[] ignoredFolders = syncManager.RemoteVault.IgnoreDirectories.ToArray(); bool isFile = PathBuilder.IsFile(path); @@ -132,12 +135,11 @@ private async Task SyncInternalAsync(ISyncManager syncManager, string path, bool return; } - CommandLine.WriteLine(syncManager.RemoteVault, $"Syncing {files.Length:N0} files...", ConsoleColor.DarkGray); + CommandLine.WriteLine(syncManager.RemoteVault, $"Pushing {files.Length:N0} files...", ConsoleColor.DarkGray); IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, successFiles) : new LoggingProgressReporter(syncManager.RemoteVault); - int backedUpFiles = await syncManager.BackupFilesAsync(files, progressReporter, force); - - CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully synced {backedUpFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); - await syncManager.DisconnectAsync(); + int pushedFiles = await syncManager.PushFilesAsync(files, progressReporter, force); + + CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pushed {pushedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); } #endregion @@ -166,7 +168,7 @@ private async Task AddPathAsync(LocalVaultConfig vault, string path) return; } - if (!syncManager.RemoteVault.BackupDirectories.Add(path)) + if (!syncManager.RemoteVault.PushDirectories.Add(path)) { CommandLine.WriteLine(vault, $"Unable to add path: '{path}'", ConsoleColor.Yellow); return; @@ -202,7 +204,7 @@ private async Task RemovePathAsync(LocalVaultConfig vault, string path) return; } - if (!syncManager.RemoteVault.BackupDirectories.Remove(path)) + if (!syncManager.RemoteVault.PushDirectories.Remove(path)) { CommandLine.WriteLine(vault, $"Unable to remove path: '{path}'", ConsoleColor.Yellow); return; diff --git a/Parallel.Cli/Commands/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs index af14330..3dc9ca1 100644 --- a/Parallel.Cli/Commands/UnzipCommand.cs +++ b/Parallel.Cli/Commands/UnzipCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Diagnostics; @@ -13,7 +13,7 @@ public class UnzipCommand : Command private Stopwatch? _sw; private readonly List _tasks = new List(); - private int _totalTasks = 0; + private readonly int _totalTasks = 0; public UnzipCommand() : base("unzip", "Unzips files in a directory.") { @@ -21,7 +21,7 @@ public UnzipCommand() : base("unzip", "Unzips files in a directory.") this.SetHandler(HandleUnzipAsync, sourceArg); } - private async Task HandleUnzipAsync(string path) + private Task HandleUnzipAsync(string path) { _sw = Stopwatch.StartNew(); CommandLine.WriteLine($"Scanning for files in {path}...", ConsoleColor.DarkGray); @@ -29,11 +29,11 @@ private async Task HandleUnzipAsync(string path) if (files.Length == 0) { CommandLine.WriteLine("No files found to unzip!", ConsoleColor.Yellow); - return; + return Task.CompletedTask; } CommandLine.WriteLine($"Unzipping {files.Length:N0} files...", ConsoleColor.DarkGray); - ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; + ParallelOptions options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; System.Threading.Tasks.Parallel.ForEach(files, options, (file, ct) => { using (FileStream openFile = File.OpenRead(file)) @@ -42,12 +42,13 @@ private async Task HandleUnzipAsync(string path) { gZip.CopyTo(createFile); } - + File.SetAttributes(file, File.GetAttributes(file) & ~FileAttributes.ReadOnly); File.Delete(file); }); CommandLine.WriteLine($"Successfully unzipped {files.Length:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); + return Task.CompletedTask; } private void DecompressFile(string path, bool keep) diff --git a/Parallel.Cli/Commands/VaultsCommand.cs b/Parallel.Cli/Commands/VaultsCommand.cs index 4129322..aeb584e 100644 --- a/Parallel.Cli/Commands/VaultsCommand.cs +++ b/Parallel.Cli/Commands/VaultsCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using Parallel.Cli.Utils; @@ -21,6 +21,7 @@ public class VaultsCommand : Command private readonly Command findCmd = new("find", "Finds vault configurations in a location."); private readonly Command viewCmd = new("view", "Shows the vault configuration."); private readonly Command setCmd = new("set", "Sets a new vault configuration."); + private readonly Command statsCmd = new("stats", "Displays various vault statistics."); private readonly Command delCmd = new("delete", "Deletes a vault configuration."); public VaultsCommand() : base("vaults", "View or edit the vaults.") @@ -30,7 +31,7 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.") CommandLine.WriteLine("Active vaults:"); foreach (LocalVaultConfig vault in Program.Settings.Vaults.OrderBy(v => v.Name)) { - CommandLine.WriteLine($"[{vault.Id}]: {vault.Name} {(vault.Enabled ? "(Syncing)" : string.Empty)}"); + CommandLine.WriteLine($"[{vault.Id}]: {vault.Name} {(vault.Enabled ? "(Active)" : string.Empty)}"); } }); @@ -53,7 +54,7 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.") string? bucketInput = CommandLine.ReadString("Bucket Name (Leave empty for default)"); string bucketName = string.IsNullOrEmpty(bucketInput) ? "parallel" : bucketInput; spc.RootDirectory = bucketName; - + string? regionInput = CommandLine.ReadString("Region Name (Leave empty for default)"); string regionName = string.IsNullOrEmpty(regionInput) ? "us-east-1" : regionInput; spc.Region = regionName; @@ -112,7 +113,8 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.") RemoteVaultConfig remoteVault = syncManager.RemoteVault; CommandLine.WriteLine($"'{remoteVault.Name}' ({remoteVault.Id}):"); - CommandLine.WriteArray("Backup Directories", remoteVault.BackupDirectories); + CommandLine.WriteArray("Push Directories", remoteVault.PushDirectories); + CommandLine.WriteArray("Pull Directories", remoteVault.PullDirectories.Select(d => d.Source)); CommandLine.WriteArray("Ignore Directories", remoteVault.IgnoreDirectories); CommandLine.WriteArray("Prune Directories", remoteVault.PruneDirectories); CommandLine.WriteLine($"Prune Period: {remoteVault.PrunePeriod} days"); @@ -123,6 +125,60 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.") { }); + + this.AddCommand(statsCmd); + statsCmd.AddOption(configOpt); + statsCmd.SetHandler(async (config) => + { + LocalVaultConfig? vault = ParallelConfig.Load().Vaults.FirstOrDefault(v => v.Enabled); + if (!string.IsNullOrEmpty(config)) vault = ParallelConfig.GetVault(config); + if (vault == null) + { + CommandLine.WriteLine($"No vault was found!", ConsoleColor.Yellow); + return; + } + + await DisplayDiskInformationAsync(vault); + }, configOpt); + } + + private async Task DisplayDiskInformationAsync(LocalVaultConfig vault) + { + CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray); + ISyncManager? syncManager = SyncManager.CreateNew(vault); + if (syncManager == null || !await syncManager.ConnectAsync()) + { + CommandLine.WriteLine(vault, $"Failed to connect to vault!", ConsoleColor.Red); + return; + } + + IDatabase? db = syncManager.Database; + long localSize = await (db?.GetLocalSizeAsync() ?? Task.FromResult(0L)); + long totalSize = await (db?.GetTotalSizeAsync() ?? Task.FromResult(0L)); + long totalFiles = await (db?.GetTotalFilesAsync() ?? Task.FromResult(0L)); + long totalLocalFiles = await (db?.GetTotalFilesAsync(false) ?? Task.FromResult(0L)); + long totalDeletedFiles = await (db?.GetTotalFilesAsync(true) ?? Task.FromResult(0L)); + long totalRevisedFiles = await (db?.GetTotalRevisedFilesAsync() ?? Task.FromResult(0L)); + + CommandLine.WriteLine($"Using vault '{syncManager.RemoteVault.Name}' ({vault.Id}):"); + CommandLine.WriteLine($"Service Type: {vault.Credentials.Service}"); + CommandLine.WriteLine($"Root Directory: {vault.Credentials.RootDirectory}"); + CommandLine.WriteLine($"Managed Files: {totalFiles:N0}"); + CommandLine.WriteLine($"Local Files: {totalLocalFiles:N0}"); + CommandLine.WriteLine($"Deleted Files: {totalDeletedFiles:N0}"); + CommandLine.WriteLine($"Revisions: {totalRevisedFiles:N0}"); + CommandLine.WriteLine($"Local Size: {Formatter.FromBytes(localSize)}"); + CommandLine.WriteLine($"Remote Size: {Formatter.FromBytes(totalSize)}"); + + if (vault.Credentials.Service.Equals(FileService.Local)) + { + DriveInfo drive = new(vault.Credentials.RootDirectory); + long diskUsage = drive.TotalSize - drive.TotalFreeSpace; + CommandLine.WriteLine($"Total Usage: {Formatter.FromBytes(diskUsage)} ({Math.Round(diskUsage / (double)drive.TotalSize * 100, 1)}%)"); + CommandLine.WriteLine($"Disk Usage: {Formatter.FromBytes(diskUsage - totalSize)} ({Math.Round((diskUsage - totalSize) / (double)drive.TotalSize * 100, 1)}%)"); + CommandLine.WriteLine($"Disk Free: {Formatter.FromBytes(drive.TotalFreeSpace)} ({Math.Round(drive.TotalFreeSpace / (double)drive.TotalSize * 100, 1)}%)"); + CommandLine.WriteLine($"Disk Total: {Formatter.FromBytes(drive.TotalSize)}"); + } } } } \ No newline at end of file diff --git a/Parallel.Cli/Commands/ZipCommand.cs b/Parallel.Cli/Commands/ZipCommand.cs index 3316284..df8737f 100644 --- a/Parallel.Cli/Commands/ZipCommand.cs +++ b/Parallel.Cli/Commands/ZipCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Diagnostics; diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index 3f1c2a9..fb64575 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -40,11 +40,15 @@ + + + + - - + + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index 7c37c0a..b563b40 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -1,13 +1,11 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; -using System.Diagnostics; using System.Reflection; using Parallel.Cli.Utils; using Parallel.Core.IO; using Parallel.Core.Settings; using Parallel.Core.Utils; -using Serilog.Events; namespace Parallel.Cli { @@ -29,7 +27,7 @@ public static async Task Main(string[] args) #if DEBUG Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); #else - Log.Logger = new LoggerConfiguration().WriteTo.File(PathBuilder.LogFile).CreateLogger(); + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.File(PathBuilder.LogFile).CreateLogger(); #endif Log.Information($"{assembly.Name} [Version {assembly.Version}]"); diff --git a/Parallel.Cli/Services/CleanService.cs b/Parallel.Cli/Services/CleanService.cs new file mode 100644 index 0000000..d9e5754 --- /dev/null +++ b/Parallel.Cli/Services/CleanService.cs @@ -0,0 +1,18 @@ +// Copyright 2026 Entex Interactive + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Cli.Services +{ + public class CleanService : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Log.Debug("Clean: {DateTimeOffset}", DateTimeOffset.Now); + await Task.Delay(2000, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Services/LifetimeService.cs b/Parallel.Cli/Services/LifetimeService.cs new file mode 100644 index 0000000..b7b499d --- /dev/null +++ b/Parallel.Cli/Services/LifetimeService.cs @@ -0,0 +1,55 @@ +// Copyright 2026 Entex Interactive + +using Microsoft.Extensions.Hosting; +using Parallel.Core.IO; +using Parallel.Core.Settings; +using Parallel.Core.Utils; + +namespace Parallel.Cli.Services +{ + public class LifetimeService : BackgroundService + { + //private readonly LogEventTracker _logEventTracker; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Task loadTask = RunLoadTaskAsync(stoppingToken); + Task logTask = RunLogTasksAsync(stoppingToken); + await Task.WhenAll(loadTask, logTask); + } + } + + private async Task RunLoadTaskAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + Program.Settings = ParallelConfig.Load(); + } + } + + private async Task RunLogTasksAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(GetNextDay(), stoppingToken); + /*await Log.CloseAndFlushAsync(); + + if (_logEventTracker.ErrorCount <= 0) continue; + + string logDir = Path.Combine(PathBuilder.ProgramData, "Logs"); + if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir); + File.Move(PathBuilder.LogFile, Path.Combine(logDir, $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log"));*/ + } + } + + private static TimeSpan GetNextDay() + { + DateTime current = DateTime.UtcNow; + DateTime nextMidnight = current.AddDays(1); + return nextMidnight - current; + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Services/PruneService.cs b/Parallel.Cli/Services/PruneService.cs new file mode 100644 index 0000000..5638cb7 --- /dev/null +++ b/Parallel.Cli/Services/PruneService.cs @@ -0,0 +1,18 @@ +// Copyright 2026 Entex Interactive + +using Microsoft.Extensions.Hosting; + +namespace Parallel.Cli.Services +{ + public class PruneService : BackgroundService + { + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Log.Debug("Prune: {DateTimeOffset}", DateTimeOffset.Now); + await Task.Delay(2000, stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Services/RunCommand.cs b/Parallel.Cli/Services/RunCommand.cs new file mode 100644 index 0000000..85a06ad --- /dev/null +++ b/Parallel.Cli/Services/RunCommand.cs @@ -0,0 +1,41 @@ +// Copyright 2026 Entex Interactive + +using System.CommandLine; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Parallel.Cli.Services +{ + public class RunCommand : Command + { + private readonly Option _cleanOpt = new("-clean", "If the service should auto clean directories."); + private readonly Option _pruneOpt = new("-prune", "If the service should auto prune deleted files."); + private readonly Option _syncOpt = new("-sync", "If the service should auto sync files."); + + public RunCommand() : base("run", "Starts the background services.") + { + this.AddOption(_cleanOpt); + this.AddOption(_syncOpt); + this.AddOption(_pruneOpt); + this.SetHandler(HandleCommandAsync, _cleanOpt, _pruneOpt, _syncOpt); + } + + private async Task HandleCommandAsync(bool clean, bool prune, bool sync) + { + HostApplicationBuilder builder = Host.CreateApplicationBuilder(); + if (OperatingSystem.IsWindows()) builder.Services.AddWindowsService(); + if (OperatingSystem.IsLinux()) builder.Services.AddSystemd(); + + if (clean) builder.Services.AddHostedService(); + if (prune) builder.Services.AddHostedService(); + if (sync) builder.Services.AddHostedService(); + builder.Services.AddHostedService(); + + builder.Logging.ClearProviders(); + builder.Logging.AddSerilog(); + + await builder.Build().RunAsync(); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Services/SyncService.cs b/Parallel.Cli/Services/SyncService.cs new file mode 100644 index 0000000..9aab986 --- /dev/null +++ b/Parallel.Cli/Services/SyncService.cs @@ -0,0 +1,82 @@ +// Copyright 2026 Entex Interactive + +using System.Collections.Concurrent; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Parallel.Core.IO.Syncing; +using Parallel.Core.Settings; + +namespace Parallel.Cli.Services +{ + public class SyncWorker + { + public CancellationTokenSource Cts { get; } + public Task WorkerTask { get; } + + public SyncWorker(Task task, CancellationTokenSource cts) + { + WorkerTask = task; + Cts = cts; + } + } + + public class SyncService : BackgroundService + { + private readonly ConcurrentDictionary _vaults = new(); + private readonly ILogger _logger; + //private readonly TaskQueuer _queuer; + + public SyncService(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + LocalVaultConfig[] enabledVaults = ParallelConfig.GetEnabledVaults(); + string[] enabledVaultIds = enabledVaults.Select(v => v.Id).ToArray(); + string[] disabledVaults = _vaults.Keys.Where(v => !enabledVaultIds.Contains(v)).ToArray(); + + // Adds newly enabled vaults to be synced. + foreach (LocalVaultConfig vault in enabledVaults) + { + if (_vaults.ContainsKey(vault.Id)) continue; + + _logger.LogDebug("Adding vault to syncing..."); + CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + Task workerTask = SyncVaultAsync(new FileSyncManager(vault), cts.Token); + SyncWorker worker = new SyncWorker(workerTask, cts); + + if (!_vaults.TryAdd(vault.Id, worker)) continue; + _logger.LogInformation("Added vault to be synced: {VaultId}", vault.Id); + } + + // Removes disabled vaults from being synced + foreach (string id in disabledVaults) + { + _logger.LogDebug("Removing vault from syncing..."); + if (!_vaults.TryGetValue(id, out SyncWorker? worker)) continue; + await worker.Cts.CancelAsync(); + await worker.WorkerTask; + _vaults.TryRemove(id, out _); + + _logger.LogInformation("Removed vault from syncing: {VaultId}", id); + } + + await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); + } + } + + private async Task SyncVaultAsync(FileSyncManager syncManager, CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + Log.Debug($"{syncManager.Id} will sync every {syncManager.RemoteVault.SyncInterval} minutes"); + //await Task.Delay(TimeSpan.FromMinutes(syncManager.RemoteVault.SyncInterval), stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Utils/CommandLine.cs b/Parallel.Cli/Utils/CommandLine.cs index ecfc624..91c73dd 100644 --- a/Parallel.Cli/Utils/CommandLine.cs +++ b/Parallel.Cli/Utils/CommandLine.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using System.Text; diff --git a/Parallel.Cli/Utils/MarkdownGenerator.cs b/Parallel.Cli/Utils/MarkdownGenerator.cs index 9904c0e..73c9488 100644 --- a/Parallel.Cli/Utils/MarkdownGenerator.cs +++ b/Parallel.Cli/Utils/MarkdownGenerator.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Text; diff --git a/Parallel.Cli/Utils/ProgressReporter.cs b/Parallel.Cli/Utils/ProgressReporter.cs index 10e6e91..9c06c0e 100644 --- a/Parallel.Cli/Utils/ProgressReporter.cs +++ b/Parallel.Cli/Utils/ProgressReporter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using Parallel.Core.Diagnostics; diff --git a/Parallel.Cli/Utils/TextWriter.cs b/Parallel.Cli/Utils/TextWriter.cs index 79ed852..bca8379 100644 --- a/Parallel.Cli/Utils/TextWriter.cs +++ b/Parallel.Cli/Utils/TextWriter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Parallel.Core/Data/FileTypes.cs b/Parallel.Core/Data/FileTypes.cs index b119388..6f0125d 100644 --- a/Parallel.Core/Data/FileTypes.cs +++ b/Parallel.Core/Data/FileTypes.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System; using System.Collections.Generic; diff --git a/Parallel.Core/Database/Contexts/SemaphoreContext.cs b/Parallel.Core/Database/Contexts/SemaphoreContext.cs index 7542044..6dd4ad2 100644 --- a/Parallel.Core/Database/Contexts/SemaphoreContext.cs +++ b/Parallel.Core/Database/Contexts/SemaphoreContext.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Data; using Dapper; diff --git a/Parallel.Core/Database/Contexts/SqliteContext.cs b/Parallel.Core/Database/Contexts/SqliteContext.cs index e2cf226..cac3030 100644 --- a/Parallel.Core/Database/Contexts/SqliteContext.cs +++ b/Parallel.Core/Database/Contexts/SqliteContext.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Data; using Microsoft.Data.Sqlite; @@ -39,7 +39,7 @@ public async Task InitializeAsync() public async Task AddFileAsync(LocalFile file) { if (string.IsNullOrEmpty(file.Fullname)) throw new ArgumentNullException(nameof(file.Fullname)); - if (!file.TryGenerateCheckSums()) throw new ArgumentNullException(nameof(file.LocalCheckSum)); + if (!file.TryGenerateLocalCheckSum()) throw new ArgumentNullException(nameof(file.LocalCheckSum)); string sql = "INSERT OR REPLACE INTO objects (name, fullname, parentDir, lastWrite, lastUpdate, localSize, remoteSize, type, hidden, readOnly, deleted, localCheckSum, remoteCheckSum) VALUES (@Name, @Fullname, @ParentDirectory, @LastWrite, @LastUpdate, @LocalSize, @RemoteSize, @Type, @Hidden, @ReadOnly, @Deleted, @LocalCheckSum, @RemoteCheckSum);"; return await _semaphore.ExecuteAsync(sql, new { file.Name, file.Fullname, file.ParentDirectory, LastWrite = file.LastWrite.TotalMilliseconds, LastUpdate = UnixTime.Now.TotalMilliseconds, file.LocalSize, file.RemoteSize, Type = file.Type.ToString(), file.Hidden, file.ReadOnly, file.Deleted, file.LocalCheckSum, file.RemoteCheckSum }) > 0; @@ -53,19 +53,12 @@ public async Task RemoveFileAsync(LocalFile file) } /// - public async Task GetCurrentSizeAsync() + public async Task GetLocalSizeAsync() { string sql = "SELECT COALESCE(SUM(f.localsize), 0) FROM objects f JOIN (SELECT fullname, MAX(lastupdate) AS max_lastupdate FROM objects WHERE deleted = 0 GROUP BY fullname) latest ON f.fullname = latest.fullname AND f.lastupdate = latest.max_lastupdate;"; return await _semaphore.QuerySingleAsync(sql); } - /// - public async Task GetRemoteSizeAsync() - { - string sql = "SELECT COALESCE(SUM(f.remotesize), 0) FROM objects f JOIN (SELECT localchecksum, MAX(lastupdate) AS max_lastupdate FROM objects GROUP BY localchecksum) latest ON f.localchecksum = latest.localchecksum AND f.lastupdate = latest.max_lastupdate;"; - return await _semaphore.QuerySingleAsync(sql); - } - /// public async Task GetTotalSizeAsync() { @@ -106,13 +99,13 @@ public async Task> GetLatestFilesAsync(string path, Dat string sql = "SELECT * FROM (SELECT * FROM objects WHERE fullname LIKE @Path AND lastupdate <= @Time AND deleted = @deleted ORDER BY lastwrite DESC) GROUP BY fullname;"; return await _semaphore.QueryAsync(sql, new { Path = $"{path}%", Time = new UnixTime(timestamp).TotalMilliseconds, deleted }); } - + public async Task> GetRevisedFilesAsync(string path) { string sql = $"SELECT * FROM objects WHERE fullname LIKE '{path}%' AND lastupdate NOT IN (SELECT MAX(lastupdate) FROM objects GROUP BY fullname) ORDER BY lastupdate DESC;"; return await _semaphore.QueryAsync(sql, new { Path = $"{path}%" }); } - + /// public async Task> GetFilesAsync(string path, DateTime timestamp) { @@ -166,7 +159,7 @@ public async Task> GetHistoryAsync(string path) string sql = "SELECT * FROM history WHERE fullname LIKE @Fullname ORDER BY timestamp DESC;"; return await _semaphore.QueryAsync(sql, new { Fullname = $"%{path}%" }); } - + /// public async Task> GetHistoryAsync(string path, int limit) { @@ -180,7 +173,7 @@ public async Task> GetHistoryAsync(string path, Hist string sql = "SELECT * FROM history WHERE fullname LIKE @Fullname AND type = @type ORDER BY timestamp DESC;"; return await _semaphore.QueryAsync(sql, new { Fullname = $"%{path}%", type }); } - + /// public async Task> GetHistoryAsync(string path, HistoryType? type, int limit) { @@ -195,7 +188,7 @@ public async Task GetLastSyncTimeAsync() string sql = "SELECT lastupdate FROM objects ORDER BY lastupdate DESC LIMIT 1;"; return UnixTime.FromMilliseconds(await _semaphore.QuerySingleAsync(sql)).ToLocalTime(); } - + /// public async Task AddSnapshotAsync(string snapshot) { diff --git a/Parallel.Core/Database/IDatabase.cs b/Parallel.Core/Database/IDatabase.cs index ba716f0..b07209d 100644 --- a/Parallel.Core/Database/IDatabase.cs +++ b/Parallel.Core/Database/IDatabase.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.IO; using System; @@ -58,7 +58,7 @@ public interface IDatabase #endregion - #region Files + #region Objects /// /// Adds a new file or updates an existing one. @@ -94,7 +94,7 @@ public interface IDatabase /// /// Task> GetFilesAsync(string path, DateTime timestamp); - + /// /// Gets a list of files. /// @@ -111,8 +111,7 @@ public interface IDatabase /// Task GetFileAsync(string path); - Task GetCurrentSizeAsync(); - Task GetRemoteSizeAsync(); + Task GetLocalSizeAsync(); Task GetTotalSizeAsync(); Task GetTotalFilesAsync(); Task GetTotalFilesAsync(bool deleted); @@ -145,21 +144,21 @@ public interface IDatabase Task AddHistoryAsync(HistoryType type, LocalFile file); Task> GetHistoryAsync(string path); - + Task> GetHistoryAsync(string path, int limit); Task> GetHistoryAsync(string path, HistoryType? type); - + Task> GetHistoryAsync(string path, HistoryType? type, int limit); #endregion - + #region Snapshots - + Task AddSnapshotAsync(string snapshot); - + Task> GetSnapshotsAsync(); - + Task GetSnapshotAsync(string? name); Task RemoveSnapshotAsync(string snapshot); diff --git a/Parallel.Core/Diagnostics/IProgressReporter.cs b/Parallel.Core/Diagnostics/IProgressReporter.cs index 2baab53..aa43745 100644 --- a/Parallel.Core/Diagnostics/IProgressReporter.cs +++ b/Parallel.Core/Diagnostics/IProgressReporter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Models; diff --git a/Parallel.Core/Diagnostics/LoggingProgressReporter.cs b/Parallel.Core/Diagnostics/LoggingProgressReporter.cs index b261883..b5b096e 100644 --- a/Parallel.Core/Diagnostics/LoggingProgressReporter.cs +++ b/Parallel.Core/Diagnostics/LoggingProgressReporter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using Parallel.Core.Models; @@ -12,12 +12,12 @@ namespace Parallel.Core.Diagnostics public class LoggingProgressReporter : IProgressReporter { private readonly LocalVaultConfig _localVault; - + public LoggingProgressReporter(LocalVaultConfig localVault) { _localVault = localVault; } - + /// public void Report(ProgressOperation operation, LocalFile file) { diff --git a/Parallel.Core/Events/LocalFileEventArgs.cs b/Parallel.Core/Events/LocalFileEventArgs.cs index b0133e3..34fb1a0 100644 --- a/Parallel.Core/Events/LocalFileEventArgs.cs +++ b/Parallel.Core/Events/LocalFileEventArgs.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.IO; using System; diff --git a/Parallel.Core/Events/MessageRecievedEventArgs.cs b/Parallel.Core/Events/MessageRecievedEventArgs.cs index 509f61c..5ac64ea 100644 --- a/Parallel.Core/Events/MessageRecievedEventArgs.cs +++ b/Parallel.Core/Events/MessageRecievedEventArgs.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Net.Sockets; using System.Text; diff --git a/Parallel.Core/Events/TransferFailedEventArgs.cs b/Parallel.Core/Events/TransferFailedEventArgs.cs index c4e76ca..7e7eeb7 100644 --- a/Parallel.Core/Events/TransferFailedEventArgs.cs +++ b/Parallel.Core/Events/TransferFailedEventArgs.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Security.Cryptography.X509Certificates; using Parallel.Core.IO; diff --git a/Parallel.Core/Events/TransferUpdateEventArgs.cs b/Parallel.Core/Events/TransferUpdateEventArgs.cs index 0f680ee..af5a92b 100644 --- a/Parallel.Core/Events/TransferUpdateEventArgs.cs +++ b/Parallel.Core/Events/TransferUpdateEventArgs.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Security.Cryptography.X509Certificates; using Parallel.Core.IO; diff --git a/Parallel.Core/Extensions/Json/UnixTimeConverter.cs b/Parallel.Core/Extensions/Json/UnixTimeConverter.cs index 6903d54..bfe296a 100644 --- a/Parallel.Core/Extensions/Json/UnixTimeConverter.cs +++ b/Parallel.Core/Extensions/Json/UnixTimeConverter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Newtonsoft.Json; using Parallel.Core.Utils; diff --git a/Parallel.Core/IO/PathBuilder.cs b/Parallel.Core/IO/PathBuilder.cs index 159ae10..f47a09d 100644 --- a/Parallel.Core/IO/PathBuilder.cs +++ b/Parallel.Core/IO/PathBuilder.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Runtime.InteropServices; using System.Text; @@ -38,7 +38,7 @@ public static string LogDirectory return logDir; } } - + public static string LogFile => Path.Combine(LogDirectory, UnixTime.Now.TotalMilliseconds + ".log"); /// @@ -205,9 +205,14 @@ public static string ReplacePath(string fullPath, string sourceRoot, string? out { ArgumentNullException.ThrowIfNull(fullPath); ArgumentNullException.ThrowIfNull(sourceRoot); - - if(string.IsNullOrEmpty(outputRoot)) return fullPath; - string Normalize(string p) => p.Replace('\\', '/').TrimEnd('/'); + + if (string.IsNullOrEmpty(outputRoot)) return fullPath; + + string Normalize(string p) + { + return p.Replace('\\', '/').TrimEnd('/'); + } + fullPath = Normalize(fullPath); sourceRoot = Normalize(sourceRoot); diff --git a/Parallel.Core/IO/ScanFileSystemResult.cs b/Parallel.Core/IO/ScanFileSystemResult.cs index 700a0af..55301cb 100644 --- a/Parallel.Core/IO/ScanFileSystemResult.cs +++ b/Parallel.Core/IO/ScanFileSystemResult.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System; using System.Collections.Generic; diff --git a/Parallel.Core/IO/Scanning/FileScanner.cs b/Parallel.Core/IO/Scanning/FileScanner.cs index 41a1300..04405d1 100644 --- a/Parallel.Core/IO/Scanning/FileScanner.cs +++ b/Parallel.Core/IO/Scanning/FileScanner.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using System.Data; @@ -96,14 +96,16 @@ public async Task GetFileChangesAsync(string path, string[] ignoreF public static bool HasChanged(LocalFile source, LocalFile? target) { if (target is null || source.LastWrite.TotalMilliseconds <= target.LastWrite.TotalMilliseconds) return false; - if (!source.TryGenerateCheckSums() || !target.TryGenerateCheckSums()) return false; + if (!source.TryGenerateLocalCheckSum() || !target.TryGenerateLocalCheckSum()) return false; + if (!source.TryGenerateRemoteCheckSum() || !target.TryGenerateRemoteCheckSum()) return false; return source.LocalCheckSum != target.LocalCheckSum; } - + public static bool IsSameFile(LocalFile source, LocalFile? target) { if (target is null) return false; - if (!source.TryGenerateCheckSums() || !target.TryGenerateCheckSums()) return false; + if (!source.TryGenerateLocalCheckSum() || !target.TryGenerateLocalCheckSum()) return false; + if (!source.TryGenerateRemoteCheckSum() || !target.TryGenerateRemoteCheckSum()) return false; return source.LocalCheckSum == target.LocalCheckSum; } @@ -274,19 +276,19 @@ public static Dictionary GetDuplicateFiles(string path) System.Threading.Tasks.Parallel.ForEach(files, ParallelConfig.Options, file => { LocalFile entry = new(file); - dict.AddOrUpdate(entry.Name, _ => [entry], (k, v) => - { - lock (v) + if (!entry.TryGenerateLocalCheckSum()) return; + dict.AddOrUpdate(entry.LocalCheckSum!, _ => [entry], (_, list) => { - LocalFile? key = v.FirstOrDefault(); - if (IsSameFile(entry, key)) v.Add(entry); - } + lock (list) + { + list.Add(entry); + } - return v; - }); + return list; + }); }); - return dict.Where(kv => kv.Value.Count > 1).OrderByDescending(kv => kv.Value.Count).ToDictionary(k => k.Key, v => v.Value.OrderBy(l => l.LastWrite.TotalMilliseconds).ToArray()); + return dict.Where(kv => kv.Value.Count > 1).OrderByDescending(kv => kv.Value.Count).ToDictionary(kv => kv.Key, kv => kv.Value.OrderBy(l => l.LastWrite.TotalMilliseconds).ToArray()); } /// @@ -320,7 +322,7 @@ public static bool IsIgnored(string path, string[] exempt) string[] dirs = path.Split(Path.DirectorySeparatorChar); string fileName = Path.GetFileName(path); string extension = Path.GetExtension(path); - + foreach (string entry in exempt) { if (path.StartsWith(entry, StringComparison.OrdinalIgnoreCase)) diff --git a/Parallel.Core/IO/SnapshotManager.cs b/Parallel.Core/IO/SnapshotManager.cs index 79ca1e0..4d0c718 100644 --- a/Parallel.Core/IO/SnapshotManager.cs +++ b/Parallel.Core/IO/SnapshotManager.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using Parallel.Core.Models; @@ -16,7 +16,7 @@ public static async Task CreateSnapshotAsync(IEnumerable return filePath; } - public static async Task> LoadSnapshotsAsync(string filePath) + public static Task> LoadSnapshotsAsync(string filePath) { throw new NotImplementedException(); } diff --git a/Parallel.Core/IO/Syncing/BaseSyncManager.cs b/Parallel.Core/IO/Syncing/BaseSyncManager.cs index 110e009..af8eddd 100644 --- a/Parallel.Core/IO/Syncing/BaseSyncManager.cs +++ b/Parallel.Core/IO/Syncing/BaseSyncManager.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; @@ -55,14 +55,14 @@ public async Task ConnectAsync(bool force = false) Log.Error("[{LocalVaultId}] Failed to connect to vault!", LocalVault.Id); return false; } - + string root = PathBuilder.GetRootDirectory(LocalVault); if (!await StorageProvider.ExistsAsync(root)) { await StorageProvider.CreateDirectoryAsync(root); Log.Debug("Created root directory: {Root}", root); } - + // Checks temp files for a local download of the config file if (!File.Exists(TempConfigFile) || File.GetLastWriteTimeUtc(TempConfigFile) <= DateTime.UtcNow.AddHours(-6) || force) { @@ -98,9 +98,9 @@ public async Task ConnectAsync(bool force = false) string remoteDbFile = PathBuilder.GetDatabaseFile(LocalVault); await StorageProvider.DownloadFileAsync(new LocalFile(TempDbFile), remoteDbFile); Log.Debug("Downloaded file: {TempDbFile}", TempDbFile); - } + } } - + // Load the temp files Database = new SqliteContext(TempDbFile); RemoteVaultConfig? config = RemoteVaultConfig.Load(TempConfigFile); @@ -126,10 +126,10 @@ public async Task DisconnectAsync() } /// - public abstract Task BackupFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite); + public abstract Task PushFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite); /// - public abstract Task RestoreFilesAsync(IReadOnlyList files, IProgressReporter progress); + public abstract Task PullFilesAsync(IReadOnlyList files, IProgressReporter progress); /// public abstract Task PruneFilesAsync(IReadOnlyList files, IProgressReporter progress); diff --git a/Parallel.Core/IO/Syncing/FileSyncManager.cs b/Parallel.Core/IO/Syncing/FileSyncManager.cs index be403c9..019c6a3 100644 --- a/Parallel.Core/IO/Syncing/FileSyncManager.cs +++ b/Parallel.Core/IO/Syncing/FileSyncManager.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using System.Diagnostics; @@ -24,7 +24,7 @@ public class FileSyncManager : BaseSyncManager public FileSyncManager(LocalVaultConfig localVault) : base(localVault) { } /// - public override async Task BackupFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite) + public override async Task PushFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite) { if (!files.Any()) return 0; int completed = 0; @@ -37,7 +37,7 @@ public override async Task BackupFilesAsync(IReadOnlyList files, Log.Information("Uploading {UploadFilesLength:N0} files...", uploadFiles.Length); await System.Threading.Tasks.Parallel.ForEachAsync(uploadFiles, ParallelConfig.Options, async (file, ct) => { - if (!file.TryGenerateCheckSums()) return; + if (!file.TryGenerateRemoteCheckSum()) return; SemaphoreSlim threadLock = threadPool.GetOrAdd(file.RemoteCheckSum!, _ => new SemaphoreSlim(1, 1)); string remotePath = PathBuilder.GetObjectFile(RemoteVault, file.RemoteCheckSum!); @@ -86,7 +86,7 @@ await System.Threading.Tasks.Parallel.ForEachAsync(deletedFiles, ParallelConfig. } /// - public override async Task RestoreFilesAsync(IReadOnlyList files, IProgressReporter progress) + public override async Task PullFilesAsync(IReadOnlyList files, IProgressReporter progress) { if (!files.Any()) return 0; int completed = 0; @@ -133,9 +133,12 @@ await System.Threading.Tasks.Parallel.ForEachAsync(files, ParallelConfig.Options threadLock.Release(); } }); - + Log.Information("Cleaning up {CleanFilesLength:N0} files...", cleanupFiles.Count); - foreach (string path in cleanupFiles) if(File.Exists(path)) File.Delete(path); + foreach (string path in cleanupFiles) + if (File.Exists(path)) + File.Delete(path); + return completed; } @@ -163,7 +166,7 @@ await System.Threading.Tasks.Parallel.ForEachAsync(files, ParallelConfig.Options Interlocked.Increment(ref completed); } }); - + return completed; } @@ -171,7 +174,7 @@ public override async Task ScrubFilesAsync(IReadOnlyList files, { if (!files.Any()) return 0; int completed = 0; - + ConcurrentDictionary threadPool = new ConcurrentDictionary(); await System.Threading.Tasks.Parallel.ForEachAsync(files, ParallelConfig.Options, async (file, ct) => { @@ -194,7 +197,7 @@ await System.Threading.Tasks.Parallel.ForEachAsync(files, ParallelConfig.Options progress.Failed(file, "File corrupted!"); return; } - + progress.Report(ProgressOperation.Scrubbed, file); Interlocked.Increment(ref completed); } @@ -207,7 +210,7 @@ await System.Threading.Tasks.Parallel.ForEachAsync(files, ParallelConfig.Options threadLock.Release(); } }); - + return completed; } } diff --git a/Parallel.Core/IO/Syncing/ISyncManager.cs b/Parallel.Core/IO/Syncing/ISyncManager.cs index 3929ac6..4d12f3b 100644 --- a/Parallel.Core/IO/Syncing/ISyncManager.cs +++ b/Parallel.Core/IO/Syncing/ISyncManager.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Database; using Parallel.Core.Diagnostics; @@ -54,14 +54,14 @@ public interface ISyncManager /// /// /// - Task BackupFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite); + Task PushFilesAsync(IReadOnlyList files, IProgressReporter progress, bool overwrite); /// /// Restores an array of files from a vault. /// /// /// - Task RestoreFilesAsync(IReadOnlyList files, IProgressReporter progress); + Task PullFilesAsync(IReadOnlyList files, IProgressReporter progress); /// /// Deletes files from a vault. diff --git a/Parallel.Core/IO/Syncing/SyncManager.cs b/Parallel.Core/IO/Syncing/SyncManager.cs index 34a580a..cbc3893 100644 --- a/Parallel.Core/IO/Syncing/SyncManager.cs +++ b/Parallel.Core/IO/Syncing/SyncManager.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Settings; diff --git a/Parallel.Core/Models/HistoryEvent.cs b/Parallel.Core/Models/HistoryEvent.cs index 32ffbb8..44faf48 100644 --- a/Parallel.Core/Models/HistoryEvent.cs +++ b/Parallel.Core/Models/HistoryEvent.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Database; using Parallel.Core.Utils; diff --git a/Parallel.Core/Models/LocalFile.cs b/Parallel.Core/Models/LocalFile.cs index 6a07999..2c558a4 100644 --- a/Parallel.Core/Models/LocalFile.cs +++ b/Parallel.Core/Models/LocalFile.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Data; using System.Security.Cryptography; @@ -74,7 +74,7 @@ public class LocalFile /// The checksum used to check if the file has changed. /// public string? LocalCheckSum { get; set; } = string.Empty; - + /// /// The checksum used to check if the file was fully uploaded. /// @@ -94,7 +94,7 @@ public LocalFile(string path) Deleted = !File.Exists(path); if (!File.Exists(path)) return; - + FileInfo fileInfo = new FileInfo(path); LocalSize = fileInfo.Length; RemoteSize = fileInfo.Length; @@ -171,9 +171,30 @@ public bool Equals(LocalFile value) return results.All(b => b != null && (bool)b); } - public bool TryGenerateCheckSums() + public bool TryGenerateLocalCheckSum() { if (!string.IsNullOrEmpty(LocalCheckSum)) return true; + + try + { + if (!File.Exists(Fullname)) return false; + + using SHA256 sha256 = SHA256.Create(); + using FileStream fs = File.OpenRead(Fullname); + + fs.Position = 0; + LocalCheckSum = Convert.ToHexStringLower(sha256.ComputeHash(fs)); + return !string.IsNullOrEmpty(LocalCheckSum); + } + catch (Exception ex) + { + Log.Error(ex, "Error while generating checksum"); + return false; + } + } + + public bool TryGenerateRemoteCheckSum() + { if (!string.IsNullOrEmpty(RemoteCheckSum)) return true; try @@ -189,14 +210,7 @@ public bool TryGenerateCheckSums() } RemoteCheckSum = hs.GetHashHexString(); - - fs.Position = 0; - LocalCheckSum = Convert.ToHexStringLower(sha256.ComputeHash(fs)); - - //Log.Debug("LocalCheckSum: {LocalCheckSum}", LocalCheckSum); - //Log.Debug("RemoteCheckSum: {RemoteCheckSum}", RemoteCheckSum); - - return !string.IsNullOrEmpty(LocalCheckSum) && !string.IsNullOrEmpty(RemoteCheckSum); + return !string.IsNullOrEmpty(RemoteCheckSum); } catch (Exception ex) { @@ -204,7 +218,7 @@ public bool TryGenerateCheckSums() return false; } } - + private LocalFile(LocalFile file, long remoteSize, string? remoteCheckSum) { Name = file.Name; diff --git a/Parallel.Core/Models/PullRecord.cs b/Parallel.Core/Models/PullRecord.cs new file mode 100644 index 0000000..7ec2e5b --- /dev/null +++ b/Parallel.Core/Models/PullRecord.cs @@ -0,0 +1,37 @@ +// Copyright 2026 Entex Interactive + +namespace Parallel.Core.Models +{ + public class PullRecord + { + /// + /// The machine to use this record. + /// + public string Machine { get; } = Environment.MachineName; + + /// + /// The source path to pull. + /// + public string Source { get; } + + /// + /// The destination to remap files to. + /// + public string Destination { get; set; } + + /// + /// Creates a new instance of the class. + /// + /// + /// + /// + public PullRecord(string source, string? destination) + { + Source = source; + Destination = destination ?? source; + } + + public override bool Equals(object? obj) => obj is PullRecord or && Machine == or.Machine && Source == or.Source; + public override int GetHashCode() => HashCode.Combine(Machine, Source); + } +} \ No newline at end of file diff --git a/Parallel.Core/Models/RemoteFile.cs b/Parallel.Core/Models/RemoteFile.cs index 7ee5fb5..f90e20b 100644 --- a/Parallel.Core/Models/RemoteFile.cs +++ b/Parallel.Core/Models/RemoteFile.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Utils; @@ -25,12 +25,12 @@ public class RemoteFile /// The time the file was either last saved or deleted. /// public UnixTime LastUpdate { get; set; } - + /// /// The size, in bytes, of the file in the remote backup. /// public long RemoteSize { get; set; } - + /// /// The checksum used to check if the file was fully uploaded. /// diff --git a/Parallel.Core/Models/SnapshotRecord.cs b/Parallel.Core/Models/SnapshotRecord.cs index 6e946ce..a99d6c4 100644 --- a/Parallel.Core/Models/SnapshotRecord.cs +++ b/Parallel.Core/Models/SnapshotRecord.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; @@ -13,22 +13,22 @@ public class SnapshotRecord /// The path of the file on the local machine. /// public string Fullname { get; set; } - + /// /// The time the current file was last written to. /// public DateTime LastWrite { get; set; } - + /// /// The checksum used to check if the file has changed. /// public string? LocalCheckSum { get; set; } - + /// /// The checksum used to check if the file was fully uploaded. /// public string? RemoteCheckSum { get; set; } - + /// /// If the file is currently hidden. /// diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 4553730..e30f761 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -35,19 +35,13 @@ - - - + + + - - - - - - - - C:\Users\kebbi\.nuget\packages\newtonsoft.json\13.0.3\lib\net6.0\Newtonsoft.Json.dll - + + + diff --git a/Parallel.Core/Security/Encryption.cs b/Parallel.Core/Security/Encryption.cs index 10320ba..d64ba35 100644 --- a/Parallel.Core/Security/Encryption.cs +++ b/Parallel.Core/Security/Encryption.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Security.Cryptography; using System.Text; diff --git a/Parallel.Core/Security/HashGenerator.cs b/Parallel.Core/Security/HashGenerator.cs index eb3c5f6..e1a6bf6 100644 --- a/Parallel.Core/Security/HashGenerator.cs +++ b/Parallel.Core/Security/HashGenerator.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Security.Cryptography; using System.Text; diff --git a/Parallel.Core/Settings/LocalVaultConfig.cs b/Parallel.Core/Settings/LocalVaultConfig.cs index 779b2fa..33da764 100644 --- a/Parallel.Core/Settings/LocalVaultConfig.cs +++ b/Parallel.Core/Settings/LocalVaultConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Security; using Parallel.Core.Storage; diff --git a/Parallel.Core/Settings/ParallelConfig.cs b/Parallel.Core/Settings/ParallelConfig.cs index f51073a..7803985 100644 --- a/Parallel.Core/Settings/ParallelConfig.cs +++ b/Parallel.Core/Settings/ParallelConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using Newtonsoft.Json; diff --git a/Parallel.Core/Settings/RemoteVaultConfig.cs b/Parallel.Core/Settings/RemoteVaultConfig.cs index c74eb4e..b1d03d6 100644 --- a/Parallel.Core/Settings/RemoteVaultConfig.cs +++ b/Parallel.Core/Settings/RemoteVaultConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Reflection; using System.Runtime.InteropServices; @@ -6,6 +6,7 @@ using Newtonsoft.Json.Linq; using Parallel.Core.Database; using Parallel.Core.IO.Syncing; +using Parallel.Core.Models; using Parallel.Core.Security; using Parallel.Core.Storage; using Parallel.Core.Utils; @@ -30,14 +31,17 @@ public class RemoteVaultConfig : LocalVaultConfig public int PrunePeriod { get; set; } = 180; /// - /// A collection of directories to be backed up. - /// Default: Empty + /// A collection of directories to be pushed to vaults. /// - public HashSet BackupDirectories { get; } = CreateBackupDirectories(); + public HashSet PushDirectories { get; } = CreatePushDirectories(); + + /// + /// A collection of directories to be pulled when reconciling. + /// + public HashSet PullDirectories { get; } = []; /// /// A collection of directories to be ignored when archiving or cleaning. - /// Default: Empty /// public HashSet IgnoreDirectories { get; } = CreateIgnoreDirectories(); @@ -51,17 +55,18 @@ public class RemoteVaultConfig : LocalVaultConfig public RemoteVaultConfig(LocalVaultConfig localVault) : base(localVault.Id, localVault.Name, localVault.Credentials) { } [JsonConstructor] - public RemoteVaultConfig(string id, string name, StorageCredentials credentials, int prunePeriod, IEnumerable backupDirectories, IEnumerable ignoreDirectories, IEnumerable pruneDirectories) : base(id, name, credentials) + public RemoteVaultConfig(string id, string name, StorageCredentials credentials, int prunePeriod, IEnumerable? pushDirectories, IEnumerable? pullDirectories, IEnumerable? ignoreDirectories, IEnumerable? pruneDirectories) : base(id, name, credentials) { PrunePeriod = prunePeriod; - BackupDirectories = new HashSet(backupDirectories); - IgnoreDirectories = new HashSet(ignoreDirectories); - PruneDirectories = new HashSet(pruneDirectories); + PushDirectories = new HashSet(pushDirectories ?? []); + PullDirectories = new HashSet(pullDirectories ?? []); + IgnoreDirectories = new HashSet(ignoreDirectories ?? []); + PruneDirectories = new HashSet(pruneDirectories ?? []); } #region Privates - private static HashSet CreateBackupDirectories() + private static HashSet CreatePushDirectories() { return [ @@ -115,7 +120,7 @@ private static HashSet CreateIgnoreDirectories() public void Save(string path) { - File.WriteAllText(path, JsonConvert.SerializeObject(this)); + File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); } } } \ No newline at end of file diff --git a/Parallel.Core/Settings/StorageCredentials.cs b/Parallel.Core/Settings/StorageCredentials.cs index c387e4e..9f906c8 100644 --- a/Parallel.Core/Settings/StorageCredentials.cs +++ b/Parallel.Core/Settings/StorageCredentials.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Parallel.Core/Storage/IStorageProvider.cs b/Parallel.Core/Storage/IStorageProvider.cs index 69cfa75..9209727 100644 --- a/Parallel.Core/Storage/IStorageProvider.cs +++ b/Parallel.Core/Storage/IStorageProvider.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Diagnostics; using Parallel.Core.Models; @@ -15,7 +15,7 @@ public interface IStorageProvider : IDisposable /// /// Task CheckConnectionAsync(); - + /// /// Creates all directories and subdirectories in the specified path unless they already exist. /// @@ -68,7 +68,7 @@ public interface IStorageProvider : IDisposable /// /// Task GetFileAsync(string path); - + Task HashFileAsync(string remotePath, int bufferSize, CancellationToken ct); /// diff --git a/Parallel.Core/Storage/LocalStorageProvider.cs b/Parallel.Core/Storage/LocalStorageProvider.cs index 71280ea..e1161c4 100644 --- a/Parallel.Core/Storage/LocalStorageProvider.cs +++ b/Parallel.Core/Storage/LocalStorageProvider.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.IO.Compression; using System.Security.Cryptography; @@ -64,11 +64,11 @@ public Task DeleteFileAsync(string path) public async Task DownloadFileAsync(LocalFile file, string remotePath, CancellationToken ct = default) { - if(!File.Exists(remotePath)) return null; - + if (!File.Exists(remotePath)) return null; + long totalBytes = 0; string remoteChecksum; - + await using FileStream openStream = File.OpenRead(remotePath); await using FileStream createStream = File.Create(file.Fullname); await using (ZstdStream zstdStream = new(openStream, ZstdStreamMode.Decompress)) @@ -76,10 +76,10 @@ public Task DeleteFileAsync(string path) { await hashStream.CopyToAsync(createStream, ct); await hashStream.FlushAsync(ct); - + remoteChecksum = hashStream.GetHashHexString(); } - + Log.Information("Downloaded file: {SourcePath} ({RemoteChecksum})", file.Fullname, remoteChecksum); return new RemoteFile(file.Name, file.Fullname, file.LastWrite, file.LastUpdate, totalBytes, remoteChecksum); } @@ -105,7 +105,7 @@ public Task GetDirectoryName(string path) RemoteFile file = new(fi.Name, path, fi.LastWriteTimeUtc, fi.Length, fi.Name); return Task.FromResult(file); } - + /// public async Task HashFileAsync(string remotePath, int bufferSize = 81920, CancellationToken ct = default) { @@ -114,27 +114,27 @@ public Task GetDirectoryName(string path) Log.Debug("Skipping file: {RemotePath}", remotePath); return null; } - + await using FileStream stream = File.OpenRead(remotePath); using IncrementalHash sha = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - + int bytesRead; byte[] buffer = new byte[bufferSize]; while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct)) > 0) { sha.AppendData(buffer.AsSpan(0, bytesRead)); } - + return Convert.ToHexStringLower(sha.GetHashAndReset()); } - + private void InternalRenameFile(string sourcePath, string destPath) { - if(!File.Exists(sourcePath)) return; - + if (!File.Exists(sourcePath)) return; + FileAttributes sourceAttrs = File.GetAttributes(sourcePath); bool isReadOnly = (sourceAttrs & FileAttributes.ReadOnly) != 0; - + if (File.Exists(destPath)) { FileAttributes destAttrs = File.GetAttributes(destPath); @@ -143,9 +143,9 @@ private void InternalRenameFile(string sourcePath, string destPath) File.SetAttributes(destPath, destAttrs & ~FileAttributes.ReadOnly); } } - + File.Move(sourcePath, destPath, true); - + if (isReadOnly) { FileAttributes destAttrs = File.GetAttributes(destPath); @@ -164,7 +164,7 @@ private void InternalRenameFile(string sourcePath, string destPath) string tempPath = remotePath + ".tmp"; await CreateDirectoryAsync(await GetDirectoryName(remotePath)); - + long totalBytes = 0; string remoteChecksum; @@ -180,9 +180,9 @@ private void InternalRenameFile(string sourcePath, string destPath) remoteChecksum = hashStream.GetHashHexString(); } - + InternalRenameFile(tempPath, remotePath); - + Log.Information("Uploaded file: {SourcePath} ({RemoteChecksum})", file.Fullname, remoteChecksum); return new RemoteFile(file.Name, file.Fullname, file.LastWrite, file.LastUpdate, totalBytes, remoteChecksum); } diff --git a/Parallel.Core/Storage/S3StorageProvider.cs b/Parallel.Core/Storage/S3StorageProvider.cs index 031458c..ff9e93d 100644 --- a/Parallel.Core/Storage/S3StorageProvider.cs +++ b/Parallel.Core/Storage/S3StorageProvider.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using System.IO.Compression; @@ -88,8 +88,8 @@ public async Task DeleteFileAsync(string path) public async Task DownloadFileAsync(LocalFile file, string remotePath, CancellationToken ct = default) { - if(!await ExistsAsync(remotePath)) return null; - + if (!await ExistsAsync(remotePath)) return null; + using GetObjectResponse? response = await _client.GetObjectAsync(_bucket, remotePath, ct); await using FileStream createStream = File.Create(file.Fullname); await using ZstdStream zstdStream = new(response.ResponseStream, ZstdStreamMode.Decompress); diff --git a/Parallel.Core/Storage/SshStorageProvider.cs b/Parallel.Core/Storage/SshStorageProvider.cs index 1e5ff6c..564929f 100644 --- a/Parallel.Core/Storage/SshStorageProvider.cs +++ b/Parallel.Core/Storage/SshStorageProvider.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Diagnostics; using System.IO.Compression; @@ -92,7 +92,7 @@ public async Task DeleteFileAsync(string path) { InsureConnection(); if (!await ExistsAsync(remotePath)) return null; - + await using SftpFileStream openStream = _client.OpenRead(remotePath); await using FileStream createStream = File.Create(file.Fullname); await using ZstdStream zstdStream = new(openStream, ZstdStreamMode.Decompress); @@ -142,7 +142,7 @@ public Task GetDirectoryName(string path) string tempPath = remotePath + ".tmp"; await CreateDirectoryAsync(await GetDirectoryName(remotePath)); - + long totalBytes = 0; await using SftpFileStream createStream = _client.Create(tempPath); await using HashStream hashStream = new(createStream, b => totalBytes = b); @@ -156,7 +156,7 @@ public Task GetDirectoryName(string path) if (overwrite && await ExistsAsync(remotePath)) await _client.DeleteFileAsync(remotePath, ct); await _client.RenameFileAsync(tempPath, remotePath, ct); _client.ChangePermissions(remotePath, 444); - + string remoteChecksum = hashStream.GetHashHexString(); Log.Information("Uploaded file: {SourcePath} ({RemoteChecksum})", file.Fullname, remoteChecksum); return new RemoteFile(file.Name, file.Fullname, file.LastWrite, file.LastUpdate, totalBytes, remoteChecksum); diff --git a/Parallel.Core/Storage/StorageConnection.cs b/Parallel.Core/Storage/StorageConnection.cs index 678df25..88b2408 100644 --- a/Parallel.Core/Storage/StorageConnection.cs +++ b/Parallel.Core/Storage/StorageConnection.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Parallel.Core.Settings; diff --git a/Parallel.Core/Utils/Converter.cs b/Parallel.Core/Utils/Converter.cs index 1820ee6..dcbd12c 100644 --- a/Parallel.Core/Utils/Converter.cs +++ b/Parallel.Core/Utils/Converter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Net; diff --git a/Parallel.Core/Utils/Formatter.cs b/Parallel.Core/Utils/Formatter.cs index 367613d..47babf7 100644 --- a/Parallel.Core/Utils/Formatter.cs +++ b/Parallel.Core/Utils/Formatter.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive namespace Parallel.Core.Utils { diff --git a/Parallel.Core/Utils/HashStream.cs b/Parallel.Core/Utils/HashStream.cs index 60f7459..cf7fb53 100644 --- a/Parallel.Core/Utils/HashStream.cs +++ b/Parallel.Core/Utils/HashStream.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Security.Cryptography; @@ -11,26 +11,29 @@ public class HashStream : Stream private readonly Action? _reportBytes; private long _totalWrite = 0; private long _totalRead = 0; - + public HashStream(Stream inner) { _inner = inner; } - + public HashStream(Stream inner, Action reportBytes) { _inner = inner; _reportBytes = reportBytes; } - + protected override void Dispose(bool disposing) { base.Dispose(disposing); _inner.Dispose(); } - - public string GetHashHexString() => Convert.ToHexStringLower(_hash.GetHashAndReset()); - + + public string GetHashHexString() + { + return Convert.ToHexStringLower(_hash.GetHashAndReset()); + } + public override int Read(byte[] buffer, int offset, int count) { int read = _inner.Read(buffer, offset, count); @@ -60,7 +63,7 @@ public override void Write(byte[] buffer, int offset, int count) _hash.AppendData(buffer.AsSpan(offset, count)); _inner.Write(buffer, offset, count); _totalWrite += count; - + _reportBytes?.Invoke(_totalWrite); } @@ -69,7 +72,7 @@ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, Cancella _hash.AppendData(buffer.Span); await _inner.WriteAsync(buffer, cancellationToken); _totalWrite += buffer.Length; - + _reportBytes?.Invoke(_totalWrite); } diff --git a/Parallel.Core/Utils/LogEventTracker.cs b/Parallel.Core/Utils/LogEventTracker.cs index 92434b9..2c24fd2 100644 --- a/Parallel.Core/Utils/LogEventTracker.cs +++ b/Parallel.Core/Utils/LogEventTracker.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using Serilog.Core; @@ -15,7 +15,7 @@ public class LogEventTracker : ILogEventSink /// Gets an array of log messages. /// public readonly ConcurrentBag Logs = new ConcurrentBag(); - + /// /// Gets the number of errors in this log. /// diff --git a/Parallel.Core/Utils/UnixTime.cs b/Parallel.Core/Utils/UnixTime.cs index 1ecb288..9d7e609 100644 --- a/Parallel.Core/Utils/UnixTime.cs +++ b/Parallel.Core/Utils/UnixTime.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using Newtonsoft.Json; using Parallel.Core.Extensions.Json; diff --git a/Parallel.Desktop/App.axaml b/Parallel.Desktop/App.axaml deleted file mode 100644 index 9c1b9f2..0000000 --- a/Parallel.Desktop/App.axaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Parallel.Desktop/App.axaml.cs b/Parallel.Desktop/App.axaml.cs deleted file mode 100644 index cd3a091..0000000 --- a/Parallel.Desktop/App.axaml.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Data.Core; -using Avalonia.Data.Core.Plugins; -using System.Linq; -using Avalonia.Markup.Xaml; -using Parallel.Desktop.ViewModels; -using Parallel.Desktop.Views; - -namespace Parallel.Desktop; - -public partial class App : Application -{ - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); - desktop.MainWindow = new MainWindow - { - DataContext = new MainWindowViewModel(), - }; - } - - base.OnFrameworkInitializationCompleted(); - } - - private void DisableAvaloniaDataAnnotationValidation() - { - // Get an array of plugins to remove - var dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) - { - BindingPlugins.DataValidators.Remove(plugin); - } - } -} \ No newline at end of file diff --git a/Parallel.Desktop/Assets/avalonia-logo.ico b/Parallel.Desktop/Assets/avalonia-logo.ico deleted file mode 100644 index f7da8bb..0000000 Binary files a/Parallel.Desktop/Assets/avalonia-logo.ico and /dev/null differ diff --git a/Parallel.Desktop/Assets/parallel-red.ico b/Parallel.Desktop/Assets/parallel-red.ico deleted file mode 100644 index 27a395d..0000000 Binary files a/Parallel.Desktop/Assets/parallel-red.ico and /dev/null differ diff --git a/Parallel.Desktop/Parallel.Desktop.csproj b/Parallel.Desktop/Parallel.Desktop.csproj deleted file mode 100644 index 467bdfd..0000000 --- a/Parallel.Desktop/Parallel.Desktop.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - WinExe - net9.0 - enable - app.manifest - true - Parallel Desktop - 0.0.0.0 - Entex Interactive, LLC - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) - - - - $(DefineConstants);TRACE - True - True - - - $(DefineConstants);DEBUG;TRACE - False - True - Full - - - False - True - True - True - - - - - - - - - - - - - - - none - all - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Parallel.Desktop/Program.cs b/Parallel.Desktop/Program.cs deleted file mode 100644 index 9ceb0f0..0000000 --- a/Parallel.Desktop/Program.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Avalonia; -using System; -using System.IO; -using System.Reflection; -using MsBox.Avalonia; -using MsBox.Avalonia.Enums; -using Parallel.Core.IO; -using Parallel.Core.Settings; -using Parallel.Desktop.Settings; - -namespace Parallel.Desktop -{ - sealed class Program - { - internal static ParallelDesktopConfig Settings = new ParallelDesktopConfig(); - - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - [STAThread] - public static void Main(string[] args) - { - AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); - - string logDir = Path.Combine(PathBuilder.TempDirectory, "Logs"); - if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir); - - Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.File(Path.Combine(logDir, $"{assembly.Name}.log")).WriteTo.Console().CreateLogger(); - Log.Information($"{assembly.Name} [Version {assembly.Version}]"); - - if (!ParallelConfig.CanStartNewInstance()) - { - Log.Warning("Can't start new instance of Parallel!"); - - //MessageBoxManager.GetMessageBoxStandard("Warning", "An instance of Parallel is already running!", ButtonEnum.Ok, Icon.Warning); - return; - } - - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - Settings = ParallelDesktopConfig.Load(); - } - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace(); - } -} \ No newline at end of file diff --git a/Parallel.Desktop/Settings/ParallelDesktopConfig.cs b/Parallel.Desktop/Settings/ParallelDesktopConfig.cs deleted file mode 100644 index 917610e..0000000 --- a/Parallel.Desktop/Settings/ParallelDesktopConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2026 Kyle Ebbinga - -using System.IO; -using Parallel.Core.IO; -using Parallel.Core.Settings; - -namespace Parallel.Desktop.Settings -{ - public class ParallelDesktopConfig : ParallelConfig - { - private static string ConfigFile { get; } = Path.Combine(PathBuilder.ProgramData, "Desktop-Configuration.json"); - - public new static ParallelDesktopConfig Load() - { - Log.Debug($"Loading config file: {ConfigFile}"); - if (!File.Exists(ConfigFile)) return new ParallelDesktopConfig(); - - string json = File.ReadAllText(ConfigFile); - ParallelDesktopConfig? config = JsonConvert.DeserializeObject(json); - return config ?? new ParallelDesktopConfig(); - } - - /// - /// Saves settings to a file. - /// - public new void Save() - { - Log.Debug($"Saving config file: {ConfigFile}"); - if (!Directory.Exists(PathBuilder.ProgramData)) Directory.CreateDirectory(PathBuilder.ProgramData); - File.WriteAllText(ConfigFile, JsonConvert.SerializeObject(this, Formatting.Indented)); - } - } -} \ No newline at end of file diff --git a/Parallel.Desktop/ViewLocator.cs b/Parallel.Desktop/ViewLocator.cs deleted file mode 100644 index f811fc5..0000000 --- a/Parallel.Desktop/ViewLocator.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Parallel.Desktop.ViewModels; - -namespace Parallel.Desktop; - -/// -/// Given a view model, returns the corresponding view if possible. -/// -[RequiresUnreferencedCode( - "Default implementation of ViewLocator involves reflection which may be trimmed away.", - Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] -public class ViewLocator : IDataTemplate -{ - public Control? Build(object? param) - { - if (param is null) - return null; - - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); - - if (type != null) - { - return (Control)Activator.CreateInstance(type)!; - } - - return new TextBlock { Text = "Not Found: " + name }; - } - - public bool Match(object? data) - { - return data is ViewModelBase; - } -} \ No newline at end of file diff --git a/Parallel.Desktop/ViewModels/MainWindowViewModel.cs b/Parallel.Desktop/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 3d63c3e..0000000 --- a/Parallel.Desktop/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Parallel.Desktop.ViewModels; - -public partial class MainWindowViewModel : ViewModelBase -{ - public string Greeting { get; } = "Welcome to Avalonia!"; -} \ No newline at end of file diff --git a/Parallel.Desktop/ViewModels/ViewModelBase.cs b/Parallel.Desktop/ViewModels/ViewModelBase.cs deleted file mode 100644 index 1b44fbf..0000000 --- a/Parallel.Desktop/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Parallel.Desktop.ViewModels; - -public abstract class ViewModelBase : ObservableObject -{ -} \ No newline at end of file diff --git a/Parallel.Desktop/Views/MainWindow.axaml b/Parallel.Desktop/Views/MainWindow.axaml deleted file mode 100644 index cf64f06..0000000 --- a/Parallel.Desktop/Views/MainWindow.axaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/Parallel.Desktop/Views/MainWindow.axaml.cs b/Parallel.Desktop/Views/MainWindow.axaml.cs deleted file mode 100644 index 1225293..0000000 --- a/Parallel.Desktop/Views/MainWindow.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace Parallel.Desktop.Views; - -public partial class MainWindow : Window -{ - public MainWindow() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/Parallel.Desktop/app.manifest b/Parallel.Desktop/app.manifest deleted file mode 100644 index 4671a47..0000000 --- a/Parallel.Desktop/app.manifest +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Parallel.sln b/Parallel.sln index 6fb18e9..b4a1928 100644 --- a/Parallel.sln +++ b/Parallel.sln @@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Core", "Parallel.C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Cli", "Parallel.Cli\Parallel.Cli.csproj", "{4BFE65E9-9534-4C85-B59F-1F64A998C76D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Desktop", "Parallel.Desktop\Parallel.Desktop.csproj", "{85113B09-8E51-4745-B638-85863711AABB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU