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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*.userosscache
*.sln.docstates
*.sln
*.lnk

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
Expand Down
15 changes: 8 additions & 7 deletions Parallel.Cli/Commands/DecryptCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,7 +15,7 @@ namespace Parallel.Cli.Commands
public class DecryptCommand : Command
{
private readonly Argument<string> _sourceArg = new("path", "The source path of files to zip.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The profile configuration to use.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");

private IDatabase? _database;
private Stopwatch _sw = new Stopwatch();
Expand All @@ -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);
Expand All @@ -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);
Expand Down
23 changes: 2 additions & 21 deletions Parallel.Cli/Commands/DuplicatesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,15 @@ namespace Parallel.Cli.Commands
public class DuplicatesCommand : Command
{
private Argument<string> sourceArg = new("path", "The directory to scan.");
private Option<string> 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<string, SystemFile[]> duplicates = FileScanner.GetDuplicateFiles(path);
Dictionary<string, string[]> result = duplicates.ToDictionary(k => k.Key, v => v.Value.Select(l => l.LocalPath).ToArray());
Expand Down
19 changes: 10 additions & 9 deletions Parallel.Cli/Commands/EncryptCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,7 +16,7 @@ namespace Parallel.Cli.Commands
public class EncryptCommand : Command
{
private readonly Argument<string> _sourceArg = new("path", "The source path to encrypt.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The profile configuration to use.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");

private IDatabase? _database;
private Stopwatch _sw = new Stopwatch();
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
139 changes: 139 additions & 0 deletions Parallel.Cli/Commands/HistoryCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string> _sourceOpt = new(["--path", "-p"], "The source path.");
private Option<string> _vaultOpt = new(["--vault", "-v"], "The vault to use.");
private Option<int> _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);
}
}
}
}
9 changes: 9 additions & 0 deletions Parallel.Cli/Commands/PullCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2025 Kyle Ebbinga

namespace Parallel.Cli.Commands
{
public class PullCommand
{

}
}
94 changes: 94 additions & 0 deletions Parallel.Cli/Commands/PushCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string> _sourceArg = new(["--path", "-p"], "The source path to backup.");
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");
private readonly Option<bool> _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);
});
}
}
}
Loading
Loading