From ab14444c6898dc5e2a4615a32a15f4927c0253a6 Mon Sep 17 00:00:00 2001 From: TheGuitarleader <26614720+TheGuitarleader@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:06:43 -0500 Subject: [PATCH 1/7] Renamed sync/restore to push/pull --- .../{RestoreCommand.cs => PullCommand.cs} | 34 +++++++++---------- .../{SyncCommand.cs => PushCommand.cs} | 10 +++--- Parallel.Cli/Program.cs | 4 +-- Parallel.Core/IO/Syncing/BaseSyncManager.cs | 4 +-- Parallel.Core/IO/Syncing/FileSyncManager.cs | 4 +-- Parallel.Core/IO/Syncing/ISyncManager.cs | 4 +-- 6 files changed, 29 insertions(+), 31 deletions(-) rename Parallel.Cli/Commands/{RestoreCommand.cs => PullCommand.cs} (74%) rename Parallel.Cli/Commands/{SyncCommand.cs => PushCommand.cs} (95%) diff --git a/Parallel.Cli/Commands/RestoreCommand.cs b/Parallel.Cli/Commands/PullCommand.cs similarity index 74% rename from Parallel.Cli/Commands/RestoreCommand.cs rename to Parallel.Cli/Commands/PullCommand.cs index 317664c..7695250 100644 --- a/Parallel.Cli/Commands/RestoreCommand.cs +++ b/Parallel.Cli/Commands/PullCommand.cs @@ -15,19 +15,19 @@ namespace Parallel.Cli.Commands { - public class RestoreCommand : Command + public class PullCommand : Command { private Stopwatch _sw = new Stopwatch(); private readonly Option _sourceOpt = new(["--path", "-p"], "The source path to restore."); 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 _beforeOpt = new(["--before"], "Pulls 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 _dryRunOpt = new(["--dry-run"], "Previews the command without executing it."); private readonly Option _verboseOpt = new(["--verbose", "-v"], "Shows verbose output."); - public RestoreCommand() : base("restore", "Restores files from the backup.") + public PullCommand() : base("restore", "Pulls files from the backup.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); @@ -36,10 +36,10 @@ public RestoreCommand() : base("restore", "Restores files from the backup.") 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, _remapOpt, _forceOpt, _verboseOpt, _dryRunOpt); } - 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? remap, bool force, bool verbose, bool dryRun) { _sw = Stopwatch.StartNew(); DateTime timestamp = before != DateTime.MinValue ? before.AddMinutes(1).AddTicks(-1) : DateTime.Now; @@ -48,27 +48,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, remap, force, verbose, dryRun); } else { - await RestoreSystemAsync(localVault, timestamp, remap, force, verbose, dryRun); + await PullSystemAsync(localVault, timestamp, remap, 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, remap, force, verbose, dryRun)); } else { - await Program.Settings.ForEachVaultAsync(vault => RestoreSystemAsync(vault, timestamp, remap, force, verbose, dryRun)); + await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, remap, 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 force, bool verbose, bool dryRun) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -79,11 +79,11 @@ private async Task RestoreSystemAsync(LocalVaultConfig vault, DateTime timestamp foreach (string path in syncManager.RemoteVault.BackupDirectories) { - await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun); + await PullInternalAsync(syncManager, path, timestamp, output, 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? output, bool force, bool verbose, bool dryRun) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -92,10 +92,10 @@ private async Task RestorePathAsync(LocalVaultConfig vault, string path, DateTim return; } - await RestoreInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun); + await PullInternalAsync(syncManager, path, timestamp, output, 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? output, 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>([])); @@ -128,11 +128,11 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D } else { - CommandLine.WriteLine(syncManager.RemoteVault, $"Restoring {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray); + 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 restoredFiles = await syncManager.RestoreFilesAsync(restoreFiles.ToArray(), progressReporter); + int pulledFiles = await syncManager.PullFilesAsync(restoreFiles.ToArray(), progressReporter); - CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully restored {restoredFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); + CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pulled {pulledFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); await syncManager.DisconnectAsync(); } } diff --git a/Parallel.Cli/Commands/SyncCommand.cs b/Parallel.Cli/Commands/PushCommand.cs similarity index 95% rename from Parallel.Cli/Commands/SyncCommand.cs rename to Parallel.Cli/Commands/PushCommand.cs index a189404..50dc94f 100644 --- a/Parallel.Cli/Commands/SyncCommand.cs +++ b/Parallel.Cli/Commands/PushCommand.cs @@ -12,7 +12,7 @@ namespace Parallel.Cli.Commands { - public class SyncCommand : Command + public class PushCommand : Command { private Stopwatch _sw = new Stopwatch(); @@ -27,7 +27,7 @@ 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 PushCommand() : base("sync", "Syncs the system with the vaults.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); @@ -132,11 +132,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); + int pushedFiles = await syncManager.PushFilesAsync(files, progressReporter, force); - CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully synced {backedUpFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); + CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pushed {pushedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); await syncManager.DisconnectAsync(); } diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index 7c37c0a..d2d78df 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -1,13 +1,11 @@ // Copyright 2026 Kyle Ebbinga 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.Core/IO/Syncing/BaseSyncManager.cs b/Parallel.Core/IO/Syncing/BaseSyncManager.cs index 110e009..1ca9471 100644 --- a/Parallel.Core/IO/Syncing/BaseSyncManager.cs +++ b/Parallel.Core/IO/Syncing/BaseSyncManager.cs @@ -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..187b88d 100644 --- a/Parallel.Core/IO/Syncing/FileSyncManager.cs +++ b/Parallel.Core/IO/Syncing/FileSyncManager.cs @@ -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; @@ -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; diff --git a/Parallel.Core/IO/Syncing/ISyncManager.cs b/Parallel.Core/IO/Syncing/ISyncManager.cs index 3929ac6..57ddc91 100644 --- a/Parallel.Core/IO/Syncing/ISyncManager.cs +++ b/Parallel.Core/IO/Syncing/ISyncManager.cs @@ -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. From 36334836775654e5e53c5e45490fcfac6c88a831 Mon Sep 17 00:00:00 2001 From: TheGuitarleader <26614720+TheGuitarleader@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:10:30 -0500 Subject: [PATCH 2/7] Reformated code --- Parallel.Cli/Commands/CleanCommand.cs | 2 +- Parallel.Cli/Commands/DuplicatesCommand.cs | 6 +- Parallel.Cli/Commands/FetchCommand.cs | 6 +- Parallel.Cli/Commands/HistoryCommand.cs | 2 +- Parallel.Cli/Commands/IgnoreCommand.cs | 2 +- Parallel.Cli/Commands/PruneCommand.cs | 4 +- Parallel.Cli/Commands/PullCommand.cs | 114 +++++++++++++++--- Parallel.Cli/Commands/PushCommand.cs | 38 +++--- Parallel.Cli/Commands/ScrubCommand.cs | 10 +- Parallel.Cli/Commands/SnapshotsCommand.cs | 34 +++--- Parallel.Cli/Commands/StatsCommand.cs | 2 +- Parallel.Cli/Commands/UnzipCommand.cs | 8 +- Parallel.Cli/Commands/VaultsCommand.cs | 7 +- Parallel.Cli/Commands/ZipCommand.cs | 2 +- Parallel.Cli/Parallel.Cli.csproj | 4 +- Parallel.Cli/Program.cs | 2 +- Parallel.Cli/Utils/CommandLine.cs | 2 +- Parallel.Cli/Utils/MarkdownGenerator.cs | 2 +- Parallel.Cli/Utils/ProgressReporter.cs | 2 +- Parallel.Cli/Utils/TextWriter.cs | 2 +- Parallel.Core/Data/FileTypes.cs | 2 +- .../Database/Contexts/SemaphoreContext.cs | 2 +- .../Database/Contexts/SqliteContext.cs | 16 +-- Parallel.Core/Database/IDatabase.cs | 16 +-- .../Diagnostics/IProgressReporter.cs | 2 +- .../Diagnostics/LoggingProgressReporter.cs | 6 +- Parallel.Core/Events/LocalFileEventArgs.cs | 2 +- .../Events/MessageRecievedEventArgs.cs | 2 +- .../Events/TransferFailedEventArgs.cs | 2 +- .../Events/TransferUpdateEventArgs.cs | 2 +- .../Extensions/Json/UnixTimeConverter.cs | 2 +- Parallel.Core/IO/PathBuilder.cs | 15 ++- Parallel.Core/IO/ScanFileSystemResult.cs | 2 +- Parallel.Core/IO/Scanning/FileScanner.cs | 6 +- Parallel.Core/IO/SnapshotManager.cs | 2 +- Parallel.Core/IO/Syncing/BaseSyncManager.cs | 10 +- Parallel.Core/IO/Syncing/FileSyncManager.cs | 17 +-- Parallel.Core/IO/Syncing/ISyncManager.cs | 2 +- Parallel.Core/IO/Syncing/SyncManager.cs | 2 +- Parallel.Core/Models/HistoryEvent.cs | 2 +- Parallel.Core/Models/LocalFile.cs | 10 +- Parallel.Core/Models/RemoteFile.cs | 6 +- Parallel.Core/Models/SnapshotRecord.cs | 10 +- Parallel.Core/Parallel.Core.csproj | 12 +- Parallel.Core/Security/Encryption.cs | 2 +- Parallel.Core/Security/HashGenerator.cs | 2 +- Parallel.Core/Settings/LocalVaultConfig.cs | 2 +- Parallel.Core/Settings/ParallelConfig.cs | 2 +- Parallel.Core/Settings/RemoteVaultConfig.cs | 26 ++-- Parallel.Core/Settings/StorageCredentials.cs | 2 +- Parallel.Core/Storage/IStorageProvider.cs | 6 +- Parallel.Core/Storage/LocalStorageProvider.cs | 38 +++--- Parallel.Core/Storage/S3StorageProvider.cs | 6 +- Parallel.Core/Storage/SshStorageProvider.cs | 8 +- Parallel.Core/Storage/StorageConnection.cs | 2 +- Parallel.Core/Utils/Converter.cs | 2 +- Parallel.Core/Utils/Formatter.cs | 2 +- Parallel.Core/Utils/HashStream.cs | 21 ++-- Parallel.Core/Utils/LogEventTracker.cs | 4 +- Parallel.Core/Utils/UnixTime.cs | 2 +- Parallel.Desktop/App.axaml | 6 +- Parallel.Desktop/App.axaml.cs | 6 +- Parallel.Desktop/Parallel.Desktop.csproj | 16 +-- Parallel.Desktop/Program.cs | 21 ++-- .../Settings/ParallelDesktopConfig.cs | 6 +- Parallel.Desktop/ViewLocator.cs | 6 +- .../ViewModels/MainWindowViewModel.cs | 4 +- Parallel.Desktop/ViewModels/ViewModelBase.cs | 4 +- Parallel.Desktop/Views/MainWindow.axaml | 6 +- Parallel.Desktop/Views/MainWindow.axaml.cs | 2 + Parallel.Desktop/app.manifest | 26 ++-- 71 files changed, 370 insertions(+), 261 deletions(-) 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/PullCommand.cs b/Parallel.Cli/Commands/PullCommand.cs index 7695250..549ee7f 100644 --- a/Parallel.Cli/Commands/PullCommand.cs +++ b/Parallel.Cli/Commands/PullCommand.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.Collections.Concurrent; using System.CommandLine; @@ -23,23 +23,25 @@ public class PullCommand : Command private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); private readonly Option _beforeOpt = new(["--before"], "Pulls files before a certain timestamp."); private readonly Option _remapOpt = new(["--remap"], "The new directory to map restored files to."); + private readonly Option _archiveOpt = new(["--archive", "-a"], "Pulls only archived files."); private readonly Option _forceOpt = new(["--force", "-f"], "Forces restoring, 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."); - public PullCommand() : base("restore", "Pulls files from the backup.") + public PullCommand() : base("pull", "Pulls files a vault.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); this.AddOption(_beforeOpt); this.AddOption(_remapOpt); + this.AddOption(_archiveOpt); this.AddOption(_forceOpt); this.AddOption(_dryRunOpt); this.AddOption(_verboseOpt); - this.SetHandler(HandlePullAsync, _sourceOpt, _configOpt, _beforeOpt, _remapOpt, _forceOpt, _verboseOpt, _dryRunOpt); + this.SetHandler(HandlePullAsync, _sourceOpt, _configOpt, _beforeOpt, _remapOpt, _archiveOpt, _forceOpt, _verboseOpt, _dryRunOpt); } - private async Task HandlePullAsync(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? remap, 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 +50,27 @@ private async Task HandlePullAsync(string? path, string? config, DateTime before { if (!string.IsNullOrEmpty(path)) { - await PullPathAsync(localVault, path, timestamp, remap, force, verbose, dryRun); + await PullPathAsync(localVault, path, timestamp, remap, archive, force, verbose, dryRun); } else { - await PullSystemAsync(localVault, timestamp, remap, force, verbose, dryRun); + await PullSystemAsync(localVault, timestamp, remap, archive, force, verbose, dryRun); } } else { if (!string.IsNullOrEmpty(path)) { - await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, remap, force, verbose, dryRun)); + await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, remap, archive, force, verbose, dryRun)); } else { - await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, remap, force, verbose, dryRun)); + await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, remap, archive, force, verbose, dryRun)); } } } - private async Task PullSystemAsync(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 +79,13 @@ private async Task PullSystemAsync(LocalVaultConfig vault, DateTime timestamp, s return; } - foreach (string path in syncManager.RemoteVault.BackupDirectories) + foreach (string path in syncManager.RemoteVault.PullDirectories) { - await PullInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun); + await PullInternalAsync(syncManager, path, timestamp, output, archive, force, verbose, dryRun); } } - private async Task PullPathAsync(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? output, bool archive, bool force, bool verbose, bool dryRun) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -92,13 +94,13 @@ private async Task PullPathAsync(LocalVaultConfig vault, string path, DateTime t return; } - await PullInternalAsync(syncManager, path, timestamp, output, force, verbose, dryRun); + await PullInternalAsync(syncManager, path, timestamp, output, archive, force, verbose, dryRun); } - private async Task PullInternalAsync(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? output, 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(); @@ -108,7 +110,7 @@ private async Task PullInternalAsync(ISyncManager syncManager, string path, Date //string outputPath = string.IsNullOrEmpty(output) ? sourcePath : PathBuilder.ReplacePath(sourcePath, path, output); string outputPath = PathBuilder.ReplacePath(file.Fullname, path, output); if (File.Exists(outputPath) && !FileScanner.HasChanged(file, new LocalFile(outputPath)) && !force) return; - + file.Fullname = outputPath; restoreFiles.Add(file); }); @@ -123,18 +125,90 @@ private async Task PullInternalAsync(ISyncManager syncManager, string path, Date { 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(output) ? path : output)}", ConsoleColor.Green); CommandLine.WriteLine($"A detailed list can be found here: {fileName}", ConsoleColor.DarkGray); } else { CommandLine.WriteLine(syncManager.RemoteVault, $"Pulling {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray); - IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault); + 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); + + 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) + { + 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); + } + else + { + await Program.Settings.ForEachVaultAsync(vault => AddPathAsync(vault, path)); + } + } + + private async Task AddPathAsync(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; + } + + if (!syncManager.RemoteVault.PushDirectories.Add(path)) + { + 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; + } + + if (!syncManager.RemoteVault.PushDirectories.Remove(path)) + { + CommandLine.WriteLine(vault, $"Unable to remove path: '{path}'", ConsoleColor.Yellow); + return; + } + + CommandLine.WriteLine(vault, $"Successfully removed '{path}'", ConsoleColor.Green); + await syncManager.DisconnectAsync(); + } + + #endregion } } \ No newline at end of file diff --git a/Parallel.Cli/Commands/PushCommand.cs b/Parallel.Cli/Commands/PushCommand.cs index 50dc94f..ee48977 100644 --- a/Parallel.Cli/Commands/PushCommand.cs +++ b/Parallel.Cli/Commands/PushCommand.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 PushCommand : 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 PushCommand() : base("sync", "Syncs the system with the vaults.") + public PushCommand() : base("push", "Pushes files to 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 PushCommand() : 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,13 @@ 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); } } - 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 +100,13 @@ private async Task SyncPathAsync(LocalVaultConfig vault, string path, bool force return; } - await SyncInternalAsync(syncManager, path, force, verbose); + await PushInternalAsync(syncManager, path, force, verbose); } - 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); @@ -135,7 +135,7 @@ private async Task SyncInternalAsync(ISyncManager syncManager, string path, bool CommandLine.WriteLine(syncManager.RemoteVault, $"Pushing {files.Length:N0} files...", ConsoleColor.DarkGray); IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, successFiles) : new LoggingProgressReporter(syncManager.RemoteVault); int pushedFiles = await syncManager.PushFilesAsync(files, progressReporter, force); - + CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pushed {pushedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); await syncManager.DisconnectAsync(); } @@ -166,7 +166,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 +202,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/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 index 6ce8b6a..946a973 100644 --- a/Parallel.Cli/Commands/StatsCommand.cs +++ b/Parallel.Cli/Commands/StatsCommand.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/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs index af14330..fac6284 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.") { @@ -33,7 +33,7 @@ private async Task HandleUnzipAsync(string path) } 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,7 +42,7 @@ private async Task HandleUnzipAsync(string path) { gZip.CopyTo(createFile); } - + File.SetAttributes(file, File.GetAttributes(file) & ~FileAttributes.ReadOnly); File.Delete(file); }); diff --git a/Parallel.Cli/Commands/VaultsCommand.cs b/Parallel.Cli/Commands/VaultsCommand.cs index 4129322..1119167 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; @@ -53,7 +53,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 +112,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); CommandLine.WriteArray("Ignore Directories", remoteVault.IgnoreDirectories); CommandLine.WriteArray("Prune Directories", remoteVault.PruneDirectories); CommandLine.WriteLine($"Prune Period: {remoteVault.PrunePeriod} days"); 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..dc97fa9 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -43,8 +43,8 @@ - - + + diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index d2d78df..b563b40 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.CommandLine; using System.Reflection; 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..144beae 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; @@ -26,7 +26,9 @@ public SqliteContext(string filePath) public async Task InitializeAsync() { Log.Information("Creating index database..."); - await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `objects` (`name` TEXT NOT NULL, `fullname` TEXT NOT NULL, `parentDir` TEXT NOT NULL, `lastWrite` LONG INTEGER NOT NULL, `lastUpdate` LONG INTEGER NOT NULL, `localSize` LONG INTEGER NOT NULL, `remoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readOnly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, `localCheckSum` TEXT, `remoteCheckSum` TEXT, UNIQUE (fullname, localCheckSum));"); + await _semaphore.ExecuteAsync( + "CREATE TABLE IF NOT EXISTS `objects` (`name` TEXT NOT NULL, `fullname` TEXT NOT NULL, `parentDir` TEXT NOT NULL, `lastWrite` LONG INTEGER NOT NULL, `lastUpdate` LONG INTEGER NOT NULL, `localSize` LONG INTEGER NOT NULL, `remoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readOnly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, `localCheckSum` TEXT, `remoteCheckSum` TEXT, UNIQUE (fullname, localCheckSum));"); + await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `history` (`timestamp` LONG INTEGER NOT NULL, `fullname` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`timestamp`));"); await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `snapshots` (`timestamp` LONG INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`timestamp`));"); await _semaphore.ExecuteAsync("CREATE INDEX idx_objects_path_update ON objects(fullname, lastupdate DESC, deleted);"); @@ -106,13 +108,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 +168,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 +182,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 +197,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..1891c14 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; @@ -94,7 +94,7 @@ public interface IDatabase /// /// Task> GetFilesAsync(string path, DateTime timestamp); - + /// /// Gets a list of files. /// @@ -145,21 +145,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..e2d7182 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; @@ -99,7 +99,7 @@ public static bool HasChanged(LocalFile source, LocalFile? target) if (!source.TryGenerateCheckSums() || !target.TryGenerateCheckSums()) return false; return source.LocalCheckSum != target.LocalCheckSum; } - + public static bool IsSameFile(LocalFile source, LocalFile? target) { if (target is null) return false; @@ -320,7 +320,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..b388b92 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; diff --git a/Parallel.Core/IO/Syncing/BaseSyncManager.cs b/Parallel.Core/IO/Syncing/BaseSyncManager.cs index 1ca9471..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); diff --git a/Parallel.Core/IO/Syncing/FileSyncManager.cs b/Parallel.Core/IO/Syncing/FileSyncManager.cs index 187b88d..605cd5e 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; @@ -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 57ddc91..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; 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..ea2c946 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; @@ -192,7 +192,7 @@ public bool TryGenerateCheckSums() fs.Position = 0; LocalCheckSum = Convert.ToHexStringLower(sha256.ComputeHash(fs)); - + //Log.Debug("LocalCheckSum: {LocalCheckSum}", LocalCheckSum); //Log.Debug("RemoteCheckSum: {RemoteCheckSum}", RemoteCheckSum); @@ -204,7 +204,7 @@ public bool TryGenerateCheckSums() return false; } } - + private LocalFile(LocalFile file, long remoteSize, string? remoteCheckSum) { Name = file.Name; 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..56bded9 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -35,13 +35,13 @@ - - - + + + - - - + + + 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..c4b46fa 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; @@ -30,14 +30,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 +54,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 [ @@ -110,6 +114,8 @@ private static HashSet CreateIgnoreDirectories() /// public new static RemoteVaultConfig? Load(string path) { + Console.WriteLine($"Loading remote vault config: {path}"); + Console.WriteLine(File.ReadAllText(path)); return !File.Exists(path) ? null : JsonConvert.DeserializeObject(File.ReadAllText(path)); } 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 index 9c1b9f2..ec83ee7 100644 --- a/Parallel.Desktop/App.axaml +++ b/Parallel.Desktop/App.axaml @@ -3,12 +3,12 @@ x:Class="Parallel.Desktop.App" xmlns:local="using:Parallel.Desktop" RequestedThemeVariant="Default"> - + - + - + diff --git a/Parallel.Desktop/App.axaml.cs b/Parallel.Desktop/App.axaml.cs index cd3a091..be3f5ad 100644 --- a/Parallel.Desktop/App.axaml.cs +++ b/Parallel.Desktop/App.axaml.cs @@ -1,3 +1,5 @@ +// Copyright 2026 Entex Interactive + using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core; @@ -35,11 +37,11 @@ public override void OnFrameworkInitializationCompleted() private void DisableAvaloniaDataAnnotationValidation() { // Get an array of plugins to remove - var dataValidationPluginsToRemove = + DataAnnotationsValidationPlugin[] dataValidationPluginsToRemove = BindingPlugins.DataValidators.OfType().ToArray(); // remove each entry found - foreach (var plugin in dataValidationPluginsToRemove) + foreach (DataAnnotationsValidationPlugin plugin in dataValidationPluginsToRemove) { BindingPlugins.DataValidators.Remove(plugin); } diff --git a/Parallel.Desktop/Parallel.Desktop.csproj b/Parallel.Desktop/Parallel.Desktop.csproj index 467bdfd..45ba91a 100644 --- a/Parallel.Desktop/Parallel.Desktop.csproj +++ b/Parallel.Desktop/Parallel.Desktop.csproj @@ -35,27 +35,27 @@ - + - - - - + + + + none all - - + + - + diff --git a/Parallel.Desktop/Program.cs b/Parallel.Desktop/Program.cs index 9ceb0f0..961498a 100644 --- a/Parallel.Desktop/Program.cs +++ b/Parallel.Desktop/Program.cs @@ -1,4 +1,6 @@ -using Avalonia; +// Copyright 2026 Entex Interactive + +using Avalonia; using System; using System.IO; using System.Reflection; @@ -13,7 +15,7 @@ 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. @@ -21,26 +23,29 @@ sealed class Program 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(); + public static AppBuilder BuildAvaloniaApp() + { + return 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 index 917610e..894db21 100644 --- a/Parallel.Desktop/Settings/ParallelDesktopConfig.cs +++ b/Parallel.Desktop/Settings/ParallelDesktopConfig.cs @@ -1,4 +1,4 @@ -// Copyright 2026 Kyle Ebbinga +// Copyright 2026 Entex Interactive using System.IO; using Parallel.Core.IO; @@ -9,7 +9,7 @@ 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}"); @@ -19,7 +19,7 @@ public class ParallelDesktopConfig : ParallelConfig ParallelDesktopConfig? config = JsonConvert.DeserializeObject(json); return config ?? new ParallelDesktopConfig(); } - + /// /// Saves settings to a file. /// diff --git a/Parallel.Desktop/ViewLocator.cs b/Parallel.Desktop/ViewLocator.cs index f811fc5..f00d473 100644 --- a/Parallel.Desktop/ViewLocator.cs +++ b/Parallel.Desktop/ViewLocator.cs @@ -1,3 +1,5 @@ +// Copyright 2026 Entex Interactive + using System; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; @@ -19,8 +21,8 @@ public class ViewLocator : IDataTemplate if (param is null) return null; - var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - var type = Type.GetType(name); + string name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + Type? type = Type.GetType(name); if (type != null) { diff --git a/Parallel.Desktop/ViewModels/MainWindowViewModel.cs b/Parallel.Desktop/ViewModels/MainWindowViewModel.cs index 3d63c3e..748aa35 100644 --- a/Parallel.Desktop/ViewModels/MainWindowViewModel.cs +++ b/Parallel.Desktop/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,6 @@ -namespace Parallel.Desktop.ViewModels; +// Copyright 2026 Entex Interactive + +namespace Parallel.Desktop.ViewModels; public partial class MainWindowViewModel : ViewModelBase { diff --git a/Parallel.Desktop/ViewModels/ViewModelBase.cs b/Parallel.Desktop/ViewModels/ViewModelBase.cs index 1b44fbf..5851454 100644 --- a/Parallel.Desktop/ViewModels/ViewModelBase.cs +++ b/Parallel.Desktop/ViewModels/ViewModelBase.cs @@ -1,4 +1,6 @@ -using CommunityToolkit.Mvvm.ComponentModel; +// Copyright 2026 Entex Interactive + +using CommunityToolkit.Mvvm.ComponentModel; namespace Parallel.Desktop.ViewModels; diff --git a/Parallel.Desktop/Views/MainWindow.axaml b/Parallel.Desktop/Views/MainWindow.axaml index cf64f06..f8a3b96 100644 --- a/Parallel.Desktop/Views/MainWindow.axaml +++ b/Parallel.Desktop/Views/MainWindow.axaml @@ -13,9 +13,9 @@ - + - + - + \ No newline at end of file diff --git a/Parallel.Desktop/Views/MainWindow.axaml.cs b/Parallel.Desktop/Views/MainWindow.axaml.cs index 1225293..35bcf5b 100644 --- a/Parallel.Desktop/Views/MainWindow.axaml.cs +++ b/Parallel.Desktop/Views/MainWindow.axaml.cs @@ -1,3 +1,5 @@ +// Copyright 2026 Entex Interactive + using Avalonia.Controls; namespace Parallel.Desktop.Views; diff --git a/Parallel.Desktop/app.manifest b/Parallel.Desktop/app.manifest index 4671a47..24ffb81 100644 --- a/Parallel.Desktop/app.manifest +++ b/Parallel.Desktop/app.manifest @@ -1,18 +1,18 @@  - - + + - - - + + + - - - - + + + + From 95495467f36720d57168821d267ac24ef2c0c916 Mon Sep 17 00:00:00 2001 From: TheGuitarleader <26614720+TheGuitarleader@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:55:06 -0500 Subject: [PATCH 3/7] Pull command now allows for default directories --- Parallel.Cli/Commands/PullCommand.cs | 66 +++++++++++------ Parallel.Cli/Commands/StatsCommand.cs | 73 ------------------- Parallel.Cli/Commands/VaultsCommand.cs | 59 ++++++++++++++- .../Database/Contexts/SqliteContext.cs | 9 +-- Parallel.Core/Database/IDatabase.cs | 5 +- Parallel.Core/Models/PullRecord.cs | 37 ++++++++++ Parallel.Core/Settings/RemoteVaultConfig.cs | 9 ++- 7 files changed, 144 insertions(+), 114 deletions(-) delete mode 100644 Parallel.Cli/Commands/StatsCommand.cs create mode 100644 Parallel.Core/Models/PullRecord.cs diff --git a/Parallel.Cli/Commands/PullCommand.cs b/Parallel.Cli/Commands/PullCommand.cs index 549ee7f..8f06835 100644 --- a/Parallel.Cli/Commands/PullCommand.cs +++ b/Parallel.Cli/Commands/PullCommand.cs @@ -19,29 +19,45 @@ public class PullCommand : 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"], "Pulls files before a certain timestamp."); - private readonly Option _remapOpt = new(["--remap"], "The new directory to map restored files to."); + 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 restoring, bypassing safe guards."); + 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 PullCommand() : base("pull", "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(HandlePullAsync, _sourceOpt, _configOpt, _beforeOpt, _remapOpt, _archiveOpt, _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 HandlePullAsync(string? path, string? config, DateTime before, string? remap, bool archive, 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; @@ -50,22 +66,22 @@ private async Task HandlePullAsync(string? path, string? config, DateTime before { if (!string.IsNullOrEmpty(path)) { - await PullPathAsync(localVault, path, timestamp, remap, archive, force, verbose, dryRun); + await PullPathAsync(localVault, path, timestamp, destination, archive, force, verbose, dryRun); } else { - await PullSystemAsync(localVault, timestamp, remap, archive, force, verbose, dryRun); + await PullSystemAsync(localVault, timestamp, destination, archive, force, verbose, dryRun); } } else { if (!string.IsNullOrEmpty(path)) { - await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, remap, archive, force, verbose, dryRun)); + await Program.Settings.ForEachVaultAsync(vault => PullPathAsync(vault, path, timestamp, destination, archive, force, verbose, dryRun)); } else { - await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, remap, archive, force, verbose, dryRun)); + await Program.Settings.ForEachVaultAsync(vault => PullSystemAsync(vault, timestamp, destination, archive, force, verbose, dryRun)); } } } @@ -79,13 +95,13 @@ private async Task PullSystemAsync(LocalVaultConfig vault, DateTime timestamp, s return; } - foreach (string path in syncManager.RemoteVault.PullDirectories) + foreach (PullRecord record in syncManager.RemoteVault.PullDirectories.Where(r => r.Machine == Environment.MachineName)) { - await PullInternalAsync(syncManager, path, timestamp, output, archive, force, verbose, dryRun); + await PullInternalAsync(syncManager, record.Source, timestamp, (output ?? record.Destination), archive, force, verbose, dryRun); } } - private async Task PullPathAsync(LocalVaultConfig vault, string path, DateTime timestamp, string? output, bool archive, 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()) @@ -94,10 +110,10 @@ private async Task PullPathAsync(LocalVaultConfig vault, string path, DateTime t return; } - await PullInternalAsync(syncManager, path, timestamp, output, archive, force, verbose, dryRun); + await PullInternalAsync(syncManager, path, timestamp, destination, archive, force, verbose, dryRun); } - private async Task PullInternalAsync(ISyncManager syncManager, string path, DateTime timestamp, string? output, bool archive, 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, archive) ?? Task.FromResult>([])); @@ -108,7 +124,7 @@ private async Task PullInternalAsync(ISyncManager syncManager, string path, Date { //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; @@ -125,7 +141,7 @@ private async Task PullInternalAsync(ISyncManager syncManager, string path, Date { string fileName = PathBuilder.TempFile; await File.WriteAllLinesAsync(fileName, restoreFiles.Select(f => f.Fullname).OrderBy(f => f)); - CommandLine.WriteLine($"This operation will pull {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 @@ -141,20 +157,20 @@ private async Task PullInternalAsync(ISyncManager syncManager, string path, Date #region Add - private async Task HandleAddAsync(string path, string? config) + 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); + await AddPathAsync(localVault, path, destination); } else { - await Program.Settings.ForEachVaultAsync(vault => AddPathAsync(vault, path)); + await Program.Settings.ForEachVaultAsync(vault => AddPathAsync(vault, path, destination)); } } - private async Task AddPathAsync(LocalVaultConfig vault, string path) + private async Task AddPathAsync(LocalVaultConfig vault, string path, string? destination) { ISyncManager? syncManager = SyncManager.CreateNew(vault); if (syncManager == null || !await syncManager.ConnectAsync()) @@ -163,7 +179,8 @@ private async Task AddPathAsync(LocalVaultConfig vault, string path) return; } - if (!syncManager.RemoteVault.PushDirectories.Add(path)) + PullRecord record = new(path, destination); + if (!syncManager.RemoteVault.PullDirectories.Add(record)) { CommandLine.WriteLine(vault, $"Unable to add path: '{path}'", ConsoleColor.Yellow); return; @@ -199,10 +216,11 @@ private async Task RemovePathAsync(LocalVaultConfig vault, string path) return; } - if (!syncManager.RemoteVault.PushDirectories.Remove(path)) + 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); - return; } CommandLine.WriteLine(vault, $"Successfully removed '{path}'", ConsoleColor.Green); diff --git a/Parallel.Cli/Commands/StatsCommand.cs b/Parallel.Cli/Commands/StatsCommand.cs deleted file mode 100644 index 946a973..0000000 --- a/Parallel.Cli/Commands/StatsCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2026 Entex Interactive - -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/VaultsCommand.cs b/Parallel.Cli/Commands/VaultsCommand.cs index 1119167..aeb584e 100644 --- a/Parallel.Cli/Commands/VaultsCommand.cs +++ b/Parallel.Cli/Commands/VaultsCommand.cs @@ -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)}"); } }); @@ -113,7 +114,7 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.") RemoteVaultConfig remoteVault = syncManager.RemoteVault; CommandLine.WriteLine($"'{remoteVault.Name}' ({remoteVault.Id}):"); CommandLine.WriteArray("Push Directories", remoteVault.PushDirectories); - CommandLine.WriteArray("Pull Directories", remoteVault.PullDirectories); + 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"); @@ -124,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.Core/Database/Contexts/SqliteContext.cs b/Parallel.Core/Database/Contexts/SqliteContext.cs index 144beae..595695d 100644 --- a/Parallel.Core/Database/Contexts/SqliteContext.cs +++ b/Parallel.Core/Database/Contexts/SqliteContext.cs @@ -55,19 +55,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() { diff --git a/Parallel.Core/Database/IDatabase.cs b/Parallel.Core/Database/IDatabase.cs index 1891c14..b07209d 100644 --- a/Parallel.Core/Database/IDatabase.cs +++ b/Parallel.Core/Database/IDatabase.cs @@ -58,7 +58,7 @@ public interface IDatabase #endregion - #region Files + #region Objects /// /// Adds a new file or updates an existing one. @@ -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); 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/Settings/RemoteVaultConfig.cs b/Parallel.Core/Settings/RemoteVaultConfig.cs index c4b46fa..bd763b8 100644 --- a/Parallel.Core/Settings/RemoteVaultConfig.cs +++ b/Parallel.Core/Settings/RemoteVaultConfig.cs @@ -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; @@ -37,7 +38,7 @@ public class RemoteVaultConfig : LocalVaultConfig /// /// A collection of directories to be pulled when reconciling. /// - public HashSet PullDirectories { get; } = []; + public HashSet PullDirectories { get; } = []; /// /// A collection of directories to be ignored when archiving or cleaning. @@ -54,11 +55,11 @@ 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? pushDirectories, IEnumerable? pullDirectories, 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; PushDirectories = new HashSet(pushDirectories ?? []); - PullDirectories = new HashSet(pullDirectories ?? []); + PullDirectories = new HashSet(pullDirectories ?? []); IgnoreDirectories = new HashSet(ignoreDirectories ?? []); PruneDirectories = new HashSet(pruneDirectories ?? []); } @@ -121,7 +122,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 From 7eb689d59e8ce6a72619ad9f038c48cd5d759556 Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:00:45 -0500 Subject: [PATCH 4/7] Separated remote and local checksum generation for performance --- Parallel.Cli/Commands/UnzipCommand.cs | 5 +-- .../Database/Contexts/SqliteContext.cs | 6 ++-- Parallel.Core/IO/Scanning/FileScanner.cs | 24 +++++++------- Parallel.Core/IO/SnapshotManager.cs | 2 +- Parallel.Core/IO/Syncing/FileSyncManager.cs | 2 +- Parallel.Core/Models/LocalFile.cs | 32 +++++++++++++------ Parallel.Core/Parallel.Core.csproj | 6 ---- Parallel.Core/Settings/RemoteVaultConfig.cs | 2 -- 8 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Parallel.Cli/Commands/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs index fac6284..3dc9ca1 100644 --- a/Parallel.Cli/Commands/UnzipCommand.cs +++ b/Parallel.Cli/Commands/UnzipCommand.cs @@ -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,7 +29,7 @@ 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); @@ -48,6 +48,7 @@ private async Task HandleUnzipAsync(string path) }); 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.Core/Database/Contexts/SqliteContext.cs b/Parallel.Core/Database/Contexts/SqliteContext.cs index 595695d..cac3030 100644 --- a/Parallel.Core/Database/Contexts/SqliteContext.cs +++ b/Parallel.Core/Database/Contexts/SqliteContext.cs @@ -26,9 +26,7 @@ public SqliteContext(string filePath) public async Task InitializeAsync() { Log.Information("Creating index database..."); - await _semaphore.ExecuteAsync( - "CREATE TABLE IF NOT EXISTS `objects` (`name` TEXT NOT NULL, `fullname` TEXT NOT NULL, `parentDir` TEXT NOT NULL, `lastWrite` LONG INTEGER NOT NULL, `lastUpdate` LONG INTEGER NOT NULL, `localSize` LONG INTEGER NOT NULL, `remoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readOnly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, `localCheckSum` TEXT, `remoteCheckSum` TEXT, UNIQUE (fullname, localCheckSum));"); - + await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `objects` (`name` TEXT NOT NULL, `fullname` TEXT NOT NULL, `parentDir` TEXT NOT NULL, `lastWrite` LONG INTEGER NOT NULL, `lastUpdate` LONG INTEGER NOT NULL, `localSize` LONG INTEGER NOT NULL, `remoteSize` LONG INTEGER NOT NULL, `type` TEXT NOT NULL DEFAULT Other CHECK(`type` IN ('Document', 'Photo', 'Music', 'Video', 'Other')), `hidden` INTEGER NOT NULL DEFAULT 0, `readOnly` INTEGER NOT NULL DEFAULT 0, `deleted` INTEGER NOT NULL DEFAULT 0, `localCheckSum` TEXT, `remoteCheckSum` TEXT, UNIQUE (fullname, localCheckSum));"); await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `history` (`timestamp` LONG INTEGER NOT NULL, `fullname` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`timestamp`));"); await _semaphore.ExecuteAsync("CREATE TABLE IF NOT EXISTS `snapshots` (`timestamp` LONG INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`timestamp`));"); await _semaphore.ExecuteAsync("CREATE INDEX idx_objects_path_update ON objects(fullname, lastupdate DESC, deleted);"); @@ -41,7 +39,7 @@ await _semaphore.ExecuteAsync( 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; diff --git a/Parallel.Core/IO/Scanning/FileScanner.cs b/Parallel.Core/IO/Scanning/FileScanner.cs index e2d7182..04405d1 100644 --- a/Parallel.Core/IO/Scanning/FileScanner.cs +++ b/Parallel.Core/IO/Scanning/FileScanner.cs @@ -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()); } /// diff --git a/Parallel.Core/IO/SnapshotManager.cs b/Parallel.Core/IO/SnapshotManager.cs index b388b92..4d0c718 100644 --- a/Parallel.Core/IO/SnapshotManager.cs +++ b/Parallel.Core/IO/SnapshotManager.cs @@ -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/FileSyncManager.cs b/Parallel.Core/IO/Syncing/FileSyncManager.cs index 605cd5e..019c6a3 100644 --- a/Parallel.Core/IO/Syncing/FileSyncManager.cs +++ b/Parallel.Core/IO/Syncing/FileSyncManager.cs @@ -37,7 +37,7 @@ public override async Task PushFilesAsync(IReadOnlyList files, I 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!); diff --git a/Parallel.Core/Models/LocalFile.cs b/Parallel.Core/Models/LocalFile.cs index ea2c946..2c558a4 100644 --- a/Parallel.Core/Models/LocalFile.cs +++ b/Parallel.Core/Models/LocalFile.cs @@ -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) { diff --git a/Parallel.Core/Parallel.Core.csproj b/Parallel.Core/Parallel.Core.csproj index 56bded9..e30f761 100644 --- a/Parallel.Core/Parallel.Core.csproj +++ b/Parallel.Core/Parallel.Core.csproj @@ -44,12 +44,6 @@ - - - C:\Users\kebbi\.nuget\packages\newtonsoft.json\13.0.3\lib\net6.0\Newtonsoft.Json.dll - - - diff --git a/Parallel.Core/Settings/RemoteVaultConfig.cs b/Parallel.Core/Settings/RemoteVaultConfig.cs index bd763b8..b1d03d6 100644 --- a/Parallel.Core/Settings/RemoteVaultConfig.cs +++ b/Parallel.Core/Settings/RemoteVaultConfig.cs @@ -115,8 +115,6 @@ private static HashSet CreateIgnoreDirectories() /// public new static RemoteVaultConfig? Load(string path) { - Console.WriteLine($"Loading remote vault config: {path}"); - Console.WriteLine(File.ReadAllText(path)); return !File.Exists(path) ? null : JsonConvert.DeserializeObject(File.ReadAllText(path)); } From 1ddd57f6c42a1e800aea7a590f9dd9e248128d3c Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:02:35 -0500 Subject: [PATCH 5/7] Now can start its own host service to run tasks in the background --- Parallel.Cli/Parallel.Cli.csproj | 4 ++ Parallel.Cli/Services/CleanService.cs | 18 ++++++ Parallel.Cli/Services/LifetimeService.cs | 55 ++++++++++++++++ Parallel.Cli/Services/PruneService.cs | 18 ++++++ Parallel.Cli/Services/RunCommand.cs | 41 ++++++++++++ Parallel.Cli/Services/SyncService.cs | 82 ++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 Parallel.Cli/Services/CleanService.cs create mode 100644 Parallel.Cli/Services/LifetimeService.cs create mode 100644 Parallel.Cli/Services/PruneService.cs create mode 100644 Parallel.Cli/Services/RunCommand.cs create mode 100644 Parallel.Cli/Services/SyncService.cs diff --git a/Parallel.Cli/Parallel.Cli.csproj b/Parallel.Cli/Parallel.Cli.csproj index dc97fa9..fb64575 100644 --- a/Parallel.Cli/Parallel.Cli.csproj +++ b/Parallel.Cli/Parallel.Cli.csproj @@ -40,7 +40,11 @@ + + + + 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 From 65a4744d3728bcb083c088a088200ca41bbd8b84 Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:03:05 -0500 Subject: [PATCH 6/7] Renamed push and pull back to sync and restore for clarity --- .../Commands/{PullCommand.cs => RestoreCommand.cs} | 4 ++-- Parallel.Cli/Commands/{PushCommand.cs => SyncCommand.cs} | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) rename Parallel.Cli/Commands/{PullCommand.cs => RestoreCommand.cs} (98%) rename Parallel.Cli/Commands/{PushCommand.cs => SyncCommand.cs} (98%) diff --git a/Parallel.Cli/Commands/PullCommand.cs b/Parallel.Cli/Commands/RestoreCommand.cs similarity index 98% rename from Parallel.Cli/Commands/PullCommand.cs rename to Parallel.Cli/Commands/RestoreCommand.cs index 8f06835..c2415b4 100644 --- a/Parallel.Cli/Commands/PullCommand.cs +++ b/Parallel.Cli/Commands/RestoreCommand.cs @@ -15,7 +15,7 @@ namespace Parallel.Cli.Commands { - public class PullCommand : Command + public class RestoreCommand : Command { private Stopwatch _sw = new Stopwatch(); @@ -33,7 +33,7 @@ public class PullCommand : 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 PullCommand() : base("pull", "Pulls files a vault.") + public RestoreCommand() : base("restore", "Pulls files a vault.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); diff --git a/Parallel.Cli/Commands/PushCommand.cs b/Parallel.Cli/Commands/SyncCommand.cs similarity index 98% rename from Parallel.Cli/Commands/PushCommand.cs rename to Parallel.Cli/Commands/SyncCommand.cs index ee48977..1084fcf 100644 --- a/Parallel.Cli/Commands/PushCommand.cs +++ b/Parallel.Cli/Commands/SyncCommand.cs @@ -12,7 +12,7 @@ namespace Parallel.Cli.Commands { - public class PushCommand : Command + public class SyncCommand : Command { private Stopwatch _sw = new Stopwatch(); @@ -27,7 +27,7 @@ public class PushCommand : 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 PushCommand() : base("push", "Pushes files to vaults.") + public SyncCommand() : base("sync", "Syncs system files with vaults.") { this.AddOption(_sourceOpt); this.AddOption(_configOpt); @@ -89,6 +89,8 @@ private async Task PushSystemAsync(LocalVaultConfig vault, bool force, bool verb { await PushInternalAsync(syncManager, path, force, verbose); } + + await syncManager.DisconnectAsync(); } private async Task PushPathAsync(LocalVaultConfig vault, string path, bool force, bool verbose) @@ -101,6 +103,7 @@ private async Task PushPathAsync(LocalVaultConfig vault, string path, bool force } await PushInternalAsync(syncManager, path, force, verbose); + await syncManager.DisconnectAsync(); } private async Task PushInternalAsync(ISyncManager syncManager, string path, bool force, bool verbose) @@ -137,7 +140,6 @@ private async Task PushInternalAsync(ISyncManager syncManager, string path, bool int pushedFiles = await syncManager.PushFilesAsync(files, progressReporter, force); CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pushed {pushedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green); - await syncManager.DisconnectAsync(); } #endregion From f6ba5950af4819e4458e6cf177959674f04bc4b8 Mon Sep 17 00:00:00 2001 From: Kyle Ebbinga <26614720+TheGuitarleader@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:05:25 -0500 Subject: [PATCH 7/7] Remove desktop app as its causing more issues currently --- Parallel.Desktop/App.axaml | 15 ---- Parallel.Desktop/App.axaml.cs | 49 ------------- Parallel.Desktop/Assets/avalonia-logo.ico | Bin 175875 -> 0 bytes Parallel.Desktop/Assets/parallel-red.ico | Bin 180837 -> 0 bytes Parallel.Desktop/Parallel.Desktop.csproj | 65 ------------------ Parallel.Desktop/Program.cs | 51 -------------- .../Settings/ParallelDesktopConfig.cs | 33 --------- Parallel.Desktop/ViewLocator.cs | 39 ----------- .../ViewModels/MainWindowViewModel.cs | 8 --- Parallel.Desktop/ViewModels/ViewModelBase.cs | 9 --- Parallel.Desktop/Views/MainWindow.axaml | 21 ------ Parallel.Desktop/Views/MainWindow.axaml.cs | 13 ---- Parallel.Desktop/app.manifest | 18 ----- Parallel.sln | 2 - 14 files changed, 323 deletions(-) delete mode 100644 Parallel.Desktop/App.axaml delete mode 100644 Parallel.Desktop/App.axaml.cs delete mode 100644 Parallel.Desktop/Assets/avalonia-logo.ico delete mode 100644 Parallel.Desktop/Assets/parallel-red.ico delete mode 100644 Parallel.Desktop/Parallel.Desktop.csproj delete mode 100644 Parallel.Desktop/Program.cs delete mode 100644 Parallel.Desktop/Settings/ParallelDesktopConfig.cs delete mode 100644 Parallel.Desktop/ViewLocator.cs delete mode 100644 Parallel.Desktop/ViewModels/MainWindowViewModel.cs delete mode 100644 Parallel.Desktop/ViewModels/ViewModelBase.cs delete mode 100644 Parallel.Desktop/Views/MainWindow.axaml delete mode 100644 Parallel.Desktop/Views/MainWindow.axaml.cs delete mode 100644 Parallel.Desktop/app.manifest diff --git a/Parallel.Desktop/App.axaml b/Parallel.Desktop/App.axaml deleted file mode 100644 index ec83ee7..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 be3f5ad..0000000 --- a/Parallel.Desktop/App.axaml.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2026 Entex Interactive - -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 - DataAnnotationsValidationPlugin[] dataValidationPluginsToRemove = - BindingPlugins.DataValidators.OfType().ToArray(); - - // remove each entry found - foreach (DataAnnotationsValidationPlugin 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 f7da8bb5863b7cecec2adcdebd948fe2f9418d0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 175875 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

tYWpos4X*=^( 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 - - 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 961498a..0000000 --- a/Parallel.Desktop/Program.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2026 Entex Interactive - -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() - { - return 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 894db21..0000000 --- a/Parallel.Desktop/Settings/ParallelDesktopConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2026 Entex Interactive - -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 f00d473..0000000 --- a/Parallel.Desktop/ViewLocator.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2026 Entex Interactive - -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; - - string name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); - Type? 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 748aa35..0000000 --- a/Parallel.Desktop/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2026 Entex Interactive - -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 5851454..0000000 --- a/Parallel.Desktop/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2026 Entex Interactive - -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 f8a3b96..0000000 --- a/Parallel.Desktop/Views/MainWindow.axaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Parallel.Desktop/Views/MainWindow.axaml.cs b/Parallel.Desktop/Views/MainWindow.axaml.cs deleted file mode 100644 index 35bcf5b..0000000 --- a/Parallel.Desktop/Views/MainWindow.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2026 Entex Interactive - -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 24ffb81..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