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
13 changes: 13 additions & 0 deletions Build-Release.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@echo off

set SCRIPT_DIR=%~dp0
set BUILDS_DIR="%SCRIPT_DIR%Builds"

echo Building to folder: %BUILDS_DIR%
rd /s /q "%BUILDS_DIR%"

echo Building Parallel.Cli...
CALL dotnet publish "%SCRIPT_DIR%Parallel.Cli\Parallel.Cli.csproj" -r win-x64 -c Release /p:PublishSingleFile=true /p:PublishDir="%BUILDS_DIR%/win-x64"
CALL dotnet publish "%SCRIPT_DIR%Parallel.Cli\Parallel.Cli.csproj" -r osx-x64 -c Release /p:PublishSingleFile=true /p:PublishDir="%BUILDS_DIR%/osx-x64"
CALL dotnet publish "%SCRIPT_DIR%Parallel.Cli\Parallel.Cli.csproj" -r linux-x64 -c Release /p:PublishSingleFile=true /p:PublishDir="%BUILDS_DIR%/linux-x64"
CALL dotnet publish "%SCRIPT_DIR%Parallel.Cli\Parallel.Cli.csproj" -r linux-arm -c Release /p:PublishSingleFile=true /p:PublishDir="%BUILDS_DIR%/linux-arm"
75 changes: 75 additions & 0 deletions Parallel.Cli/Commands/DiskCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2025 Kyle Ebbinga

using System.CommandLine;
using System.Data;
using Newtonsoft.Json.Linq;
using Parallel.Cli.Utils;
using Parallel.Core.Database;
using Parallel.Core.IO.Backup;
using Parallel.Core.IO.FileSystem;
using Parallel.Core.IO.Syncing;
using Parallel.Core.Settings;
using Parallel.Core.Utils;

namespace Parallel.Cli.Commands
{
public class DiskCommand : Command
{
private readonly Argument<string> configArg = new("config", "The vault configuration to use.");

public DiskCommand() : base("disk", "Shows the current disk usage.")
{
this.AddArgument(configArg);
this.SetHandler(async (vault) =>
{
CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray);
LocalVaultConfig? config = ParallelConfig.GetVault(vault);
if (config == null)
{
CommandLine.WriteLine($"Unable to find vault with name: '{vault}'", ConsoleColor.Yellow);
return;
}

await DisplayDiskInformationAsync(config);
}, configArg);
}

private async Task DisplayDiskInformationAsync(LocalVaultConfig vault)
{
ISyncManager syncManager = new FileSyncManager(vault);
if (!await syncManager.ConnectAsync())
{
CommandLine.WriteLine(vault, $"Failed to connect to vault '{vault.Name}'!", ConsoleColor.Red);
return;
}

IDatabase db = syncManager.Database;
long localSize = await db.GetLocalSizeAsync();
long remoteSize = await db.GetRemoteSizeAsync();
long totalLocalFiles = await db.GetTotalFilesAsync(false);
long totalDeletedFiles = await db.GetTotalFilesAsync(true);

CommandLine.WriteLine($"Using vault '{vault.Name}' ({vault.Id}):");
CommandLine.WriteLine($"Service Type: {vault.FileSystem.Service}");
CommandLine.WriteLine($"Root Directory: {vault.FileSystem.RootDirectory}");
CommandLine.WriteLine($"Managed Files: {(totalLocalFiles + totalDeletedFiles):N0}");
CommandLine.WriteLine($"Local Files: {totalLocalFiles:N0}");
CommandLine.WriteLine($"Deleted Files: {totalDeletedFiles:N0}");
CommandLine.WriteLine($"Local Size: {Formatter.FromBytes(localSize)}");
CommandLine.WriteLine($"Remote Size: {Formatter.FromBytes(remoteSize)}");
CommandLine.WriteLine($"Space Saved: {Math.Round((localSize - remoteSize) / (double)localSize * 100, 1)}%");

if (vault.FileSystem.Service.Equals(FileService.Local))
{
DriveInfo drive = new(vault.FileSystem.RootDirectory);
long diskUsage = drive.TotalSize - drive.TotalFreeSpace;
CommandLine.WriteLine($"Total Usage: {Formatter.FromBytes(diskUsage)} ({Math.Round(diskUsage / (double)drive.TotalSize * 100, 1)}%)");
CommandLine.WriteLine($"Disk Usage: {Formatter.FromBytes(diskUsage - remoteSize)} ({Math.Round((diskUsage - remoteSize) / (double)drive.TotalSize * 100, 1)}%)");
CommandLine.WriteLine($"Disk Free: {Formatter.FromBytes(drive.TotalFreeSpace)} ({Math.Round(drive.TotalFreeSpace / (double)drive.TotalSize * 100, 1)}%)");
CommandLine.WriteLine($"Disk Total: {Formatter.FromBytes(drive.TotalSize)}");
}

await syncManager.DisconnectAsync();
}
}
}
12 changes: 6 additions & 6 deletions Parallel.Cli/Commands/PushCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ 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 Command addCmd = new("add", "Adds a new directory to the sync list.");
private Command listCmd = new("list", "Shows all directories in the sync list.");
private Command removeCmd = new("remove", "Removes a directory from the sync list.");

private readonly Option<string> _sourceArg = new(["--path", "-p"], "The source path to backup.");
private readonly Option<string> _sourceArg = new(["--path", "-p"], "The source path to sync.");
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.")
public PushCommand() : base("push", "Pushes changed files to one vault or multiple.")
{
this.AddOption(_sourceArg);
this.AddOption(_configOpt);
Expand Down Expand Up @@ -50,7 +50,7 @@ private async Task SyncPathAsync(string path)
{
await Program.Settings.ForEachVaultAsync(async vault =>
{
FileSyncManager syncManager = new FileSyncManager(vault);
ISyncManager syncManager = new FileSyncManager(vault);
if (!await syncManager.ConnectAsync())
{
CommandLine.WriteLine(vault, $"Failed to connect to vault '{vault.Name}'!", ConsoleColor.Red);
Expand Down
19 changes: 19 additions & 0 deletions Parallel.Cli/Commands/RestoreCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2025 Kyle Ebbinga

using System.CommandLine;

namespace Parallel.Cli.Commands
{
public class RestoreCommand : Command
{
private Command createCmd = new("create", "Creates a new recovery point current of the system state.");
private Command listCmd = new("list", "Lists all available recovery points.");
private Command restoreCmd = new("restore", "Restores the system state from a previous recovery point.");
private Option<string> credsOpt = new(["--credentials", "-c"], "The file system credentials to use.");

public RestoreCommand() : base("restore", "Creates or loads a system restore point.")
{

}
}
}
38 changes: 35 additions & 3 deletions Parallel.Cli/Commands/VaultsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.CommandLine;
using Parallel.Cli.Utils;
using Parallel.Core.Database;
using Parallel.Core.IO.Backup;
using Parallel.Core.IO.FileSystem;
using Parallel.Core.IO.Syncing;
using Parallel.Core.Security;
using Parallel.Core.Settings;
using Parallel.Core.Utils;
Expand All @@ -12,7 +14,8 @@ namespace Parallel.Cli.Commands
{
public class VaultsCommand : Command
{
private Option<string> configOpt = new(["--config", "-c"], "The vault configuration to use.");
private readonly Argument<string> configArg = new("config", "The vault configuration to use.");
private readonly Option<string> configOpt = new(["--config", "-c"], "The vault configuration to use.");

private Command addCmd = new("add", "Adds a new vault configuration.");
private Command editCmd = new("edit", "Edits a vault configuration.");
Expand Down Expand Up @@ -56,8 +59,8 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.")
fsc.Password = CommandLine.ReadPassword("Password");
}

fsc.Encrypt = CommandLine.ReadBool("Encrypt files? (y/n)", false);
fsc.EncryptionKey = HashGenerator.GenerateHash(32, true);
//fsc.Encrypt = CommandLine.ReadBool("Encrypt files? (y/n)", false);
//fsc.EncryptionKey = HashGenerator.GenerateHash(32, true);

string? profileName = CommandLine.ReadString("Profile Name");
LocalVaultConfig localVault = new LocalVaultConfig(profileName, fsc);
Expand All @@ -67,6 +70,35 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.")
CommandLine.WriteLine($"Saved new storage vault: '{localVault.Name}' ({localVault.Id})");
});

this.AddCommand(viewCmd);
viewCmd.AddArgument(configArg);
viewCmd.SetHandler(async (vault) =>
{
CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray);
LocalVaultConfig? config = ParallelConfig.GetVault(vault);
if (config == null)
{
CommandLine.WriteLine($"Unable to find vault with name: '{vault}'", ConsoleColor.Yellow);
return;
}

ISyncManager syncManager = new FileSyncManager(config);
if (!await syncManager.ConnectAsync())
{
CommandLine.WriteLine(config, $"Failed to connect to vault '{config.Name}'!", ConsoleColor.Red);
return;
}

RemoteVaultConfig remoteVault = syncManager.RemoteVault;
CommandLine.WriteLine($"'{remoteVault.Name}' ({remoteVault.Id}):");
CommandLine.WriteArray($"Backup Directories", remoteVault.BackupDirectories);
CommandLine.WriteArray($"Ignore Directories", remoteVault.IgnoreDirectories);
CommandLine.WriteArray($"Prune Directories", remoteVault.PruneDirectories);
CommandLine.WriteLine($"Prune Period: {remoteVault.PrunePeriod} days");

await syncManager.DisconnectAsync();
}, configArg);

this.AddCommand(setCmd);
setCmd.SetHandler(() =>
{
Expand Down
20 changes: 20 additions & 0 deletions Parallel.Cli/Utils/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,25 @@ public static void ProgressBar(double part, double total, TimeSpan elapsed, Cons

Console.Write($"\r{percentStr} [{progressBar.ToString()}] {remainingStr}");
}

public static void WriteArray(string message, IEnumerable<string> elements)
{
lock (_consoleLock)
{
string[] array = elements.Order().ToArray();
if (array.Length > 0)
{
Console.WriteLine($"> {message}:");
foreach (string item in array)
{
Console.WriteLine($"> - {item}");
}
}
else
{
Console.WriteLine($"> {message}: 0");
}
}
}
}
}
45 changes: 30 additions & 15 deletions Parallel.Core/Database/Contexts/SqliteContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,31 +56,46 @@ public async Task InitializeAsync()
/// <inheritdoc />
public async Task<bool> AddFileAsync(SystemFile file)
{
using (IDbConnection connection = CreateConnection())
{
string sql = @"INSERT OR REPLACE INTO files (id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, checksum) VALUES (@Id, @Name, @LocalPath, @RemotePath, @LastWrite, @LastUpdate, @LocalSize, @RemoteSize, @Type, @Hidden, @ReadOnly, @Deleted, @CheckSum);";
return await connection.ExecuteAsync(sql, new { 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.CheckSum }) > 0;
}
using IDbConnection connection = CreateConnection();
string sql = @"INSERT OR REPLACE INTO files (id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, checksum) VALUES (@Id, @Name, @LocalPath, @RemotePath, @LastWrite, @LastUpdate, @LocalSize, @RemoteSize, @Type, @Hidden, @ReadOnly, @Deleted, @CheckSum);";
return await connection.ExecuteAsync(sql, new { 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.CheckSum }) > 0;
}

public async Task<long> GetLocalSizeAsync()
{
using IDbConnection connection = CreateConnection();
string sql = $"SELECT SUM(localsize) FROM files;";
return await connection.QuerySingleOrDefaultAsync<long>(sql);
}

public async Task<long> GetRemoteSizeAsync()
{
using IDbConnection connection = CreateConnection();
string sql = $"SELECT SUM(remotesize) FROM files;";
return await connection.QuerySingleOrDefaultAsync<long>(sql);
}

public async Task<long> GetTotalFilesAsync(bool deleted)
{
using IDbConnection connection = CreateConnection();
string sql = $"SELECT COUNT(*) FROM files WHERE deleted = @deleted;";
return await connection.QuerySingleOrDefaultAsync<long>(sql, new { deleted });
}

/// <inheritdoc />
public async Task<IEnumerable<SystemFile>> GetFilesAsync(string path, bool deleted)
{
using (IDbConnection connection = CreateConnection())
{
string sql = $"SELECT * FROM files WHERE deleted = {deleted} ORDER BY lastupdate DESC";
return await connection.QueryAsync<SystemFile>(sql);
}
using IDbConnection connection = CreateConnection();
string sql = $"SELECT * FROM files WHERE deleted = {deleted} ORDER BY lastupdate DESC";
return await connection.QueryAsync<SystemFile>(sql);
}

/// <inheritdoc />
public async Task<SystemFile?> GetFileAsync(string path)
{
using (IDbConnection connection = CreateConnection())
{
string sql = $"SELECT (id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, checksum) FROM files WHERE localpath LIKE \"%{path}%\" OR remotepath LIKE \"%{path}%\" ORDER BY lastupdate DESC";
return await connection.QuerySingleOrDefaultAsync<SystemFile>(sql);
}
using IDbConnection connection = CreateConnection();
string sql = $"SELECT (id, name, localpath, remotepath, lastwrite, lastupdate, LocalSize, RemoteSize, type, hidden, readonly, deleted, checksum) FROM files WHERE localpath LIKE \"%{path}%\" OR remotepath LIKE \"%{path}%\" ORDER BY lastupdate DESC";
return await connection.QuerySingleOrDefaultAsync<SystemFile>(sql);
}

#endregion
Expand Down
4 changes: 4 additions & 0 deletions Parallel.Core/Database/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public interface IDatabase : IDisposable
/// <returns>True if successful, false otherwise</returns>
Task<bool> AddFileAsync(SystemFile file);

Task<long> GetLocalSizeAsync();
Task<long> GetRemoteSizeAsync();
Task<long> GetTotalFilesAsync(bool deleted);

#endregion

#region History
Expand Down
4 changes: 2 additions & 2 deletions Parallel.Core/IO/Scanning/FileScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public static Dictionary<string, SystemFile[]> GetDuplicateFiles(string path)
{
Dictionary<string, List<SystemFile>> dict = new();
IEnumerable<string> files = GetFiles(path, "*");
foreach (string file in files)
System.Threading.Tasks.Parallel.ForEach(files, ParallelConfig.Options, file =>
{
SystemFile entry = new(file);
if (dict.TryGetValue(entry.Name, out List<SystemFile> value))
Expand All @@ -293,7 +293,7 @@ public static Dictionary<string, SystemFile[]> GetDuplicateFiles(string path)
{
dict.Add(entry.Name, new List<SystemFile> { entry });
}
}
});

return dict.Where(kv => kv.Value.Count > 1).OrderByDescending(kv => kv.Value.Count).ToDictionary(k => k.Key, v => v.Value.OrderBy(l => l.LastWrite.TotalMilliseconds).ToArray());
}
Expand Down
10 changes: 10 additions & 0 deletions Parallel.Core/Settings/ParallelConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,15 @@ await System.Threading.Tasks.Parallel.ForEachAsync(Vaults, options, async (vault
await actionAsync(vault);
});
}

/// <summary>
/// Gets a <see cref="LocalVaultConfig"/> by either its id or name.
/// </summary>
/// <param name="vault"></param>
/// <returns></returns>
public static LocalVaultConfig? GetVault(string vault)
{
return Load().Vaults.FirstOrDefault(v => v.Id.Equals(vault, StringComparison.OrdinalIgnoreCase) || v.Name.Equals(vault, StringComparison.OrdinalIgnoreCase));
}
}
}
Loading