diff --git a/.gitignore b/.gitignore index 9a9d1e7..707d565 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.userosscache *.sln.docstates *.sln +*.lnk # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Parallel.Cli/Commands/DecryptCommand.cs b/Parallel.Cli/Commands/DecryptCommand.cs index ea28e9c..b451fba 100644 --- a/Parallel.Cli/Commands/DecryptCommand.cs +++ b/Parallel.Cli/Commands/DecryptCommand.cs @@ -2,6 +2,7 @@ using System.CommandLine; using System.Diagnostics; +using System.Text; using Parallel.Cli.Utils; using Parallel.Core.Database; using Parallel.Core.IO; @@ -14,7 +15,7 @@ namespace Parallel.Cli.Commands public class DecryptCommand : Command { private readonly Argument _sourceArg = new("path", "The source path of files to zip."); - private readonly Option _configOpt = new(["--config", "-c"], "The profile configuration to use."); + private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); private IDatabase? _database; private Stopwatch _sw = new Stopwatch(); @@ -26,15 +27,15 @@ public DecryptCommand() : base("decrypt", "Decrypts a file or directory.") this.AddArgument(_sourceArg); this.SetHandler(async (path, config) => { - ProfileConfig? profile = ProfileConfig.Load(Program.Settings, config); - if (profile == null) + VaultConfig? vault = VaultConfig.Load(Program.Settings, config); + if (vault == null) { - CommandLine.WriteLine("No active profile was found!", ConsoleColor.Yellow); + CommandLine.WriteLine("No active vault was found!", ConsoleColor.Yellow); return; } - _database = DatabaseConnection.CreateNew(profile); - string masterKey = profile.FileSystem.EncryptionKey ?? throw new ArgumentException("No encryption key provided!"); + _database = DatabaseConnection.CreateNew(vault); + string masterKey = vault.FileSystem.EncryptionKey ?? throw new ArgumentException("No encryption key provided!"); if (PathBuilder.IsDirectory(path)) { await DecryptDirectoryAsync(path, masterKey); @@ -56,7 +57,7 @@ public DecryptCommand() : base("decrypt", "Decrypts a file or directory.") private async Task DecryptDirectoryAsync(string path, string masterKey) { CommandLine.WriteLine($"Scanning for files in {path}...", ConsoleColor.DarkGray); - string[] files = Directory.EnumerateFiles(path, $"*", SearchOption.AllDirectories).Where(f => !f.EndsWith(".gz")).ToArray(); + string[] files = Directory.EnumerateFiles(path, $"*", SearchOption.AllDirectories).ToArray(); if (files.Length == 0) { CommandLine.WriteLine("No files found to decrypt!", ConsoleColor.Yellow); diff --git a/Parallel.Cli/Commands/DuplicatesCommand.cs b/Parallel.Cli/Commands/DuplicatesCommand.cs index 4d5e2aa..6a4ad92 100644 --- a/Parallel.Cli/Commands/DuplicatesCommand.cs +++ b/Parallel.Cli/Commands/DuplicatesCommand.cs @@ -14,34 +14,15 @@ namespace Parallel.Cli.Commands public class DuplicatesCommand : Command { private Argument sourceArg = new("path", "The directory to scan."); - private Option credsOpt = new(["--credentials", "-c"], "The file system credentials to use."); public DuplicatesCommand() : base("duplicates", "Scans a directory for duplicate files.") { this.AddArgument(sourceArg); - this.AddOption(credsOpt); - this.SetHandler((path, config) => - { - ProfileConfig profile = ProfileConfig.Load(Program.Settings, config); - ScanForDuplicateFiles(path, profile); - }, sourceArg, credsOpt); + this.SetHandler(ScanForDuplicateFiles, sourceArg); } - private void ScanForDuplicateFiles(string path, ProfileConfig profile) + private void ScanForDuplicateFiles(string path) { - IBackupManager backup = BackupManager.CreateNew(profile); - if (!backup.Initialize()) - { - CommandLine.WriteLine("Failed to connect to backup file system!", ConsoleColor.Red); - return; - } - - if (!Directory.Exists(path)) - { - CommandLine.WriteLine("The provided directory is invalid!", ConsoleColor.Yellow); - return; - } - CommandLine.WriteLine($"Scanning for duplicate files in {path}...", ConsoleColor.DarkGray); Dictionary duplicates = FileScanner.GetDuplicateFiles(path); Dictionary result = duplicates.ToDictionary(k => k.Key, v => v.Value.Select(l => l.LocalPath).ToArray()); diff --git a/Parallel.Cli/Commands/EncryptCommand.cs b/Parallel.Cli/Commands/EncryptCommand.cs index dd048c4..f76caf0 100644 --- a/Parallel.Cli/Commands/EncryptCommand.cs +++ b/Parallel.Cli/Commands/EncryptCommand.cs @@ -7,6 +7,7 @@ using Parallel.Core.Database; using Parallel.Core.IO; using Parallel.Core.Models; +using Parallel.Core.Security; using Parallel.Core.Settings; using Parallel.Core.Utils; @@ -15,7 +16,7 @@ namespace Parallel.Cli.Commands public class EncryptCommand : Command { private readonly Argument _sourceArg = new("path", "The source path to encrypt."); - private readonly Option _configOpt = new(["--config", "-c"], "The profile configuration to use."); + private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); private IDatabase? _database; private Stopwatch _sw = new Stopwatch(); @@ -28,15 +29,15 @@ public EncryptCommand() : base("encrypt", "Encrypts a file or directory.") this.SetHandler(async (path, config) => { _sw = Stopwatch.StartNew(); - ProfileConfig? profile = ProfileConfig.Load(Program.Settings, config); - if (profile == null) + VaultConfig? vault = VaultConfig.Load(Program.Settings, config); + if (vault == null) { - CommandLine.WriteLine("No active profile was found!", ConsoleColor.Yellow); + CommandLine.WriteLine("No active vault was found!", ConsoleColor.Yellow); return; } - _database = DatabaseConnection.CreateNew(profile); - string masterKey = profile.FileSystem.EncryptionKey ?? throw new ArgumentException("No encryption key provided!"); + _database = DatabaseConnection.CreateNew(vault); + string masterKey = vault.FileSystem.EncryptionKey ?? throw new ArgumentException("No encryption key provided!"); if (PathBuilder.IsDirectory(path)) { await EncryptDirectoryAsync(path, masterKey); @@ -59,7 +60,7 @@ public EncryptCommand() : base("encrypt", "Encrypts a file or directory.") private async Task EncryptDirectoryAsync(string path, string masterKey) { CommandLine.WriteLine($"Scanning for files in {path}...", ConsoleColor.DarkGray); - string[] files = Directory.EnumerateFiles(path, $"*", SearchOption.AllDirectories).Where(f => !f.EndsWith(".gz")).ToArray(); + string[] files = Directory.EnumerateFiles(path, $"*", SearchOption.AllDirectories).ToArray(); if (files.Length == 0) { CommandLine.WriteLine("No files found to encrypt!", ConsoleColor.Yellow); @@ -87,8 +88,8 @@ private async Task EncryptFileAsync(string path, string masterKey) await using (FileStream openFile = new FileStream(systemFile.LocalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) await using (FileStream createFile = new FileStream(tempFile, FileMode.OpenOrCreate)) { - systemFile.Salt = HashGenerator.RandomBytes(16); - systemFile.IV = HashGenerator.RandomBytes(16); + systemFile.Salt = HashGenerator.GenerateHash(16); + systemFile.IV = HashGenerator.GenerateHash(16); systemFile.Encrypted = true; Encryption.EncryptStream(openFile, createFile, masterKey, systemFile.LastWrite, systemFile.Salt, systemFile.IV); diff --git a/Parallel.Cli/Commands/HistoryCommand.cs b/Parallel.Cli/Commands/HistoryCommand.cs new file mode 100644 index 0000000..eabd7cb --- /dev/null +++ b/Parallel.Cli/Commands/HistoryCommand.cs @@ -0,0 +1,139 @@ +// Copyright 2025 Entex Interactive, LLC + +using System.CommandLine; +using System.Data; +using Parallel.Cli.Utils; +using Parallel.Core.Database; +using Parallel.Core.IO; +using Parallel.Core.IO.Backup; +using Parallel.Core.Models; +using Parallel.Core.Settings; +using Parallel.Core.Utils; +using Formatter = Parallel.Cli.Utils.Formatter; + +namespace Parallel.Cli.Commands +{ + public class HistoryCommand : Command + { + private const int Limit = 25; + + private Command _pullCmd = new("pull", "Shows the history related to pulling files from vaults."); + private Command _pushCmd = new("push", "Shows the history related to pushing files from vaults."); + private Command _deleteCmd = new("archive", "Shows the history related to file deletions."); + private Command _cleanCmd = new("cleaned", "Shows the history related to file cleaning."); + private Command _cloneCmd = new("cloned", "Shows the history related to file cloning."); + private Command _pruneCmd = new("pruned", "Shows the history related to file pruning."); + + private Option _sourceOpt = new(["--path", "-p"], "The source path."); + private Option _vaultOpt = new(["--vault", "-v"], "The vault to use."); + private Option _limitOpt = new(["--limit", "-l"], "The number of entries to show."); + + public HistoryCommand() : base("history", "Shows the history of files related to the archive.") + { + this.AddOption(_sourceOpt); + this.AddOption(_vaultOpt); + this.AddOption(_limitOpt); + this.AddCommand(_pullCmd); + this.AddCommand(_pushCmd); + this.AddCommand(_deleteCmd); + this.AddCommand(_cleanCmd); + this.AddCommand(_cloneCmd); + this.AddCommand(_pruneCmd); + this.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving backup information...", ConsoleColor.DarkGray); + IDatabase? db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _pushCmd.AddOption(_sourceOpt); + _pushCmd.AddOption(_vaultOpt); + _pushCmd.AddOption(_limitOpt); + _pushCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving backup information...", ConsoleColor.DarkGray); + IDatabase? db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Pushed, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _deleteCmd.AddOption(_sourceOpt); + _deleteCmd.AddOption(_vaultOpt); + _deleteCmd.AddOption(_limitOpt); + _deleteCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving archive information...", ConsoleColor.DarkGray); + IDatabase? db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Archived, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _cleanCmd.AddOption(_sourceOpt); + _cleanCmd.AddOption(_vaultOpt); + _cleanCmd.AddOption(_limitOpt); + _cleanCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving clean information...", ConsoleColor.DarkGray); + IDatabase? db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Cleaned, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _cloneCmd.AddOption(_sourceOpt); + _cloneCmd.AddOption(_vaultOpt); + _cloneCmd.AddOption(_limitOpt); + _cloneCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving clone information...", ConsoleColor.DarkGray); + IDatabase db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Cloned, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _pruneCmd.AddOption(_sourceOpt); + _pruneCmd.AddOption(_vaultOpt); + _pruneCmd.AddOption(_limitOpt); + _pruneCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving prune information...", ConsoleColor.DarkGray); + IDatabase db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Pruned, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + + _pullCmd.AddOption(_sourceOpt); + _pullCmd.AddOption(_vaultOpt); + _pullCmd.AddOption(_limitOpt); + _pullCmd.SetHandler((path, config, limit) => + { + CommandLine.WriteLine($"Retrieving restore information...", ConsoleColor.DarkGray); + IDatabase db = DatabaseConnection.CreateNew(VaultConfig.Load(Program.Settings, config)); + + if (limit == 0) limit = Limit; + DisplayHistories(db?.GetHistory(path, HistoryType.Pulled, limit).ToArray()); + }, _sourceOpt, _vaultOpt, _limitOpt); + } + + private void DisplayHistories(HistoryEvent[]? histories) + { + if (histories?.Length == 0) + { + CommandLine.WriteLine("No backup history found!", ConsoleColor.Yellow); + return; + } + + foreach (HistoryEvent history in histories.ToArray()) + { + string typeStr = (history.Type + ":").PadRight(9); + CommandLine.WriteLine($"[{Formatter.FromDateTime(history.CreatedAt.ToLocalTime())}] <{history.Vault}> {typeStr} {history.Fullname}", ConsoleColor.White); + } + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/PullCommand.cs b/Parallel.Cli/Commands/PullCommand.cs new file mode 100644 index 0000000..23d40ba --- /dev/null +++ b/Parallel.Cli/Commands/PullCommand.cs @@ -0,0 +1,9 @@ +// Copyright 2025 Kyle Ebbinga + +namespace Parallel.Cli.Commands +{ + public class PullCommand + { + + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/PushCommand.cs b/Parallel.Cli/Commands/PushCommand.cs new file mode 100644 index 0000000..7d7961c --- /dev/null +++ b/Parallel.Cli/Commands/PushCommand.cs @@ -0,0 +1,94 @@ +// Copyright 2025 Kyle Ebbinga + +using System.CommandLine; +using Parallel.Cli.Utils; +using Parallel.Core.IO; +using Parallel.Core.IO.Backup; +using Parallel.Core.IO.Scanning; +using Parallel.Core.IO.Syncing; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Cli.Commands +{ + public class PushCommand : Command + { + private Command addCmd = new("add", "Adds a new directory to the backup list."); + private Command listCmd = new("list", "Shows all directories in the backup list."); + private Command removeCmd = new("remove", "Removes a directory from the backup list."); + + private readonly Option _sourceArg = new(["--path", "-p"], "The source path to backup."); + private readonly Option _configOpt = new(["--config", "-c"], "The vault configuration to use."); + private readonly Option _verboseOpt = new(["--verbose", "-v"], "Shows verbose output."); + + public PushCommand() : base("push", "Pushes changed files to vaults.") + { + this.AddOption(_sourceArg); + this.AddOption(_configOpt); + this.AddOption(_verboseOpt); + this.SetHandler(async (path, config, verbose) => + { + if (string.IsNullOrEmpty(path)) + { + await SyncSystemAsync(); + } + else + { + await SyncPathAsync(path); + } + + }, _sourceArg, _configOpt, _verboseOpt); + } + + private async Task SyncSystemAsync() + { + throw new NotImplementedException(); + } + + private async Task SyncPathAsync(string path) + { + await ParallelSettings.ForEachVaultAsync(async vault => + { + ISyncManager sync = SyncManager.CreateNew(vault); + if (!sync.Initialize()) + { + CommandLine.WriteLine(vault, $"Failed to connect to vault '{vault.Name}'!", ConsoleColor.Red); + return; + } + + // Normalize paths for safe comparison + string fullPath = Path.GetFullPath(path); + string[] backupFolders = vault.BackupDirectories.ToArray(); + string[] ignoredFolders = vault.IgnoreDirectories.ToArray(); + + bool isFile = PathBuilder.IsFile(fullPath); + if (!backupFolders.Any(dir => fullPath.StartsWith(dir, StringComparison.OrdinalIgnoreCase))) + { + CommandLine.WriteLine(vault, $"The provided {(isFile ? "file" : "folder")} is not set to be backed up!", ConsoleColor.Yellow); + return; + } + + if (FileScanner.IsIgnored(fullPath, ignoredFolders)) + { + CommandLine.WriteLine(vault, $"The provided {(isFile ? "file" : "folder")} is set to be ignored!", ConsoleColor.Yellow); + return; + } + + CommandLine.WriteLine(vault, $"Scanning for file changes in {path}...", ConsoleColor.DarkGray); + + FileScanner scanner = new FileScanner(sync); + SystemFile[] files = await scanner.GetFileChangesAsync(path, ignoredFolders); + int successFiles = files.Length; + if (successFiles == 0) + { + CommandLine.WriteLine(vault, $"The provided {(isFile ? "file" : "folder")} is already up to date.", ConsoleColor.Green); + return; + } + + CommandLine.WriteLine(vault, $"Backing up {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); + await sync.PushFilesAsync(files, new ProgressReport(vault)); + CommandLine.WriteLine(vault, $"Successfully pushed {successFiles.ToString("N0")} files to '{vault.FileSystem.Address}'.", ConsoleColor.Green); + }); + } + } +} \ No newline at end of file diff --git a/Parallel.Cli/Commands/UnzipCommand.cs b/Parallel.Cli/Commands/UnzipCommand.cs index 385bad6..97c0030 100644 --- a/Parallel.Cli/Commands/UnzipCommand.cs +++ b/Parallel.Cli/Commands/UnzipCommand.cs @@ -33,27 +33,13 @@ public UnzipCommand() : base("unzip", "Unzips files in a directory.") CommandLine.WriteLine($"Unzipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); _totalTasks = files.Length; - foreach (string file in files) - { - StartDecompressFile(file, keep); - } - + _tasks.AddRange(files.Select(file => Task.Run(() => DecompressFile(file, keep)))); await Task.WhenAll(_tasks); + CommandLine.WriteLine($"Successfully unzipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); }, sourceArg, keepOpt); } - private void StartDecompressFile(string path, bool keep) - { - Task decompTask = Task.Run(() => - { - DecompressFile(path, keep); - }); - - decompTask.ContinueWith(t => t.Dispose()); - _tasks.Add(decompTask); - } - private void DecompressFile(string path, bool keep) { if (File.Exists(path)) diff --git a/Parallel.Cli/Commands/ConfigCommand.cs b/Parallel.Cli/Commands/VaultsCommand.cs similarity index 72% rename from Parallel.Cli/Commands/ConfigCommand.cs rename to Parallel.Cli/Commands/VaultsCommand.cs index 661a68d..6413342 100644 --- a/Parallel.Cli/Commands/ConfigCommand.cs +++ b/Parallel.Cli/Commands/VaultsCommand.cs @@ -4,27 +4,31 @@ using Parallel.Cli.Utils; using Parallel.Core.Database; using Parallel.Core.IO.FileSystem; +using Parallel.Core.Security; using Parallel.Core.Settings; using Parallel.Core.Utils; namespace Parallel.Cli.Commands { - public class ConfigCommand : Command + public class VaultsCommand : Command { - private Option configOpt = new(["--config", "-c"], "The profile configuration to use."); + private Option configOpt = new(["--config", "-c"], "The vault configuration to use."); - private Command addCmd = new("add", "Adds a new profile configuration."); - private Command editCmd = new("edit", "Edits a profile configuration."); - private Command viewCmd = new("view", "Shows the profile configuration."); - private Command setCmd = new("set", "Sets a new profile configuration."); - private Command delCmd = new("delete", "Deletes a profile configuration."); + private Command addCmd = new("add", "Adds a new vault configuration."); + private Command editCmd = new("edit", "Edits a vault configuration."); + private Command viewCmd = new("view", "Shows the vault configuration."); + private Command setCmd = new("set", "Sets a new vault configuration."); + private Command delCmd = new("delete", "Deletes a vault configuration."); - public ConfigCommand() : base("config", "View or edit the profile configurations.") + public VaultsCommand() : base("vaults", "View or edit the vaults.") { this.SetHandler(() => { - //ProfileConfig profile = ProfileConfig.Load(); - CommandLine.WriteLine($"Current profile: '{Program.Settings.Profiles.FirstOrDefault()}'"); + CommandLine.WriteLine("Active vaults:"); + Program.Settings.ForEachVault(vault => + { + CommandLine.WriteLine(vault.Name); + }); }); this.AddCommand(addCmd); @@ -69,11 +73,11 @@ public ConfigCommand() : base("config", "View or edit the profile configurations fsc.Encrypt = CommandLine.ReadBool("Encrypt files? (y/n)", false); fsc.EncryptionKey = HashGenerator.GenerateHash(32, true); - string profileName = CommandLine.ReadString("Profile Name"); - ProfileConfig profile = new ProfileConfig(profileName, dbc, fsc); - profile.SaveToFile(); + string? profileName = CommandLine.ReadString("Profile Name"); + VaultConfig vault = new VaultConfig(profileName, dbc, fsc); + vault.SaveToFile(); - CommandLine.WriteLine($"Saved new connection profile: '{profile.Name}'"); + CommandLine.WriteLine($"Saved new connection vault: '{vault.Name}'"); }); this.AddCommand(setCmd); diff --git a/Parallel.Cli/Commands/ZipCommand.cs b/Parallel.Cli/Commands/ZipCommand.cs index 3dd1a5d..83722f7 100644 --- a/Parallel.Cli/Commands/ZipCommand.cs +++ b/Parallel.Cli/Commands/ZipCommand.cs @@ -35,27 +35,13 @@ public ZipCommand() : base("zip", "Zips files in a directory.") CommandLine.WriteLine($"Zipping {files.Length.ToString("N0")} files...", ConsoleColor.DarkGray); _totalTasks = files.Length; - foreach (string file in files) - { - StartCompressFile(file, keep); - } - + _tasks.AddRange(files.Select(file => Task.Run(() => CompressFile(file, keep)))); await Task.WhenAll(_tasks); + CommandLine.WriteLine($"Successfully zipped {files.Length.ToString("N0")} files in {_sw.Elapsed}.", ConsoleColor.Green); }, sourceArg, keepOpt); } - private void StartCompressFile(string path, bool keep) - { - Task compTask = Task.Run(() => - { - CompressFile(path, keep); - }); - - compTask.ContinueWith(t => t.Dispose()); - _tasks.Add(compTask); - } - private void CompressFile(string path, bool keep) { if (File.Exists(path)) diff --git a/Parallel.Cli/Program.cs b/Parallel.Cli/Program.cs index b6ed7a0..3c54b91 100644 --- a/Parallel.Cli/Program.cs +++ b/Parallel.Cli/Program.cs @@ -16,7 +16,6 @@ public static async Task Main(string[] args) Settings = ParallelSettings.Load(); string logFile = Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log"); Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.File(logFile).CreateLogger(); - //Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); Log.Information($"{assembly.Name} [Version {assembly.Version}]"); diff --git a/Parallel.Cli/Utils/CommandLine.cs b/Parallel.Cli/Utils/CommandLine.cs index ac5bdf6..049cc89 100644 --- a/Parallel.Cli/Utils/CommandLine.cs +++ b/Parallel.Cli/Utils/CommandLine.cs @@ -1,18 +1,21 @@ // Copyright 2025 Kyle Ebbinga using System.Text; +using Parallel.Core.Settings; using Parallel.Core.Utils; namespace Parallel.Cli.Utils { public class CommandLine { - public static string? ReadString(object value, ConsoleColor color = ConsoleColor.Gray) + private static readonly object _consoleLock = new(); + + public static string ReadString(object value, ConsoleColor color = ConsoleColor.Gray) { Console.ForegroundColor = color; Console.Write($"> {value}: "); Console.ResetColor(); - return Console.ReadLine(); + return Console.ReadLine() ?? string.Empty; } public static bool ReadBool(object value, bool defaultValue, ConsoleColor color = ConsoleColor.Gray) @@ -71,11 +74,56 @@ public static void Write(object value, ConsoleColor color = ConsoleColor.Gray) Console.ResetColor(); } + public static void WriteLine(VaultConfig vault, object value, ConsoleColor color = ConsoleColor.Gray) + { + string baseLog = $"[{vault.Id}] {value}"; + switch(color) + { + default: + Log.Information(baseLog); + break; + + case ConsoleColor.Yellow: + Log.Warning(baseLog); + break; + + case ConsoleColor.Red: + Log.Error(baseLog); + break; + } + + lock (_consoleLock) + { + Console.ForegroundColor = color; + Console.WriteLine($"> {baseLog}"); + Console.ResetColor(); + } + } + public static void WriteLine(object value, ConsoleColor color = ConsoleColor.Gray) { - Console.ForegroundColor = color; - Console.WriteLine($"> {value}"); - Console.ResetColor(); + string baseLog = $"{value}"; + switch(color) + { + default: + Log.Information(baseLog); + break; + + case ConsoleColor.Yellow: + Log.Warning(baseLog); + break; + + case ConsoleColor.Red: + Log.Error(baseLog); + break; + } + + lock (_consoleLock) + { + Console.ForegroundColor = color; + Console.WriteLine($"> {baseLog}"); + Console.ResetColor(); + } } public static void ProgressBar(double part, double total, TimeSpan elapsed, ConsoleColor color = ConsoleColor.Gray) diff --git a/Parallel.Cli/Utils/ProgressReport.cs b/Parallel.Cli/Utils/ProgressReport.cs index a5e1d49..9710575 100644 --- a/Parallel.Cli/Utils/ProgressReport.cs +++ b/Parallel.Cli/Utils/ProgressReport.cs @@ -2,20 +2,21 @@ using Parallel.Core.Diagnostics; using Parallel.Core.Models; +using Parallel.Core.Settings; namespace Parallel.Cli.Utils { - public class ProgressReport : IProgressReporter + public class ProgressReport(VaultConfig vault) : IProgressReporter { public void Report(ProgressOperation operation, SystemFile file, int current, int total) { int percent = current * 100 / total; - CommandLine.WriteLine($"[{percent}%] {operation}: {file.LocalPath}"); + CommandLine.WriteLine($"[{percent}%] <{vault.Id}> {operation}: {file.LocalPath}"); } public void Failed(Exception exception, SystemFile file) { - CommandLine.WriteLine($"Failed to upload file: '{file.LocalPath}'", ConsoleColor.Red); + CommandLine.WriteLine(vault, $"Failed to upload file: '{file.LocalPath}'", ConsoleColor.Red); } } } \ No newline at end of file diff --git a/Parallel.Core.Net/Communication.cs b/Parallel.Core.Net/Communication.cs deleted file mode 100644 index eb9f480..0000000 --- a/Parallel.Core.Net/Communication.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net; -using System.Net.Sockets; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Parallel.Core.Events; -using Parallel.Core.Utils; - -namespace Parallel.Core.Net -{ - /// - /// Represents UDP communication between services. - /// - public class Communication - { - private readonly CancellationTokenSource _exit = new(); - private bool _active; - - /// - /// The primary client for network communication. - /// - public UdpClient Client { get; } = new UdpClient(); - - public event EventHandler RecievedMessage; - - public Communication() - { - Client = new UdpClient(); - } - - public Communication(int port) - { - Client = new UdpClient(new IPEndPoint(IPAddress.Any, port)); - } - - /// - /// Starts listening for messages. - /// - public async Task Start() - { - _active = true; - while (_active && !_exit.IsCancellationRequested) - { - UdpReceiveResult result = await Client.ReceiveAsync(_exit.Token); - RecievedMessage?.Invoke(this, new MessageRecievedEventArgs(result)); - } - } - - /// - /// Stops listening for messages. - /// - public void Stop() - { - _active = false; - _exit.Cancel(); - } - - /// - /// Sends a message to a specified port on a specified remote host. - /// - /// - /// - public void Send(string message, IPEndPoint endPoint) - { - Client.Send(Encoding.UTF8.GetBytes(Encryption.Encode(message)), endPoint); - } - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/Connections/IConnection.cs b/Parallel.Core.Net/Connections/IConnection.cs deleted file mode 100644 index b87b4e4..0000000 --- a/Parallel.Core.Net/Connections/IConnection.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Core.Net.Connections -{ - public interface IConnection - { - ServerResponse SendRequest(ServerRequest request); - //Task SendRequestAsync(ServerRequest request); - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/Connections/TcpConnection.cs b/Parallel.Core.Net/Connections/TcpConnection.cs deleted file mode 100644 index 7e75d9c..0000000 --- a/Parallel.Core.Net/Connections/TcpConnection.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net.Sockets; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Parallel.Core.Utils; -using Serilog; -using Serilog.Core; - -namespace Parallel.Core.Net.Connections -{ - public class TcpConnection : IConnection - { - private readonly string _address; - private readonly int _port; - - /// - /// Initializes a new instance of the class with the saved settings. - /// - public TcpConnection() - { - _address = "127.0.0.1"; - _port = 8192; - } - - /// - /// Initializes a new instance of the class with a address and port. - /// - public TcpConnection(string address, int port) - { - _address = address; - _port = port; - } - - public ServerResponse SendRequest(ServerRequest request) - { - Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); - ServerResponse response = new(request); - - try - { - socket.Connect(_address, _port); - if (socket.Connected) - { - // Sends an encrypted json request to the server. - string rawJson = JsonConvert.SerializeObject(request) + ";"; - Log.Debug($"Sending request: '{rawJson}'"); - socket.Send(Encoding.UTF8.GetBytes(rawJson)); - - // The encrypted returned json - string returnedData = string.Empty; - using (NetworkStream ns = new(socket)) - { - while (!returnedData.EndsWith(';')) - { - Console.WriteLine("Waiting for response..."); - byte[] buffer = new byte[socket.ReceiveBufferSize]; - int bytesRead = ns.Read(buffer, 0, socket.ReceiveBufferSize); - returnedData += Encoding.UTF8.GetString(buffer, 0, bytesRead); - } - } - - Log.Debug($"Response: {returnedData}"); - JToken? json = JToken.Parse(returnedData.TrimEnd(';')); - response = ServerResponse.Parse(request, json); - - // Closes the socket. - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - return response; - } - else - { - Log.Warning($"Failed to connect to server '{_address}:{_port}'"); - return response; - } - } - catch (Exception ex) - { - Log.Warning(ex.Message); - return response; - } - } - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/MessageResult.cs b/Parallel.Core.Net/MessageResult.cs deleted file mode 100644 index 0bd428a..0000000 --- a/Parallel.Core.Net/MessageResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Core.Net -{ - public struct MessageResult - { - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/Parallel.Core.Net.csproj b/Parallel.Core.Net/Parallel.Core.Net.csproj deleted file mode 100644 index be6daf4..0000000 --- a/Parallel.Core.Net/Parallel.Core.Net.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - diff --git a/Parallel.Core.Net/ServerRequest.cs b/Parallel.Core.Net/ServerRequest.cs deleted file mode 100644 index 932865c..0000000 --- a/Parallel.Core.Net/ServerRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Newtonsoft.Json; - -namespace Parallel.Core.Net -{ - public class ServerRequest - { - /// - /// The request name. - /// - public string Name { get; } - - /// - /// The request parameters. - /// - public Dictionary Parameters { get; } - - /// - /// Initializes new instance of the class with a request name and a of parameters. - /// - /// The request name. - /// A collection of parameter keys and values. - [JsonConstructor] - public ServerRequest(string name, Dictionary parameters) - { - Name = name.ToLower(); - Parameters = parameters; - } - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/ServerResponse.cs b/Parallel.Core.Net/ServerResponse.cs deleted file mode 100644 index 103438c..0000000 --- a/Parallel.Core.Net/ServerResponse.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Newtonsoft.Json.Linq; - -namespace Parallel.Core.Net.Connections -{ - public class ServerResponse - { - public ServerRequest Request { get; } - public int StatusCode { get; } - public bool Success { get; } = false; - public JToken? Data { get; } - public string? Message { get; } - public string? Error { get; } - - public ServerResponse(ServerRequest request) - { - Request = request; - } - - private ServerResponse(ServerRequest request, JToken? data, string? message, string? error, int statusCode) - { - Request = request; - StatusCode = statusCode; - Success = statusCode == 200; - Data = data; - Message = message; - Error = error; - } - - public static ServerResponse Parse(ServerRequest request, JToken? json) - { - int statusCode = json?["status"]?.Value() ?? 408; - JToken? data = json?["data"]; - string? message = json?.Value("message"); - string? error = json?.Value("error"); - - return new ServerResponse(request, data, message, error, statusCode); - } - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/ISocketHandler.cs b/Parallel.Core.Net/Sockets/ISocketHandler.cs deleted file mode 100644 index 08398c3..0000000 --- a/Parallel.Core.Net/Sockets/ISocketHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net; -using Parallel.Core.Utils; - -namespace Parallel.Core.Net.Sockets -{ - public interface ISocketHandler - { - /// - /// The time the socket was received. - /// - UnixTime ReceivedAt { get; } - - /// - /// The raw string of incoming data decrypted. - /// - string RawData { get; set; } - - /// - /// The remote client that sent the request. - /// - IPEndPoint RemoteEndPoint { get; } - - /// - /// Shuts down the , closes the connection, and releases all resources. - /// - void Close(); - - /// - /// Reads the incoming encrypted data as formatted JSON string. - /// - /// - ServerRequest? Parse(); - - /// - /// Responds to the current request. - /// - /// - Task RespondAsync(object? data); - } -} \ No newline at end of file diff --git a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs b/Parallel.Core.Net/Sockets/TcpSocketHandler.cs deleted file mode 100644 index de68a70..0000000 --- a/Parallel.Core.Net/Sockets/TcpSocketHandler.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net; -using System.Net.Sockets; -using System.Text; -using Newtonsoft.Json; -using Parallel.Core.Utils; -using Serilog; -using Serilog.Core; - -namespace Parallel.Core.Net.Sockets -{ - public class TcpSocketHandler : ISocketHandler - { - /// - /// - /// - public Socket Socket { get; } - - /// - public UnixTime ReceivedAt { get; } - - /// - public string RawData { get; set; } = string.Empty; - - /// - public IPEndPoint RemoteEndPoint { get; } - - /// - /// Initializes a new instance of the class for the specified socket. - /// - /// The socket to handle. - public TcpSocketHandler(Socket socket) - { - ReceivedAt = UnixTime.Now; - Socket = socket; - RemoteEndPoint = (IPEndPoint)socket.RemoteEndPoint; - } - - public void Close() - { - Socket.Shutdown(SocketShutdown.Both); - Socket.Close(); - } - - public ServerRequest? Parse() - { - using (NetworkStream ns = new(Socket)) - { - while (!RawData.EndsWith(';')) - { - byte[] buffer = new byte[Socket.ReceiveBufferSize]; - int bytesRead = ns.Read(buffer, 0, Socket.ReceiveBufferSize); - RawData += Encoding.UTF8.GetString(buffer, 0, bytesRead); - Log.Debug(RawData); - } - } - - return JsonConvert.DeserializeObject(RawData.TrimEnd(';')); - } - - public Task RespondAsync(object? data) - { - try - { - string json = JsonConvert.SerializeObject(data, Formatting.Indented); - Socket?.Send(Encoding.UTF8.GetBytes(json + ";")); - Close(); - return Task.CompletedTask; - } - catch (ObjectDisposedException) - { - throw; - } - catch (Exception ex) - { - Log.Error(ex.Message); - return Task.CompletedTask; - } - } - } -} \ No newline at end of file diff --git a/Parallel.Core/Database/Contexts/SqliteContext.cs b/Parallel.Core/Database/Contexts/SqliteContext.cs index 738fab1..d0b4c2c 100644 --- a/Parallel.Core/Database/Contexts/SqliteContext.cs +++ b/Parallel.Core/Database/Contexts/SqliteContext.cs @@ -43,8 +43,8 @@ public async Task InitializeAsync() File.SetAttributes(FilePath, File.GetAttributes(FilePath) | FileAttributes.Hidden); using IDbConnection connection = CreateConnection(); - await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS `files` (`profile` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` 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, `encrypted` INTEGER NOT NULL DEFAULT 0, `salt` BLOB, `iv` BLOB, PRIMARY KEY(`profile`, `id`));"); - await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS `history` (`profile` TEXT NOT NULL, `timestamp` LONG INTEGER NOT NULL, `path` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`profile`, `timestamp`));"); + await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS `files` (`vault` TEXT NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `localpath` TEXT NOT NULL, `remotepath` 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, `encrypted` INTEGER NOT NULL DEFAULT 0, `salt` TEXT, `iv` TEXT, `checksum` TEXT, PRIMARY KEY(`vault`, `id`));"); + await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS `history` (`vault` TEXT NOT NULL, `timestamp` LONG INTEGER NOT NULL, `path` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`vault`, `timestamp`));"); } #endregion @@ -55,15 +55,15 @@ public async Task InitializeAsync() public async Task AddFileAsync(SystemFile file) { using IDbConnection connection = CreateConnection(); - string sql = @"INSERT OR REPLACE INTO files (profile, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, encrypted, salt, iv) VALUES (@ProfileId, @Id, @Name, @LocalPath, @RemotePath, @LastWrite, @LastUpdate, @LocalSize, @RemoteSize, @Type, @Hidden, @ReadOnly, @Deleted, @Encrypted, @Salt, @IV);"; - return await connection.ExecuteAsync(sql, new { ProfileId, file.Id, file.Name, file.LocalPath, file.RemotePath, LastWrite = file.LastWrite.TotalMilliseconds, LastUpdate = UnixTime.Now.TotalMilliseconds, file.LocalSize, file.RemoteSize, Type = file.Type.ToString(), file.Hidden, file.ReadOnly, file.Deleted, file.Encrypted, file.Salt, file.IV }) > 0; + string sql = @"INSERT OR REPLACE INTO files (vault, id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, encrypted, salt, iv, checksum) VALUES (@ProfileId, @Id, @Name, @LocalPath, @RemotePath, @LastWrite, @LastUpdate, @LocalSize, @RemoteSize, @Type, @Hidden, @ReadOnly, @Deleted, @Encrypted, @Salt, @IV, @CheckSum);"; + return await connection.ExecuteAsync(sql, new { ProfileId, file.Id, file.Name, file.LocalPath, file.RemotePath, LastWrite = file.LastWrite.TotalMilliseconds, LastUpdate = UnixTime.Now.TotalMilliseconds, file.LocalSize, file.RemoteSize, Type = file.Type.ToString(), file.Hidden, file.ReadOnly, file.Deleted, file.Encrypted, file.Salt, file.IV, file.CheckSum }) > 0; } /// public async Task> GetFilesAsync(string path, bool deleted) { using IDbConnection connection = CreateConnection(); - string sql = $"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND deleted = {deleted} ORDER BY lastupdate DESC"; + string sql = $"SELECT * FROM files WHERE vault = \"{ProfileId}\" AND deleted = {deleted} ORDER BY lastupdate DESC"; return await connection.QueryAsync(sql); } @@ -71,7 +71,7 @@ public async Task> GetFilesAsync(string path, bool delet public async Task GetFileAsync(string path) { using IDbConnection connection = CreateConnection(); - string sql = $"SELECT * FROM files WHERE profile = \"{ProfileId}\" AND localpath LIKE \"%{path}%\" OR remotepath LIKE \"%{path}%\" ORDER BY lastupdate DESC"; + string sql = $"SELECT * FROM files WHERE vault = \"{ProfileId}\" AND localpath LIKE \"%{path}%\" OR remotepath LIKE \"%{path}%\" ORDER BY lastupdate DESC"; return await connection.QuerySingleOrDefaultAsync(sql); } @@ -83,10 +83,20 @@ public async Task> GetFilesAsync(string path, bool delet public async Task AddHistoryAsync(string path, HistoryType type) { using IDbConnection connection = CreateConnection(); - string sql = @"INSERT OR REPLACE INTO history (profile, timestamp, name, path, type) VALUES(@ProfileId, @Timestamp, @Name, @Path, @Type);"; + string sql = @"INSERT OR REPLACE INTO history (vault, timestamp, name, path, type) VALUES(@ProfileId, @Timestamp, @Name, @Path, @Type);"; return await connection.ExecuteAsync(sql, new { ProfileId, Timestamp = UnixTime.Now.TotalMilliseconds, Name = Path.GetFileName(path), Path = path, Type = type }) > 0; } + public IEnumerable? GetHistory(string path, int limit) + { + throw new NotImplementedException(); + } + + public IEnumerable? GetHistory(string path, HistoryType type, int limit) + { + throw new NotImplementedException(); + } + #endregion } } \ No newline at end of file diff --git a/Parallel.Core/Database/DatabaseConnection.cs b/Parallel.Core/Database/DatabaseConnection.cs index b2c8998..3fd19f0 100644 --- a/Parallel.Core/Database/DatabaseConnection.cs +++ b/Parallel.Core/Database/DatabaseConnection.cs @@ -17,15 +17,15 @@ public enum DatabaseProvider /// public class DatabaseConnection { - public static IDatabase CreateNew(ProfileConfig profile) + public static IDatabase? CreateNew(VaultConfig? vault) { - switch(profile.Database.Provider) + switch(vault?.Database.Provider) { default: return null; case DatabaseProvider.Local: - IDatabase db = new SqliteContext(profile.Database, profile.Id); - if (!File.Exists(profile.Database.Address)) db.InitializeAsync(); + IDatabase db = new SqliteContext(vault.Database, vault.Id); + if (!File.Exists(vault.Database.Address)) db.InitializeAsync(); return db; } } diff --git a/Parallel.Core/Database/IDatabase.cs b/Parallel.Core/Database/IDatabase.cs index 594674f..65fea25 100644 --- a/Parallel.Core/Database/IDatabase.cs +++ b/Parallel.Core/Database/IDatabase.cs @@ -29,19 +29,19 @@ public enum HistoryType Cloned, /// - /// A file that has been deleted from the backup. + /// A file that has been deleted from the vault. /// Pruned, /// - /// A file that was deleted and has been restored. + /// A file that was pulled from the vault. /// - Restored, + Pulled, /// - /// A newly synced file. + /// A file that was pushed to the vault. /// - Synced + Pushed } /// @@ -50,7 +50,7 @@ public enum HistoryType public interface IDatabase { /// - /// The identifier to the profile for this database. + /// The identifier to the vault for this database. /// string ProfileId { get; } @@ -91,9 +91,13 @@ public interface IDatabase /// True if successful, false otherwise Task AddHistoryAsync(string path, HistoryType type); + IEnumerable? GetHistory(string path, int limit); + + IEnumerable? GetHistory(string path, HistoryType type, int limit); + #endregion - Task> GetFilesAsync(string path, bool b); + Task> GetFilesAsync(string path, bool deleted); Task GetFileAsync(string path); } } \ No newline at end of file diff --git a/Parallel.Core/Diagnostics/ProgressDebug.cs b/Parallel.Core/Diagnostics/ProgressLogger.cs similarity index 95% rename from Parallel.Core/Diagnostics/ProgressDebug.cs rename to Parallel.Core/Diagnostics/ProgressLogger.cs index 9ca0b6e..c624e39 100644 --- a/Parallel.Core/Diagnostics/ProgressDebug.cs +++ b/Parallel.Core/Diagnostics/ProgressLogger.cs @@ -7,7 +7,7 @@ namespace Parallel.Core.Diagnostics /// /// Represents a basic progress report debugger. /// - public class ProgressDebug : IProgressReporter + public class ProgressLogger : IProgressReporter { private ProgressOperation currentOperation; private int progressPercentage; diff --git a/Parallel.Core/IO/Backup/BaseFileManager.cs b/Parallel.Core/IO/Backup/BaseFileManager.cs deleted file mode 100644 index 53f9b7e..0000000 --- a/Parallel.Core/IO/Backup/BaseFileManager.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Database; -using Parallel.Core.Diagnostics; -using Parallel.Core.Events; -using Parallel.Core.IO.FileSystem; -using Parallel.Core.Models; -using Parallel.Core.Settings; - -namespace Parallel.Core.IO.Backup -{ - /// - /// Represents the base way of backing up files to an associated file system. - /// - public abstract class BaseFileManager : IBackupManager - { - /// - public ProfileConfig Profile { get; } - - /// - public IDatabase Database { get; set; } - - /// - public IFileSystem FileSystem { get; set; } - - /// - public string MachineName { get; } = Environment.MachineName; - - /// - public string RootFolder { get; set; } - - /// - /// - /// - /// - public BaseFileManager(ProfileConfig profile) - { - FileSystem = FileSystemManager.CreateNew(profile.FileSystem); - Profile = profile; - } - - /// - public virtual bool Initialize() - { - try - { - Database = DatabaseConnection.CreateNew(Profile); - bool fsInit = (FileSystem != null) && FileSystem.PingAsync().Result >= 0; - Profile.IgnoreDirectories.Add(Profile.FileSystem.RootDirectory); - if (Profile != null) Profile.SaveToFile(); - return fsInit; - } - catch (Exception ex) - { - Log.Error(ex.GetBaseException().ToString()); - return false; - } - } - - /// - public abstract Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress); - - /// - public abstract Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress); - } -} \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs index 9268564..941d721 100644 --- a/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs +++ b/Parallel.Core/IO/FileSystem/DotNetFileSystem.cs @@ -17,15 +17,15 @@ namespace Parallel.Core.IO.FileSystem /// public class DotNetFileSystem : IFileSystem { - private readonly FileSystemCredentials _credentials; + private readonly VaultConfig _vault; /// /// Represents an for interacting with physical machine hardware. /// - /// The credentials to log in with. - public DotNetFileSystem(FileSystemCredentials credentials) + /// The vault to use. + public DotNetFileSystem(VaultConfig vault) { - _credentials = credentials; + _vault = vault; } /// @@ -83,7 +83,7 @@ public Task GetDirectoryNameAsync(string path) public Task> GetFilesAsync() { Dictionary files = new Dictionary(); - foreach (string file in Directory.GetFiles(PathBuilder.RootDirectory(_credentials), "*.gz", SearchOption.AllDirectories)) + foreach (string file in Directory.GetFiles(PathBuilder.RootDirectory(_vault), "*.gz", SearchOption.AllDirectories)) { FileInfo fi = new(file); files.Add(fi.FullName, new SystemFile(file) @@ -132,7 +132,7 @@ public Task GetFileAsync(string path) public Task PingAsync() { Stopwatch sw = Stopwatch.StartNew(); - if (!Directory.Exists(PathBuilder.RootDirectory(_credentials))) return Task.FromResult(-1); + if (!Directory.Exists(PathBuilder.RootDirectory(_vault))) return Task.FromResult(-1); return Task.FromResult(sw.ElapsedMilliseconds); } @@ -140,13 +140,11 @@ public Task PingAsync() public async Task UploadFilesAsync(SystemFile[] files, IProgressReporter progress) { if (!files.Any()) return; - for (int i = 0; i < files.Length; i++) + await Task.WhenAll(files.Select(file => Task.Run(async () => { Stopwatch sw = new Stopwatch(); - SystemFile file = files[i]; - file.RemotePath = PathBuilder.Remote(file.LocalPath, _credentials); + file.RemotePath = PathBuilder.Remote(file.LocalPath, _vault); - progress.Report(ProgressOperation.Uploading, file, i, files.Length); if (File.Exists(file.RemotePath)) File.SetAttributes(file.RemotePath, ~FileAttributes.ReadOnly & File.GetAttributes(file.RemotePath)); string parent = Path.GetDirectoryName(file.RemotePath); if (!Directory.Exists(parent)) Directory.CreateDirectory(parent); @@ -158,7 +156,7 @@ public async Task UploadFilesAsync(SystemFile[] files, IProgressReporter progres Log.Debug($"Uploaded '{file.RemotePath}' in {sw.ElapsedMilliseconds}ms"); File.SetAttributes(file.RemotePath, File.GetAttributes(file.RemotePath) | FileAttributes.ReadOnly); - } + }))); } } } \ No newline at end of file diff --git a/Parallel.Core/IO/FileSystem/FileSystemManager.cs b/Parallel.Core/IO/FileSystem/FileSystemManager.cs index 97fa7b5..1e93b4d 100644 --- a/Parallel.Core/IO/FileSystem/FileSystemManager.cs +++ b/Parallel.Core/IO/FileSystem/FileSystemManager.cs @@ -33,13 +33,13 @@ public static class FileSystemManager /// /// Creates a new file system association. /// - /// The credentials needed for the associated file system. - public static IFileSystem CreateNew(FileSystemCredentials credentials) + /// The vault needed for the associated file system. + public static IFileSystem CreateNew(VaultConfig vault) { - return credentials?.Service switch + return vault.FileSystem.Service switch { - FileService.Local => new DotNetFileSystem(credentials), - FileService.Remote => new SftpFileSystem(credentials), + FileService.Local => new DotNetFileSystem(vault), + FileService.Remote => new SftpFileSystem(vault), //FileService.Cloud => new AmazonS3FileSystem(credentials), _ => null }; diff --git a/Parallel.Core/IO/FileSystem/SftpFileSystem.cs b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs index 58ec6cd..3a6a503 100644 --- a/Parallel.Core/IO/FileSystem/SftpFileSystem.cs +++ b/Parallel.Core/IO/FileSystem/SftpFileSystem.cs @@ -18,15 +18,16 @@ namespace Parallel.Core.IO.FileSystem public class SftpFileSystem : IFileSystem { private readonly ConnectionInfo _connectionInfo; + private readonly VaultConfig _vault; /// /// Represents an for interacting with an SSH server. /// - /// The credentials to log in with. - public SftpFileSystem(FileSystemCredentials credentials) + /// The credentials to log in with. + public SftpFileSystem(VaultConfig vault) { - Console.WriteLine(JObject.FromObject(credentials)); - _connectionInfo = new ConnectionInfo(credentials.Address, credentials.Username, new PasswordAuthenticationMethod(credentials.Username, Encryption.Decode(credentials.Password))); + _connectionInfo = new ConnectionInfo(vault.FileSystem.Address, vault.FileSystem.Username, new PasswordAuthenticationMethod(vault.FileSystem.Username, Encryption.Decode(vault.FileSystem.Password))); + _vault = vault; } /// diff --git a/Parallel.Core/IO/PathBuilder.cs b/Parallel.Core/IO/PathBuilder.cs index 6833235..99fe668 100644 --- a/Parallel.Core/IO/PathBuilder.cs +++ b/Parallel.Core/IO/PathBuilder.cs @@ -71,15 +71,15 @@ public static string Local(string path, FileSystemCredentials credentials) return main.Replace(@"\", "/"); } - public static string RootDirectory(FileSystemCredentials credentials) + public static string RootDirectory(VaultConfig vault) { - string root = Path.Combine(credentials.RootDirectory, "Parallel", Environment.MachineName); + string root = Path.Combine(vault.FileSystem.RootDirectory, "Parallel", vault.Id); Log.Debug($"Root directory: {root}"); - return credentials.Service switch + return vault.FileSystem.Service switch { FileService.Local => root, FileService.Remote => root.Replace('\\', '/'), - _ => null + _ => string.Empty }; } @@ -89,14 +89,14 @@ public static string RootDirectory(FileSystemCredentials credentials) /// /// /// - public static string Remote(string path, FileSystemCredentials credentials) + public static string Remote(string path, VaultConfig vault) { - string root = Path.Combine(credentials.RootDirectory, "Parallel", Environment.MachineName, path.Replace(":", string.Empty)) + ".gz"; - return credentials.Service switch + string root = Path.Combine(vault.FileSystem.RootDirectory, "Parallel", vault.Id, "Files", path.Replace(":", string.Empty)) + ".gz"; + return vault.FileSystem.Service switch { FileService.Local => root, FileService.Remote => root.Replace('\\', '/'), - _ => null + _ => string.Empty }; } diff --git a/Parallel.Core/IO/Recovery/RecoveryManager.cs b/Parallel.Core/IO/Recovery/RecoveryManager.cs index ccab8da..43f1ad7 100644 --- a/Parallel.Core/IO/Recovery/RecoveryManager.cs +++ b/Parallel.Core/IO/Recovery/RecoveryManager.cs @@ -17,7 +17,7 @@ public class RecoveryManager public IDatabase Database { get; set; } public IFileSystem FileSystem { get; set; } - public ProfileConfig Profile { get; set; } + public VaultConfig Vault { get; set; } public string MachineName { get; } = Environment.MachineName; public string RootFolder { get; set; } @@ -25,21 +25,21 @@ public class RecoveryManager /// Initializes a new instance of the class. /// /// - public RecoveryManager(ProfileConfig profile) + public RecoveryManager(VaultConfig vault) { - Profile = profile; - Database = DatabaseConnection.CreateNew(profile); - FileSystem = FileSystemManager.CreateNew(profile.FileSystem); + Vault = vault; + Database = DatabaseConnection.CreateNew(vault); + FileSystem = FileSystemManager.CreateNew(vault); } public bool Initialize() { try { - Database = DatabaseConnection.CreateNew(Profile); + Database = DatabaseConnection.CreateNew(Vault); bool fsInit = (FileSystem != null) && FileSystem.PingAsync().Result >= 0; - Profile.IgnoreDirectories.Add(Profile.FileSystem.RootDirectory); - if (Profile != null) Profile.SaveToFile(); + Vault.IgnoreDirectories.Add(Vault.FileSystem.RootDirectory); + if (Vault != null) Vault.SaveToFile(); return fsInit; } catch (Exception ex) diff --git a/Parallel.Core/IO/Scanning/FileScanner.cs b/Parallel.Core/IO/Scanning/FileScanner.cs index 90374fc..699af81 100644 --- a/Parallel.Core/IO/Scanning/FileScanner.cs +++ b/Parallel.Core/IO/Scanning/FileScanner.cs @@ -2,8 +2,11 @@ using System.Data; using System.Diagnostics; +using System.Text; +using Newtonsoft.Json.Linq; using Parallel.Core.Database; using Parallel.Core.IO.Backup; +using Parallel.Core.IO.Syncing; using Parallel.Core.Models; using Parallel.Core.Settings; using Parallel.Core.Utils; @@ -16,19 +19,19 @@ namespace Parallel.Core.IO.Scanning /// public class FileScanner { - private readonly ProfileConfig _profile; + private readonly VaultConfig _vault; private readonly IDatabase _db; - public FileScanner(ProfileConfig profile, IDatabase database) + public FileScanner(VaultConfig vault, IDatabase database) { - _profile = profile; + _vault = vault; _db = database; } - public FileScanner(IBackupManager backup) + public FileScanner(ISyncManager sync) { - _profile = backup.Profile; - _db = backup.Database; + _vault = sync.Vault; + _db = sync.Database; } /*/// @@ -58,77 +61,63 @@ public async Task GetFileChangesAsync(string path, string[] ignore { if (!Directory.Exists(path)) return Array.Empty(); - List scannedFiles = new(); - List localFiles = FileScanner.GetFiles(path, ".", ignoreFolders).ToList(); - List remoteFiles = (await _db.GetFilesAsync(path, false)).ToList(); - Stopwatch sw = Stopwatch.StartNew(); - - foreach (SystemFile rsf in remoteFiles.ToArray()) + List scannedFiles = new List(); + HashSet localFiles = FileScanner.GetFiles(path, ".", ignoreFolders).ToHashSet(); + IEnumerable remoteFiles = await _db.GetFilesAsync(path, false); + foreach (SystemFile remoteFile in remoteFiles) { - // Checks if the local file has a valid path and is part of a backup folder. - if (rsf.LocalPath != null && rsf.LocalPath.Contains(path)) + if (File.Exists(remoteFile.LocalPath) && remoteFile.RemotePath != null) { - // Checks if a LocalFile exists on the current file system. - if (File.Exists(rsf.LocalPath) && rsf.RemotePath != null) + SystemFile localFile = new SystemFile(remoteFile.LocalPath); + if (IsIgnored(localFile.LocalPath, ignoreFolders)) { - SystemFile lfi = new(rsf.LocalPath); - if (IsIgnored(lfi.LocalPath, ignoreFolders)) - { - Log.Debug($"Is ignored -> {lfi.LocalPath}"); - - lfi.Deleted = true; - scannedFiles.Add(lfi); - } - - if (rsf.LastWrite.TotalMilliseconds < lfi.LastWrite.TotalMilliseconds) - { - Log.Debug($"Changed -> {lfi.LocalPath}"); - - // Changed file - rsf.Deleted = false; - scannedFiles.Add(lfi); - } - - localFiles.Remove(lfi.LocalPath); + Log.Debug($"Ignored -> {localFile.LocalPath}"); + localFile.Deleted = true; + scannedFiles.Add(localFile); } - else + else if (HasChanged(localFile, remoteFile)) { - // Adds deleted files - Log.Debug($"Deleted -> {rsf.LocalPath}"); - - rsf.Deleted = true; - scannedFiles.Add(rsf); + Log.Debug($"Changed -> {localFile.LocalPath}"); + scannedFiles.Add(localFile); } + + localFiles.Remove(localFile.LocalPath); } else { - // Deletes ignored files - Log.Debug($"No contains Ignored -> {rsf.LocalPath}"); - - rsf.Deleted = true; - scannedFiles.Add(rsf); + Log.Debug($"Deleted -> {remoteFile.LocalPath}"); + remoteFile.Deleted = true; + scannedFiles.Add(remoteFile); } } Log.Debug($"{localFiles.Count} files are untracked! Adding..."); - if (localFiles.Count > 0) + foreach (var file in localFiles) { - foreach (string file in localFiles.ToArray()) + if (File.Exists(file) && !IsIgnored(file, ignoreFolders)) { - if (File.Exists(file) && !IsIgnored(file, ignoreFolders)) - { - Log.Debug($"Created -> {file}"); - scannedFiles.Add(new SystemFile(file)); - localFiles.Remove(file); - } + Log.Debug($"Created -> {file}"); + scannedFiles.Add(new SystemFile(file)); } } Log.Debug($"{localFiles.Count} files remaining."); - Log.Information($"Found {localFiles.Count.ToString("N0")} files in '{path}'. ({sw.ElapsedMilliseconds}ms)"); + Log.Information($"Found {scannedFiles.Count:N0} changes in '{path}'"); return scannedFiles.ToArray(); } + /// + /// Gets if a file has changed. + /// + /// The base file to compare. + /// The remote file to compare to. + /// True is success, otherwise false. + public static bool HasChanged(SystemFile localFile, SystemFile? remoteFile) + { + return remoteFile == null || (localFile.LastWrite.TotalMilliseconds > remoteFile.LastWrite.TotalMilliseconds && !localFile.CheckSum.SequenceEqual(remoteFile.CheckSum)); + } + + /// /// Gets the total size, in bytes, of a directory. /// @@ -225,7 +214,7 @@ public static FileInfo[] GetCleanableFiles(string path, UnixTime start, bool rec public static IEnumerable GetFiles(string root, string searchPattern) { - return GetFiles(root, searchPattern, Array.Empty()); + return GetFiles(root, searchPattern, []); } public static IEnumerable GetFiles(string root, string searchPattern, string[] exempt) @@ -235,7 +224,7 @@ public static IEnumerable GetFiles(string root, string searchPattern, st while (pending.Count != 0) { string path = pending.Pop(); - IEnumerable next = null; + IEnumerable? next = null; try { if (!IsIgnored(path, exempt)) diff --git a/Parallel.Core/IO/Syncing/BaseSyncManager.cs b/Parallel.Core/IO/Syncing/BaseSyncManager.cs new file mode 100644 index 0000000..c2f3362 --- /dev/null +++ b/Parallel.Core/IO/Syncing/BaseSyncManager.cs @@ -0,0 +1,60 @@ +// Copyright 2025 Kyle Ebbinga + +using Parallel.Core.Database; +using Parallel.Core.Diagnostics; +using Parallel.Core.IO.Backup; +using Parallel.Core.IO.FileSystem; +using Parallel.Core.Models; +using Parallel.Core.Settings; + +namespace Parallel.Core.IO.Syncing +{ + /// + /// Represents the base way of backing up files to an associated file system. + /// + public abstract class BaseSyncManager : ISyncManager + { + /// + public VaultConfig Vault { get; } + + /// + public IDatabase Database { get; set; } + + /// + public IFileSystem FileSystem { get; set; } + + /// + /// + /// + /// + public BaseSyncManager(VaultConfig vault) + { + FileSystem = FileSystemManager.CreateNew(vault); + Vault = vault; + } + + /// + public virtual bool Initialize() + { + try + { + Database = DatabaseConnection.CreateNew(Vault); + FileSystem.CreateDirectoryAsync(PathBuilder.RootDirectory(Vault)); + Vault.IgnoreDirectories.Add(Vault.FileSystem.RootDirectory); + Vault.SaveToFile(); + return FileSystem.PingAsync().Result >= 0; + } + catch (Exception ex) + { + Log.Error(ex.GetBaseException().ToString()); + return false; + } + } + + /// + public abstract Task PushFilesAsync(SystemFile[] files, IProgressReporter progress); + + /// + public abstract Task PullFilesAsync(SystemFile[] files, IProgressReporter progress); + } +} \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/DeltaBackupManager.cs b/Parallel.Core/IO/Syncing/DeltaSyncManager.cs similarity index 51% rename from Parallel.Core/IO/Backup/DeltaBackupManager.cs rename to Parallel.Core/IO/Syncing/DeltaSyncManager.cs index 20c2084..10663d6 100644 --- a/Parallel.Core/IO/Backup/DeltaBackupManager.cs +++ b/Parallel.Core/IO/Syncing/DeltaSyncManager.cs @@ -4,27 +4,27 @@ using Parallel.Core.Models; using Parallel.Core.Settings; -namespace Parallel.Core.IO.Backup +namespace Parallel.Core.IO.Syncing { /// /// Represents the way to clone files to an associated file system using file deltas. /// - public class DeltaBackupManager : BaseFileManager + public class DeltaSyncManager : BaseSyncManager { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// - public DeltaBackupManager(ProfileConfig profile) : base(profile) { } + /// + public DeltaSyncManager(VaultConfig vault) : base(vault) { } /// - public override Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress) + public override Task PushFilesAsync(SystemFile[] files, IProgressReporter progress) { throw new NotImplementedException(); } /// - public override Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress) + public override Task PullFilesAsync(SystemFile[] files, IProgressReporter progress) { throw new NotImplementedException(); } diff --git a/Parallel.Core/IO/Backup/FileBackupManager.cs b/Parallel.Core/IO/Syncing/FileSyncManager.cs similarity index 78% rename from Parallel.Core/IO/Backup/FileBackupManager.cs rename to Parallel.Core/IO/Syncing/FileSyncManager.cs index 4cd4427..57ecf45 100644 --- a/Parallel.Core/IO/Backup/FileBackupManager.cs +++ b/Parallel.Core/IO/Syncing/FileSyncManager.cs @@ -5,6 +5,7 @@ using Parallel.Core.Diagnostics; using Parallel.Core.Events; using Parallel.Core.IO.FileSystem; +using Parallel.Core.IO.Syncing; using Parallel.Core.Models; using Parallel.Core.Settings; @@ -13,24 +14,26 @@ namespace Parallel.Core.IO.Backup /// /// Represents the way to archive files to an associated file system. /// - public class FileBackupManager : BaseFileManager + public class FileSyncManager : BaseSyncManager { private List _tasks = new List(); private int _totalFiles; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// - public FileBackupManager(ProfileConfig profile) : base(profile) { } + /// + public FileSyncManager(VaultConfig vault) : base(vault) { } /// - public override async Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress) + public override async Task PushFilesAsync(SystemFile[] files, IProgressReporter progress) { if (!files.Any()) return; SystemFile[] backupFiles = files.Where(f => !f.Deleted).ToArray(); Log.Information($"Backing up {backupFiles.Length} files..."); await FileSystem.UploadFilesAsync(backupFiles, progress); + + Console.WriteLine($"Successfully pushed {backupFiles.Length} files.", ConsoleColor.Green); for (int i = 0; i < files.Length; i++) { SystemFile file = files.ElementAt(i); @@ -47,7 +50,7 @@ public override async Task BackupFilesAsync(SystemFile[] files, IProgressReporte if (remote is not null) { file.RemoteSize = remote.RemoteSize; - await Database.AddHistoryAsync(file.LocalPath, HistoryType.Synced); + await Database.AddHistoryAsync(file.LocalPath, HistoryType.Pushed); await Database.AddFileAsync(file); } } @@ -55,7 +58,7 @@ public override async Task BackupFilesAsync(SystemFile[] files, IProgressReporte } /// - public override async Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress) + public override async Task PullFilesAsync(SystemFile[] files, IProgressReporter progress) { SystemFile[] restoreFiles = files.Where(f => f.Deleted).ToArray(); @@ -66,7 +69,7 @@ public override async Task RestoreFilesAsync(SystemFile[] files, IProgressReport { SystemFile file = files[i]; Log.Information($"Restoring file: {file.LocalPath}..."); - file.RemotePath = PathBuilder.Remote(file.LocalPath, Profile.FileSystem); + file.RemotePath = PathBuilder.Remote(file.LocalPath, Vault); } } } diff --git a/Parallel.Core/IO/Backup/IBackupManager.cs b/Parallel.Core/IO/Syncing/ISyncManager.cs similarity index 60% rename from Parallel.Core/IO/Backup/IBackupManager.cs rename to Parallel.Core/IO/Syncing/ISyncManager.cs index d819857..21a5641 100644 --- a/Parallel.Core/IO/Backup/IBackupManager.cs +++ b/Parallel.Core/IO/Syncing/ISyncManager.cs @@ -2,22 +2,21 @@ using Parallel.Core.Database; using Parallel.Core.Diagnostics; -using Parallel.Core.Events; using Parallel.Core.IO.FileSystem; using Parallel.Core.Models; using Parallel.Core.Settings; -namespace Parallel.Core.IO.Backup +namespace Parallel.Core.IO.Syncing { /// /// Defines the methods needed for backing up a file system. /// - public interface IBackupManager + public interface ISyncManager { /// - /// The back-up connection profile. + /// The back-up connection vault. /// - public ProfileConfig Profile { get; } + public VaultConfig Vault { get; } /// /// The associated database connection. @@ -29,17 +28,6 @@ public interface IBackupManager /// IFileSystem FileSystem { get; set; } - /// - /// The current machine name. - /// - string MachineName { get; } - - /// - /// The root directory of the back-up. - /// - string RootFolder { get; set; } - - /// /// Initializes the backup manager by logging into the and /// @@ -47,17 +35,17 @@ public interface IBackupManager bool Initialize(); /// - /// Backs up a path. Can be either a file or directory. + /// Pushes an array of files to a vault. /// /// /// - Task BackupFilesAsync(SystemFile[] files, IProgressReporter progress); + Task PushFilesAsync(SystemFile[] files, IProgressReporter progress); /// - /// Restores a path. Can be either a file or directory. + /// Pulls an array of files from a vault. /// /// /// - Task RestoreFilesAsync(SystemFile[] files, IProgressReporter progress); + Task PullFilesAsync(SystemFile[] files, IProgressReporter progress); } } \ No newline at end of file diff --git a/Parallel.Core/IO/Backup/BackupManager.cs b/Parallel.Core/IO/Syncing/SyncManager.cs similarity index 52% rename from Parallel.Core/IO/Backup/BackupManager.cs rename to Parallel.Core/IO/Syncing/SyncManager.cs index 875c06f..4b5124f 100644 --- a/Parallel.Core/IO/Backup/BackupManager.cs +++ b/Parallel.Core/IO/Syncing/SyncManager.cs @@ -7,22 +7,23 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Parallel.Core.IO.Syncing; namespace Parallel.Core.IO.Backup { /// - /// Represents the way manage s. + /// Represents the way manage s. /// - public static class BackupManager + public static class SyncManager { /// - /// Creates a new instance of an . + /// Creates a new instance of an . /// - /// + /// /// - public static IBackupManager CreateNew(ProfileConfig profile) + public static ISyncManager CreateNew(VaultConfig vault) { - return new FileBackupManager(profile); + return new FileSyncManager(vault); } } } \ No newline at end of file diff --git a/Parallel.Core/Models/HistoryEvent.cs b/Parallel.Core/Models/HistoryEvent.cs index 320c483..e8a4d49 100644 --- a/Parallel.Core/Models/HistoryEvent.cs +++ b/Parallel.Core/Models/HistoryEvent.cs @@ -1,9 +1,15 @@ // Copyright 2025 Kyle Ebbinga +using Parallel.Core.Database; +using Parallel.Core.Utils; + namespace Parallel.Core.Models { public class HistoryEvent { - + public HistoryType Type { get; set; } + public UnixTime CreatedAt { get; set; } + public string Vault { get; set; } + public string Fullname { get; set; } } } \ No newline at end of file diff --git a/Parallel.Core/Models/SystemFile.cs b/Parallel.Core/Models/SystemFile.cs index aa4ccdb..514abbd 100644 --- a/Parallel.Core/Models/SystemFile.cs +++ b/Parallel.Core/Models/SystemFile.cs @@ -3,6 +3,7 @@ using System.Data; using Parallel.Core.Data; using Parallel.Core.Diagnostics; +using Parallel.Core.Security; using Parallel.Core.Utils; namespace Parallel.Core.Models @@ -80,12 +81,17 @@ public class SystemFile /// /// The salt used to encrypt the file. /// - public byte[] Salt { get; set; } = Array.Empty(); + public string Salt { get; set; } /// /// The initialization vector used to encrypt the file. /// - public byte[] IV { get; set; } = Array.Empty(); + public string IV { get; set; } + + /// + /// The checksum used to check if the file has changed. + /// + public string? CheckSum { get; set; } /// @@ -99,27 +105,39 @@ public SystemFile(string path) LocalPath = fileInfo.FullName; LocalSize = fileInfo.Length; RemoteSize = fileInfo.Length; - Type = FileTypes.GetFileCategory(Path.GetExtension(fileInfo.Name)); LastWrite = new UnixTime(fileInfo.LastWriteTime); LastUpdate = UnixTime.Now; + Type = FileTypes.GetFileCategory(Path.GetExtension(fileInfo.Name)); + Hidden = fileInfo.Attributes.HasFlag(FileAttributes.Hidden); + ReadOnly = fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly); Deleted = !fileInfo.Exists; - - if (fileInfo.Attributes.HasFlag(FileAttributes.Hidden)) - { - Hidden = true; - } - - if (fileInfo.Attributes.HasFlag(FileAttributes.ReadOnly)) - { - ReadOnly = true; - } + Encrypted = false; + Salt = HashGenerator.GenerateHash(16); + IV = HashGenerator.GenerateHash(16); + CheckSum = HashGenerator.CheckSum(path); } /// /// Initializes a new instance of the class. /// - /// - public SystemFile(string profile, string id, string name, string localpath, string remotepath, long lastwrite, long lastupdate, long localsize, long remotesize, string type, long hidden, long readOnly, long deleted, long encrypted, byte[] salt, byte[] iv) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SystemFile(string vault, string id, string name, string localpath, string remotepath, long lastwrite, long lastupdate, long localsize, long remotesize, string type, long hidden, long readOnly, long deleted, long encrypted, string salt, string iv, string checksum) { Id = id; Name = name; @@ -129,13 +147,13 @@ public SystemFile(string profile, string id, string name, string localpath, stri LastUpdate = UnixTime.FromMilliseconds(lastupdate); LocalSize = localsize; RemoteSize = remotesize; - //Type = type; Hidden = Converter.ToBool(hidden); ReadOnly = Converter.ToBool(readOnly); Deleted = Converter.ToBool(deleted); Encrypted = Converter.ToBool(encrypted); Salt = salt; IV = iv; + CheckSum = checksum; } public bool Equals(SystemFile value) @@ -155,6 +173,7 @@ public bool Equals(SystemFile value) value?.Encrypted != null ? this.Encrypted.Equals(value.Encrypted) : (bool?)null, this?.Salt != null && value?.Salt != null ? this.Salt.SequenceEqual(value.Salt) : (bool?)null, this?.IV != null && value?.IV != null ? this.IV.SequenceEqual(value.IV) : (bool?)null, + this?.CheckSum != null && value?.CheckSum != null ? this.CheckSum.SequenceEqual(value.CheckSum) : (bool?)null, ]; return results.All(b => b != null && (bool)b); diff --git a/Parallel.Core/Security/Encryption.cs b/Parallel.Core/Security/Encryption.cs index 029f6fe..3a7fc45 100644 --- a/Parallel.Core/Security/Encryption.cs +++ b/Parallel.Core/Security/Encryption.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using Parallel.Core.Models; +using Parallel.Core.Security; namespace Parallel.Core.Utils { @@ -46,14 +47,14 @@ public static string Decode(string value) /// /// /// - public static void EncryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp, byte[] salt, byte[] iv) + public static void EncryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp, string salt, string iv) { input.Position = 0; - byte[] derivedKey = HashGenerator.HKDF(masterKey, salt, timestamp.ToISOString(), 32); + byte[] derivedKey = HashGenerator.HKDF(masterKey, Encoding.ASCII.GetBytes(salt), timestamp.ToISOString(), 32); using (Aes aes = Aes.Create()) { aes.Key = derivedKey; - aes.IV = iv; + aes.IV = Encoding.UTF8.GetBytes(iv); aes.Mode = CipherMode.CBC; using (CryptoStream cryptoStream = new CryptoStream(output, aes.CreateEncryptor(), CryptoStreamMode.Write)) { @@ -69,14 +70,14 @@ public static void EncryptStream(Stream input, Stream output, string masterKey, /// /// /// - public static void DecryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp, byte[] salt, byte[] iv) + public static void DecryptStream(Stream input, Stream output, string masterKey, UnixTime timestamp, string salt, string iv) { input.Position = 0; - byte[] derivedKey = HashGenerator.HKDF(masterKey, salt, timestamp.ToISOString(), 32); + byte[] derivedKey = HashGenerator.HKDF(masterKey, Encoding.ASCII.GetBytes(salt), timestamp.ToISOString(), 32); using (Aes aes = Aes.Create()) { aes.Key = derivedKey; - aes.IV = iv; + aes.IV = Encoding.ASCII.GetBytes(iv); aes.Mode = CipherMode.CBC; using (CryptoStream cryptoStream = new CryptoStream(input, aes.CreateDecryptor(), CryptoStreamMode.Read)) { diff --git a/Parallel.Core/Security/HashGenerator.cs b/Parallel.Core/Security/HashGenerator.cs index 43614f4..71fa07f 100644 --- a/Parallel.Core/Security/HashGenerator.cs +++ b/Parallel.Core/Security/HashGenerator.cs @@ -3,13 +3,18 @@ using System.Security.Cryptography; using System.Text; -namespace Parallel.Core.Utils +namespace Parallel.Core.Security { /// /// Provides functionality for generating random hashes. This class cannot be inherited. /// public static class HashGenerator { + /// + /// Generates a random series of bytes. + /// + /// + /// public static byte[] RandomBytes(int length) { byte[] bytes = new byte[length]; @@ -76,5 +81,18 @@ public static string CreateSHA256(string value) ArgumentException.ThrowIfNullOrEmpty(value); return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLower(); } + + /// + /// + /// + /// + /// + public static string? CheckSum(string path) + { + if (!File.Exists(path)) return null; + using FileStream fs = File.OpenRead(path); + using SHA256 sha256 = SHA256.Create(); + return Convert.ToHexString(sha256.ComputeHash(fs)).ToLowerInvariant(); + } } } \ No newline at end of file diff --git a/Parallel.Core/Settings/DatabaseCredentials.cs b/Parallel.Core/Settings/DatabaseCredentials.cs index 306c45a..761427b 100644 --- a/Parallel.Core/Settings/DatabaseCredentials.cs +++ b/Parallel.Core/Settings/DatabaseCredentials.cs @@ -24,12 +24,12 @@ public class DatabaseCredentials /// /// The username of the database. /// - public string Username { get; set; } = string.Empty; + public string? Username { get; set; } /// /// The password of the database. /// - public string Password { get; set; } = string.Empty; + public string? Password { get; set; } /// /// The database name. diff --git a/Parallel.Core/Settings/FileSystemCredentials.cs b/Parallel.Core/Settings/FileSystemCredentials.cs index db02bf1..8000bf9 100644 --- a/Parallel.Core/Settings/FileSystemCredentials.cs +++ b/Parallel.Core/Settings/FileSystemCredentials.cs @@ -13,9 +13,9 @@ public class FileSystemCredentials { public FileService Service { get; set; } = FileService.Local; public string RootDirectory { get; set; } = string.Empty; - public string Address { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; + public string? Address { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } /// /// If the file system is encrypting files. diff --git a/Parallel.Core/Settings/ParallelSettings.cs b/Parallel.Core/Settings/ParallelSettings.cs index 412a8b5..d323473 100644 --- a/Parallel.Core/Settings/ParallelSettings.cs +++ b/Parallel.Core/Settings/ParallelSettings.cs @@ -19,7 +19,7 @@ public class ParallelSettings /// /// The location of files for different file system credentials./>. /// - public static string ProfilesDir { get; } = Path.Combine(PathBuilder.ProgramData, "Profiles"); + public static string VaultsDir { get; } = Path.Combine(PathBuilder.ProgramData, "Vaults"); /// /// The address that will accept incoming commands. @@ -37,7 +37,7 @@ public class ParallelSettings /// The profiles to use. /// The CLI defaults to the first in the list. /// - public HashSet Profiles { get; } = new HashSet(); + public HashSet Vaults { get; } = new HashSet(); /// @@ -66,5 +66,47 @@ public void Save() if (!Directory.Exists(PathBuilder.ProgramData)) Directory.CreateDirectory(PathBuilder.ProgramData); File.WriteAllText(ConfigFile, JsonConvert.SerializeObject(this, Formatting.Indented)); } + + /// + /// + /// + /// + public void ForEachVault(Action action) + { + foreach (string path in Directory.GetFiles(VaultsDir, "*.json", SearchOption.TopDirectoryOnly)) + { + VaultConfig? vault = VaultConfig.Load(path); + if (vault != null) action(vault); + } + } + + /// + /// Asynchronously runs an for each with a default of 3 at a time. + /// + /// + /// + public static async Task ForEachVaultAsync(Func actionAsync, int maxDegreeOfParallelism = 3) + { + string[] vaultPaths = Directory.GetFiles(VaultsDir, "*.json", SearchOption.TopDirectoryOnly); + SemaphoreSlim semaphore = new SemaphoreSlim(maxDegreeOfParallelism); + IEnumerable tasks = vaultPaths.Select(path => Task.Run(async () => + { + await semaphore.WaitAsync(); + try + { + VaultConfig? vault = VaultConfig.Load(path); + if (vault != null) + { + await actionAsync(vault); + } + } + finally + { + semaphore.Release(); + } + })); + + await Task.WhenAll(tasks); + } } } \ No newline at end of file diff --git a/Parallel.Core/Settings/ProfileConfig.cs b/Parallel.Core/Settings/VaultConfig.cs similarity index 75% rename from Parallel.Core/Settings/ProfileConfig.cs rename to Parallel.Core/Settings/VaultConfig.cs index 5878534..c1a480d 100644 --- a/Parallel.Core/Settings/ProfileConfig.cs +++ b/Parallel.Core/Settings/VaultConfig.cs @@ -5,6 +5,8 @@ using Newtonsoft.Json.Linq; using Parallel.Core.Database; using Parallel.Core.IO.FileSystem; +using Parallel.Core.IO.Syncing; +using Parallel.Core.Security; using Parallel.Core.Utils; namespace Parallel.Core.Settings @@ -12,15 +14,15 @@ namespace Parallel.Core.Settings /// /// Represents a back-up connection. /// - public class ProfileConfig + public class VaultConfig { /// - /// A unique hash used to identify the profile. + /// A unique hash used to identify the vault. /// public string Id { get; } = HashGenerator.GenerateHash(12, true); /// - /// The name of the profile. + /// The name of the vault. /// public string Name { get; set; } = "Default"; @@ -80,14 +82,14 @@ public class ProfileConfig /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// /// /// [JsonConstructor] - public ProfileConfig(string id, string name, DatabaseCredentials database, FileSystemCredentials fileSystem) + public VaultConfig(string id, string name, DatabaseCredentials database, FileSystemCredentials fileSystem) { Id = id; Name = name; @@ -96,12 +98,12 @@ public ProfileConfig(string id, string name, DatabaseCredentials database, FileS } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// /// - public ProfileConfig(string name, DatabaseCredentials database, FileSystemCredentials fileSystem) + public VaultConfig(string name, DatabaseCredentials database, FileSystemCredentials fileSystem) { Id = HashGenerator.GenerateHash(12, true); Name = name; @@ -112,53 +114,39 @@ public ProfileConfig(string name, DatabaseCredentials database, FileSystemCreden /// /// Loads settings from a file. /// - public static ProfileConfig Load(string path) + public static VaultConfig? Load(string path) { - if (File.Exists(path)) - { - string json = File.ReadAllText(path); - return JsonConvert.DeserializeObject(json); - } - else - { - string name = Path.GetFileNameWithoutExtension(path); - return new ProfileConfig(name, new DatabaseCredentials(), new FileSystemCredentials()); - } + if (!File.Exists(path)) return null; + string json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); } /// /// Loads credentials from the app configuration. /// - /// A instance. - public static ProfileConfig? Load(ParallelSettings settings, string name) + /// A instance. + public static VaultConfig? Load(ParallelSettings settings, string name) { - ProfileConfig profile = Load(settings.Profiles.First()); - if (!string.IsNullOrEmpty(name)) - { - string path = Path.Combine(ParallelSettings.ProfilesDir, name + ".json"); - if (!File.Exists(path)) return null; - profile = Load(Path.GetFileNameWithoutExtension(path)); - } - - return profile; + VaultConfig? vault = Load(settings.Vaults.First()); + return string.IsNullOrEmpty(name) ? vault : Load(Path.Combine(ParallelSettings.VaultsDir, name + ".json")); } /// /// Saves credentials to a file. /// - /// The current profile to save. - public static void Save(ProfileConfig profile) + /// The current vault to save. + public static void Save(VaultConfig vault) { - if (!Directory.Exists(ParallelSettings.ProfilesDir)) Directory.CreateDirectory(ParallelSettings.ProfilesDir); - string path = Path.Combine(ParallelSettings.ProfilesDir, profile.Name + ".json"); - Log.Debug($"Saving profile file: {path}"); + if (!Directory.Exists(ParallelSettings.VaultsDir)) Directory.CreateDirectory(ParallelSettings.VaultsDir); + string path = Path.Combine(ParallelSettings.VaultsDir, vault.Name + ".json"); + Log.Debug($"Saving vault file: {path}"); if (!File.Exists(path)) { Log.Debug("Creating file -> " + path); File.Create(path).Close(); } - File.WriteAllText(path, JsonConvert.SerializeObject(profile, Formatting.Indented)); + File.WriteAllText(path, JsonConvert.SerializeObject(vault, Formatting.Indented)); } /// diff --git a/Parallel.Service/Parallel.Service.csproj b/Parallel.Service/Parallel.Service.csproj deleted file mode 100644 index bfde18a..0000000 --- a/Parallel.Service/Parallel.Service.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - Exe - net9.0 - enable - parallel-red.ico - enable - ParallelService - 1.0.0 - Kyle Ebbinga - Copyright $(Company). All Rights Reserved. - $(AssemblyVersion) - $(VersionPrefix)$(AssemblyVersion) - $(Company) - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Parallel.Service/Program.cs b/Parallel.Service/Program.cs deleted file mode 100644 index 8950aa8..0000000 --- a/Parallel.Service/Program.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Parallel.Core.IO; -using Parallel.Core.Settings; -using Parallel.Core.Utils; -using Parallel.Service.Requests; -using Parallel.Service.Services; - -namespace Parallel.Service -{ - internal class Program - { - internal static readonly string LogFile = Path.Combine(PathBuilder.ProgramData, "Logs", "latest.txt"); - - static async Task Main(string[] args) - { - HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); - - // Logging - Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().WriteTo.File(LogFile).CreateLogger(); - builder.Logging.ClearProviders(); - builder.Logging.AddSerilog(); - - AssemblyName assembly = Assembly.GetExecutingAssembly().GetName(); - Log.Information($"{assembly.Name} v{assembly.Version}"); - - // Add Windows services - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - builder.Services.AddWindowsService(); - } - - // Add Linux systemd - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - builder.Services.AddSystemd(); - } - - // Background services - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - builder.Services.AddHostedService(); - - // Other services - builder.Services.AddSingleton(ParallelSettings.Load()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - IHost host = builder.Build(); - IHostApplicationLifetime lifetime = host.Services.GetRequiredService(); - ParallelSettings settings = host.Services.GetRequiredService(); - lifetime.ApplicationStopped.Register(() => - { - settings.Save(); - }); - - // Starts the application - await host.RunAsync(); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/RequestHandler.cs b/Parallel.Service/RequestHandler.cs deleted file mode 100644 index a121c6e..0000000 --- a/Parallel.Service/RequestHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using Parallel.Core.Net; -using Parallel.Service.Requests; - -namespace Parallel.Service -{ - public class RequestHandler - { - public Dictionary Requests { get; } - - public RequestHandler() - { - Type[] types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Where(t => typeof(BaseRequest).IsAssignableFrom(t) && !t.IsAbstract).ToArray(); - Requests = types.ToDictionary(t => t.Name.Replace("Request", ""), t => t, StringComparer.OrdinalIgnoreCase); - - // Logs if any requests failed - if (Requests.Count != types.Length) - { - int remaining = types.Length - Requests.Count; - Log.Warning($"Failed to register {remaining} requests"); - } - } - - /// - /// Creates an to be handled. - /// - /// The name of the request. - /// The corresponding . If none was found a help request will be returned. - public IRequest? CreateNew(ServerRequest request) - { - Dictionary headers = new Dictionary(request.Parameters, StringComparer.OrdinalIgnoreCase); - if (!Requests.TryGetValue(request.Name, out Type? requestType)) - { - Log.Warning($"Unknown command: {request.Name}"); - return null; - } - - // Instantiate the request object - object? instance = Activator.CreateInstance(requestType); - if (instance is not IRequest requestInstance) return null; - - // Map parameters to object properties - foreach (PropertyInfo prop in requestType.GetProperties()) - { - if (headers.TryGetValue(prop.Name, out string? value)) - { - try - { - object? converted = Convert.ChangeType(value, prop.PropertyType); - prop.SetValue(instance, converted); - } - catch (Exception ex) - { - Log.Warning($"Failed to convert '{value}' to {prop.PropertyType.Name} for property '{prop.Name}': {ex.Message}"); - } - } - } - - - // Validate required properties - List? validationResults = new List(); - ValidationContext? context = new ValidationContext(instance, serviceProvider: null, items: null); - if (!Validator.TryValidateObject(instance, context, validationResults, validateAllProperties: true)) - { - string? errors = string.Join("; ", validationResults.Select(r => r.ErrorMessage)); - Log.Warning($"Validation failed for '{request.Name}': {errors}"); - return null; - } - - return requestInstance; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Requests/BaseRequest.cs b/Parallel.Service/Requests/BaseRequest.cs deleted file mode 100644 index f515432..0000000 --- a/Parallel.Service/Requests/BaseRequest.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Net.Sockets; -using Parallel.Service.Responses; - -namespace Parallel.Service.Requests -{ - /// - /// The base implementation for an - /// - public abstract class BaseRequest : IRequest - { - protected ISocketHandler Handler { get; } - - public abstract Task ExecuteAsync(); - - public virtual void Dispose() - { - Handler.Close(); - GC.SuppressFinalize(this); - } - - public static MessageResponse Ok() - { - return new MessageResponse("Success", 200); - } - - public static MessageResponse Ok(string message) - { - return new MessageResponse(message, 200); - } - - public static ObjectResponse Json(object data) - { - return new ObjectResponse(data, 200); - } - - public static MessageResponse BadRequest(string message) - { - return new MessageResponse(message, 401); - } - - public static MessageResponse Unauthorized() - { - return new MessageResponse("Unauthorized", 401); - } - - public static MessageResponse Forbidden() - { - return new MessageResponse("Forbidden", 403); - } - - public static ErrorResponse InternalServerError(Exception exception) - { - return new ErrorResponse(exception, 500); - } - - public static MessageResponse NotImplemented() - { - return new MessageResponse("Function not implemented", 501); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Requests/HelpRequest.cs b/Parallel.Service/Requests/HelpRequest.cs deleted file mode 100644 index 7d4ee43..0000000 --- a/Parallel.Service/Requests/HelpRequest.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using Newtonsoft.Json.Linq; -using Parallel.Service.Responses; - -namespace Parallel.Service.Requests -{ - [Description("Lists all avalible requests to the server.")] - public class HelpRequest : BaseRequest - { - public override Task ExecuteAsync() - { - RequestHandler handler = new RequestHandler(); - - JArray jsonArray = new JArray(); - foreach (KeyValuePair request in handler.Requests.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)) - { - Type type = request.Value; - DescriptionAttribute? descAttr = type.GetCustomAttribute(); - string description = descAttr?.Description ?? "No description provided."; - - JArray parameters = new JArray(); - foreach (PropertyInfo prop in type.GetProperties()) - { - parameters.Add(new JObject - { - ["name"] = prop.Name, - ["type"] = prop.PropertyType.Name, - ["required"] = prop.GetCustomAttribute() != null - }); - } - - // Build JObject for this request - JObject summary = new JObject - { - ["name"] = request.Key, - ["description"] = description, - ["parameters"] = parameters - }; - - jsonArray.Add(summary); - } - - return Task.FromResult(Json(jsonArray)); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Requests/IRequest.cs b/Parallel.Service/Requests/IRequest.cs deleted file mode 100644 index 88e6286..0000000 --- a/Parallel.Service/Requests/IRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Net; -using Parallel.Core.Net.Sockets; -using Parallel.Service.Responses; - -namespace Parallel.Service.Requests -{ - /// - /// Defines the request class. - /// - public interface IRequest : IDisposable - { - /// - /// Executes a request and responds with an . - /// - Task ExecuteAsync(); - } -} \ No newline at end of file diff --git a/Parallel.Service/Requests/PingRequest.cs b/Parallel.Service/Requests/PingRequest.cs deleted file mode 100644 index ea918b6..0000000 --- a/Parallel.Service/Requests/PingRequest.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Service.Responses; - -namespace Parallel.Service.Requests -{ - public class PingRequest : BaseRequest - { - public override Task ExecuteAsync() - { - return Task.FromResult(Ok()); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Responses/ErrorResponse.cs b/Parallel.Service/Responses/ErrorResponse.cs deleted file mode 100644 index c726119..0000000 --- a/Parallel.Service/Responses/ErrorResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Responses -{ - public class ErrorResponse : IResponse - { - public int Status { get; } - public string Error { get; } - - public ErrorResponse(Exception exception, int status) - { - Status = status; - Error = $"{exception.GetType().FullName}: {exception.Message}"; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Responses/IResponse.cs b/Parallel.Service/Responses/IResponse.cs deleted file mode 100644 index f48de5e..0000000 --- a/Parallel.Service/Responses/IResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Responses -{ - public interface IResponse - { - int Status { get; } - } -} \ No newline at end of file diff --git a/Parallel.Service/Responses/MessageResponse.cs b/Parallel.Service/Responses/MessageResponse.cs deleted file mode 100644 index f03a76f..0000000 --- a/Parallel.Service/Responses/MessageResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Responses -{ - public class MessageResponse : IResponse - { - public int Status { get; } - public string Message { get; } - - public MessageResponse(string message, int status) - { - Message = message; - Status = status; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Responses/ObjectResponse.cs b/Parallel.Service/Responses/ObjectResponse.cs deleted file mode 100644 index 1e3577b..0000000 --- a/Parallel.Service/Responses/ObjectResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -namespace Parallel.Service.Responses -{ - public sealed class ObjectResponse : IResponse - { - public int Status { get; } - public object? Data { get; } - - public ObjectResponse(object? data, int status) - { - Status = status; - Data = data; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Services/FileBackupService.cs b/Parallel.Service/Services/FileBackupService.cs deleted file mode 100644 index 2c91ca5..0000000 --- a/Parallel.Service/Services/FileBackupService.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; - -namespace Parallel.Service.Services -{ - public class FileBackupService : BackgroundService - { - protected override Task ExecuteAsync(CancellationToken stoppingToken) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Services/FileCleanupService.cs b/Parallel.Service/Services/FileCleanupService.cs deleted file mode 100644 index e1abcc1..0000000 --- a/Parallel.Service/Services/FileCleanupService.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; - -namespace Parallel.Service.Services -{ - public class FileCleanupService : BackgroundService - { - protected override Task ExecuteAsync(CancellationToken stoppingToken) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Services/LoggingService.cs b/Parallel.Service/Services/LoggingService.cs deleted file mode 100644 index 2e1c644..0000000 --- a/Parallel.Service/Services/LoggingService.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Microsoft.Extensions.Hosting; -using Parallel.Core.IO; - -namespace Parallel.Service.Services -{ - public class LoggingService : BackgroundService - { - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(GetTimeUntilNextDay(), stoppingToken); - await Log.CloseAndFlushAsync(); - - File.Move(Program.LogFile, Path.Combine(PathBuilder.ProgramData, "Logs", $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log")); - } - } - - private static TimeSpan GetTimeUntilNextDay() - { - DateTime current = DateTime.Now; - DateTime nextMidnight = current.AddDays(1); - return nextMidnight - current; - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Services/TcpRequestService.cs b/Parallel.Service/Services/TcpRequestService.cs deleted file mode 100644 index 7b45152..0000000 --- a/Parallel.Service/Services/TcpRequestService.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using System.Net; -using System.Net.Sockets; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Parallel.Core.Net; -using Parallel.Core.Net.Sockets; -using Parallel.Core.Settings; -using Parallel.Service.Requests; -using Parallel.Service.Responses; - -namespace Parallel.Service.Services -{ - public class TcpRequestService : BackgroundService - { - // Privates - private readonly CancellationTokenSource _exit = new(); - private readonly ILogger _logger; - private readonly Socket _listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - private readonly ParallelSettings _settings; - private readonly RequestHandler _requests; - private readonly List _requestPool = new List(); - - public TcpRequestService(ILogger logger, ParallelSettings settings, RequestHandler requests) - { - _logger = logger; - _settings = settings; - _requests = requests; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - // Starts listening for requests over the TCP socket. - IPAddress address = string.IsNullOrEmpty(_settings.Address) ? IPAddress.Any : IPAddress.Parse(_settings.Address); - _listener.Bind(new IPEndPoint(address, _settings.ListenerPort)); - _listener.Listen(5); - } - catch - { - _logger.LogError("Failed to start server! This usually means either the port is already in use or another instance of Parallel is currently running."); - Environment.Exit(1); - } - - // Starts listening for connections - _logger.LogInformation($"Listening for commands on: {_listener.LocalEndPoint}"); - while (!stoppingToken.IsCancellationRequested && !_exit.IsCancellationRequested) - { - _requestPool.RemoveAll(c => c.IsCompleted); - Socket requestSocket = await _listener.AcceptAsync(_exit.Token); - StartHandlingRequests(requestSocket, stoppingToken); - } - } - - private void StartHandlingRequests(Socket socket, CancellationToken token) - { - TcpSocketHandler handler = new(socket); - Task handleTask = AcceptRequestAsync(handler); - Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), token); - - Task wrappedTask = Task.Run(async () => - { - Task completed = await Task.WhenAny(handleTask, timeoutTask); - IResponse response; - - if (completed == handleTask) - { - try - { - response = await handleTask; - } - catch (OperationCanceledException) - { - _logger.LogInformation($"[{handler.RemoteEndPoint}]: Request cancelled."); - response = new MessageResponse("Request cancelled", 503); - } - catch (Exception ex) - { - _logger.LogError(ex, $"[{handler.RemoteEndPoint}]: Handler failed."); - response = new ErrorResponse(ex, 500); - } - } - else - { - _logger.LogWarning($"[{handler.RemoteEndPoint}]: Timed out after 30 seconds."); - response = new MessageResponse("Request timed out", 408); - } - - await handler.RespondAsync(response); - handler.Close(); - }, token); - - _requestPool.Add(wrappedTask); - } - - private async Task AcceptRequestAsync(ISocketHandler handler) - { - ServerRequest? request = handler.Parse(); - if (request == null) return new MessageResponse("Unable to parse request", 401); - - Log.Debug($"Received request '{handler.RawData}' from '{handler.RemoteEndPoint}' ({_requestPool.Count} active request{(_requestPool.Count == 1 ? string.Empty : "s")})"); - IRequest? requestInstance = _requests.CreateNew(request); - if (requestInstance == null) return new MessageResponse("Required fields are missing", 401); - return await requestInstance.ExecuteAsync(); - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - // Stops listening for requests. - await _exit.CancelAsync(); - - // Checks if any requests are still being processed. - _requestPool.RemoveAll(c => c.IsCompleted); - if (_requestPool.Count > 0) _logger.LogInformation($"Shutdown received. Still processing {_requestPool.Count} request{(_requestPool.Count == 1 ? string.Empty : "s")}!"); - await Task.WhenAll(_requestPool); - - return base.StopAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/Utils/UdpReporting.cs b/Parallel.Service/Utils/UdpReporting.cs deleted file mode 100644 index 3d10d00..0000000 --- a/Parallel.Service/Utils/UdpReporting.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 Kyle Ebbinga - -using Parallel.Core.Diagnostics; -using Parallel.Core.Models; -using Parallel.Core.Net; - -namespace Parallel.Service.Utils -{ - public class UdpReporting : IProgressReporter - { - private readonly Communication _comms = new Communication(); - - public void Report(ProgressOperation operation, SystemFile file, int current, int total) - { - int percent = current * 100 / total; - //_comms.Send($"[{percent}%] {operation}: {file.LocalPath}"); - } - - public void Failed(Exception exception, SystemFile file) - { - //_comms.Send($"Failed to upload file: '{file.LocalPath}'"); - } - } -} \ No newline at end of file diff --git a/Parallel.Service/parallel-red.ico b/Parallel.Service/parallel-red.ico deleted file mode 100644 index 27a395d..0000000 Binary files a/Parallel.Service/parallel-red.ico and /dev/null differ diff --git a/Parallel.sln b/Parallel.sln index f5b080f..329e4be 100644 --- a/Parallel.sln +++ b/Parallel.sln @@ -7,10 +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.Core.Net", "Parallel.Core.Net\Parallel.Core.Net.csproj", "{157A0A8F-A393-4577-AD3B-DF5FB49A7331}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Parallel.Service", "Parallel.Service\Parallel.Service.csproj", "{DE6A8D71-E2A1-4BB5-9BCD-13839D3100AC}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,14 +21,6 @@ Global {4BFE65E9-9534-4C85-B59F-1F64A998C76D}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BFE65E9-9534-4C85-B59F-1F64A998C76D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4BFE65E9-9534-4C85-B59F-1F64A998C76D}.Release|Any CPU.Build.0 = Release|Any CPU - {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Debug|Any CPU.Build.0 = Debug|Any CPU - {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Release|Any CPU.ActiveCfg = Release|Any CPU - {157A0A8F-A393-4577-AD3B-DF5FB49A7331}.Release|Any CPU.Build.0 = Release|Any CPU - {DE6A8D71-E2A1-4BB5-9BCD-13839D3100AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE6A8D71-E2A1-4BB5-9BCD-13839D3100AC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE6A8D71-E2A1-4BB5-9BCD-13839D3100AC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE6A8D71-E2A1-4BB5-9BCD-13839D3100AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index e3fc26d..33b571b 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,30 @@ Your computer already gives you enough to fight with — your files don't have t -## 📦 Installation - -> Coming soon — Parallel is currently in active development. Stay tuned for install instructions, binaries, and package manager support. +## 📦 Quick Start Guide +#### 1. Install Parallel +Download the latest [release](https://github.com/TheGuitarleader/Parallel/releases/latest) or build from source: +``` +git clone https://github.com/TheGuitarleader/Parallel +cd Parallel +dotnet build +``` +#### 2. Set Up Your Vaults +Vaults are storage targets where Parallel sends and recieves files. This can be an external drive, NAS share, SSH server, or S3-compatible cloud. +``` +parallel vaults create +``` +*Note: All vaults are saved as JSON in `%AppData%\Parallel\Vaults` for easy importing and exporting.* +#### 3. Push Files to Vaults +Parallel can push all changed files on the system with: +``` +parallel push +``` +Or you can specify a path which can be a file or folder. +``` +parallel push --path "C:\Windows\System32" +parallel push -p "C:\Windows\System32\cmd.exe" +``` ## 🧪 Status