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
46 changes: 46 additions & 0 deletions Parallel.Cli/Commands/FetchCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2026 Kyle Ebbinga

using System.CommandLine;
using Parallel.Cli.Utils;
using Parallel.Core.IO.Syncing;
using Parallel.Core.Settings;

namespace Parallel.Cli.Commands
{
public class FetchCommand : Command
{
private readonly Option<string> _configOpt = new(["--config", "-c"], "The vault configuration to use.");

public FetchCommand() : base("fetch", "Fetches information from a vault.")
{
this.AddOption(_configOpt);
this.SetHandler(HandleFetchAsync, _configOpt);
}

private async Task HandleFetchAsync(string config)
{
LocalVaultConfig? localVault = ParallelConfig.GetVault(config);
if (localVault != null)
{
await FetchVaultAsync(localVault);
return;
}

await Program.Settings.ForEachVaultAsync(FetchVaultAsync);
}

private async Task FetchVaultAsync(LocalVaultConfig localVault)
{
CommandLine.WriteLine($"Retrieving vault information...", ConsoleColor.DarkGray);
ISyncManager? syncManager = SyncManager.CreateNew(localVault);
if (syncManager == null || !await syncManager.ConnectAsync(true))
{
CommandLine.WriteLine(localVault, "Failed to connect to vault!", ConsoleColor.Red);
return;
}

CommandLine.WriteLine(syncManager.LocalVault, $"Successfully fetched vault data for: '{localVault.Name}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
}
}
4 changes: 2 additions & 2 deletions Parallel.Cli/Commands/IgnoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private async Task AddPathAsync(LocalVaultConfig vault, string path)
return;
}

CommandLine.WriteLine(vault, $"Successfully added '{path}'", ConsoleColor.Green);
CommandLine.WriteLine(vault, $"Successfully ignored '{path}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}

Expand Down Expand Up @@ -89,7 +89,7 @@ private async Task RemovePathAsync(LocalVaultConfig vault, string path)
return;
}

CommandLine.WriteLine(vault, $"Successfully removed '{path}'", ConsoleColor.Green);
CommandLine.WriteLine(vault, $"Successfully included '{path}'", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
}
Expand Down
13 changes: 11 additions & 2 deletions Parallel.Cli/Commands/PruneCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,16 @@ private async Task PrunePathAsync(LocalVaultConfig vault, string path, DateTime
private async Task PruneInternalAsync(ISyncManager syncManager, string path, DateTime timestamp, bool force, bool dryRun)
{
CommandLine.WriteLine(syncManager.RemoteVault, $"Scanning for files in {path}...", ConsoleColor.DarkGray);
IReadOnlyList<LocalFile> files = await (syncManager.Database?.GetFilesAsync(path, timestamp, true) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
IReadOnlyList<LocalFile> files;
if (force)
{
files = await (syncManager.Database?.GetFilesAsync(path, timestamp) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
}
else
{
files = await (syncManager.Database?.GetFilesAsync(path, timestamp, true) ?? Task.FromResult<IReadOnlyList<LocalFile>>([]));
}

if (files.Count == 0)
{
CommandLine.WriteLine($"No prunable files were found!", ConsoleColor.Yellow);
Expand All @@ -153,7 +162,7 @@ private async Task PruneInternalAsync(ISyncManager syncManager, string path, Dat
else
{
CommandLine.WriteLine(syncManager.RemoteVault, $"Pruning {files.Count:N0} files...", ConsoleColor.DarkGray);
int prunedFiles = await syncManager.PruneFilesAsync(files, new ProgressReport(syncManager.RemoteVault, files.Count));
int prunedFiles = await syncManager.PruneFilesAsync(files, new ProgressReporter(syncManager.RemoteVault, files.Count));
CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully pruned {prunedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
await syncManager.DisconnectAsync();
}
Expand Down
2 changes: 1 addition & 1 deletion Parallel.Cli/Commands/RestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private async Task RestoreInternalAsync(ISyncManager syncManager, string path, D
else
{
CommandLine.WriteLine(syncManager.RemoteVault, $"Restoring {restoreFiles.Count:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReport(syncManager.RemoteVault, restoreFiles.Count) : new NullProgressReporter();
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, restoreFiles.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
int restoredFiles = await syncManager.RestoreFilesAsync(restoreFiles.ToArray(), progressReporter);

CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully restored {restoredFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
Expand Down
2 changes: 1 addition & 1 deletion Parallel.Cli/Commands/ScrubCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private async Task ScrubInternalAsync(ISyncManager syncManager, string path, boo
}

CommandLine.WriteLine(syncManager.RemoteVault, $"Scrubbing {files.Count:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReport(syncManager.RemoteVault, files.Count) : new NullProgressReporter();
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, files.Count) : new LoggingProgressReporter(syncManager.RemoteVault);
int scrubbedFiles = await syncManager.ScrubFilesAsync(files, progressReporter);

CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully scrubbed {scrubbedFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
Expand Down
5 changes: 3 additions & 2 deletions Parallel.Cli/Commands/SnapshotsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.CommandLine;
using System.Diagnostics;
using Parallel.Cli.Utils;
using Parallel.Core.Diagnostics;
using Parallel.Core.IO;
using Parallel.Core.IO.Syncing;
using Parallel.Core.Models;
Expand Down Expand Up @@ -150,8 +151,8 @@ private async Task RestoreSnapshotAsync(LocalVaultConfig vault, string? name, st

CommandLine.WriteLine($"Loading snapshot {snapshotFile}");

string remoteSnapshotFile = PathBuilder.GetSnapshotFile(vault, snapshotFilename);
string localSnapshotFile = Path.Combine(PathBuilder.TempDirectory, snapshotFilename + ".json");
//string remoteSnapshotFile = PathBuilder.GetSnapshotFile(vault, snapshotFilename);
//string localSnapshotFile = Path.Combine(PathBuilder.TempDirectory, snapshotFilename + ".json");
}
}
}
12 changes: 5 additions & 7 deletions Parallel.Cli/Commands/StatsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,22 @@ private async Task DisplayDiskInformationAsync(LocalVaultConfig vault)
}

IDatabase? db = syncManager.Database;
long localSize = await (db?.GetLocalSizeAsync() ?? Task.FromResult(0L));
long remoteSize = await (db?.GetRemoteSizeAsync() ?? Task.FromResult(0L));
long localSize = await (db?.GetCurrentSizeAsync() ?? Task.FromResult(0L));
long totalSize = await (db?.GetTotalSizeAsync() ?? Task.FromResult(0L));
long totalFiles = await (db?.GetTotalFilesAsync() ?? Task.FromResult(0L));
long totalLocalFiles = await (db?.GetTotalFilesAsync(false) ?? Task.FromResult(0L));
long totalDeletedFiles = await (db?.GetTotalFilesAsync(true) ?? Task.FromResult(0L));
double spaceSaved = Math.Round((localSize - remoteSize) / (double)localSize * 100, 2);
long totalRevisedFiles = await (db?.GetTotalRevisedFilesAsync() ?? Task.FromResult(0L));

CommandLine.WriteLine($"Using vault '{syncManager.RemoteVault.Name}' ({vault.Id}):");
CommandLine.WriteLine($"Service Type: {vault.Credentials.Service}");
CommandLine.WriteLine($"Root Directory: {vault.Credentials.RootDirectory}");
CommandLine.WriteLine($"Managed Files: {totalFiles:N0}");
CommandLine.WriteLine($"Local Files: {totalLocalFiles:N0}");
CommandLine.WriteLine($"Deleted Files: {totalDeletedFiles:N0}");
CommandLine.WriteLine($"Revisions: {totalFiles - (totalLocalFiles + totalDeletedFiles):N0}");
CommandLine.WriteLine($"Total Size: {Formatter.FromBytes(localSize)}");
CommandLine.WriteLine($"Local Size: {Formatter.FromBytes(totalSize)}");
CommandLine.WriteLine($"Remote Size: {Formatter.FromBytes(remoteSize)} ({(double.IsNaN(spaceSaved) ? 0 : spaceSaved)}%)");
CommandLine.WriteLine($"Revisions: {totalRevisedFiles:N0}");
CommandLine.WriteLine($"Local Size: {Formatter.FromBytes(localSize)}");
CommandLine.WriteLine($"Remote Size: {Formatter.FromBytes(totalSize)}");

if (vault.Credentials.Service.Equals(FileService.Local))
{
Expand Down
4 changes: 2 additions & 2 deletions Parallel.Cli/Commands/SyncCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private async Task SyncInternalAsync(ISyncManager syncManager, string path, bool

if (!backupFolders.Any(dir => path.StartsWith(dir, StringComparison.OrdinalIgnoreCase)) || FileScanner.IsIgnored(path, ignoredFolders))
{
CommandLine.WriteLine(syncManager.RemoteVault, $"The provided {(isFile ? "file" : "folder")} is set to be ignored!", ConsoleColor.Yellow);
CommandLine.WriteLine(syncManager.RemoteVault, $"The provided {(isFile ? "file" : "folder")} is not set to be synced!", ConsoleColor.Yellow);
return;
}

Expand All @@ -147,7 +147,7 @@ private async Task SyncInternalAsync(ISyncManager syncManager, string path, bool
}

CommandLine.WriteLine(syncManager.RemoteVault, $"Syncing {files.Length:N0} files...", ConsoleColor.DarkGray);
IProgressReporter progressReporter = verbose ? new ProgressReport(syncManager.RemoteVault, successFiles) : new NullProgressReporter();
IProgressReporter progressReporter = verbose ? new ProgressReporter(syncManager.RemoteVault, successFiles) : new LoggingProgressReporter(syncManager.RemoteVault);
int backedUpFiles = await syncManager.BackupFilesAsync(files, progressReporter, force);

CommandLine.WriteLine(syncManager.RemoteVault, $"Successfully synced {backedUpFiles:N0} files in {_sw.Elapsed}.", ConsoleColor.Green);
Expand Down
7 changes: 4 additions & 3 deletions Parallel.Cli/Commands/VaultsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.")
string? bucketInput = CommandLine.ReadString("Bucket Name (Leave empty for default)");
string bucketName = string.IsNullOrEmpty(bucketInput) ? "parallel" : bucketInput;
spc.RootDirectory = bucketName;

string? regionInput = CommandLine.ReadString("Region Name (Leave empty for default)");
string regionName = string.IsNullOrEmpty(regionInput) ? "us-east-1" : regionInput;
spc.Region = regionName;

spc.Address = CommandLine.ReadString("Endpoint");
spc.Username = CommandLine.ReadString("Access Key");
Expand All @@ -73,9 +77,6 @@ public VaultsCommand() : base("vaults", "View or edit the vaults.")
string? inputName = CommandLine.ReadString("Name (Leave empty for machine name)");
string profileName = string.IsNullOrEmpty(inputName) ? Environment.MachineName : inputName;

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

LocalVaultConfig localVault = new(profileId, profileName, spc);
localVault.Enabled = CommandLine.ReadBool("Enabled? (y/n)", true);
Program.Settings.Vaults.Add(localVault);
Expand Down
1 change: 1 addition & 0 deletions Parallel.Cli/Parallel.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0"/>
<PackageReference Include="Spectre.Console" Version="0.55.2" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup>

Expand Down
11 changes: 2 additions & 9 deletions Parallel.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public static async Task Main(string[] args)
Settings = ParallelConfig.Load();
AssemblyName assembly = Assembly.GetExecutingAssembly().GetName();
#if DEBUG
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Sink(EventTracker).WriteTo.Console().CreateLogger();
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
#else
Log.Logger = new LoggerConfiguration().WriteTo.Sink(EventTracker).CreateLogger();
Log.Logger = new LoggerConfiguration().WriteTo.File(PathBuilder.LogFile).CreateLogger();
#endif

Log.Information($"{assembly.Name} [Version {assembly.Version}]");
Expand All @@ -51,13 +51,6 @@ public static async Task Main(string[] args)
{
// Clean successful logs
await Log.CloseAndFlushAsync();
if (EventTracker.ErrorCount > 0)
{
string logDir = Path.Combine(PathBuilder.TempDirectory, "Logs");
if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir);
await File.WriteAllLinesAsync(Path.Combine(logDir, $"{DateTime.Now:MM-dd-yyyy hh-mm-ss}.log"), EventTracker.Logs.Reverse());
}

Settings.Save();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

namespace Parallel.Cli.Utils
{
public class ProgressReport : IProgressReporter
public class ProgressReporter : IProgressReporter
{
private Stopwatch _sw;
private readonly LocalVaultConfig _localVault;
private int _current;
private readonly int _total;

public ProgressReport(LocalVaultConfig localVault, int totalFiles)
public ProgressReporter(LocalVaultConfig localVault, int totalFiles)
{
_sw = Stopwatch.StartNew();
_localVault = localVault;
Expand All @@ -25,8 +25,8 @@ public ProgressReport(LocalVaultConfig localVault, int totalFiles)
public void Report(ProgressOperation operation, LocalFile file)
{
Interlocked.Increment(ref _current);
int percent = _current * 100 / _total;
CommandLine.WriteLine($"[{_localVault.Id}] ({percent}%) {operation}: {file.Fullname}");
double percent = _current * 100.0 / _total;
CommandLine.WriteLine($"[{_localVault.Id}] ({percent:N1}%) {operation}: {file.Fullname}");
}

/// <inheritdoc />
Expand Down
15 changes: 13 additions & 2 deletions Parallel.Core/Database/Contexts/SqliteContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<bool> RemoveFileAsync(LocalFile file)
}

/// <inheritdoc />
public async Task<long> GetLocalSizeAsync()
public async Task<long> GetCurrentSizeAsync()
{
string sql = "SELECT COALESCE(SUM(f.localsize), 0) FROM objects f JOIN (SELECT fullname, MAX(lastupdate) AS max_lastupdate FROM objects WHERE deleted = 0 GROUP BY fullname) latest ON f.fullname = latest.fullname AND f.lastupdate = latest.max_lastupdate;";
return await _semaphore.QuerySingleAsync<long>(sql);
Expand Down Expand Up @@ -87,6 +87,12 @@ public async Task<long> GetTotalFilesAsync(bool deleted)
return await _semaphore.QuerySingleAsync<long>(sql, new { deleted });
}

public async Task<long> GetTotalRevisedFilesAsync()
{
string sql = $"SELECT COUNT(*) FROM objects WHERE lastupdate NOT IN (SELECT MAX(lastupdate) FROM objects GROUP BY fullname);";
return await _semaphore.QuerySingleAsync<long>(sql);
}

/// <inheritdoc />
public async Task<IReadOnlyList<LocalFile>> GetLatestFilesAsync(string path, DateTime timestamp)
{
Expand All @@ -97,11 +103,16 @@ public async Task<IReadOnlyList<LocalFile>> GetLatestFilesAsync(string path, Dat
/// <inheritdoc />
public async Task<IReadOnlyList<LocalFile>> GetLatestFilesAsync(string path, DateTime timestamp, bool deleted)
{
Log.Debug($"SELECT * FROM (SELECT * FROM objects WHERE fullname LIKE '{path}%' AND lastupdate <= {new UnixTime(timestamp).TotalMilliseconds} AND deleted = {deleted} ORDER BY lastwrite DESC) GROUP BY fullname;");
string sql = "SELECT * FROM (SELECT * FROM objects WHERE fullname LIKE @Path AND lastupdate <= @Time AND deleted = @deleted ORDER BY lastwrite DESC) GROUP BY fullname;";
return await _semaphore.QueryAsync<LocalFile>(sql, new { Path = $"{path}%", Time = new UnixTime(timestamp).TotalMilliseconds, deleted });
}

public async Task<IReadOnlyList<LocalFile>> GetRevisedFilesAsync(string path)
{
string sql = $"SELECT * FROM objects WHERE fullname LIKE '{path}%' AND lastupdate NOT IN (SELECT MAX(lastupdate) FROM objects GROUP BY fullname) ORDER BY lastupdate DESC;";
return await _semaphore.QueryAsync<LocalFile>(sql, new { Path = $"{path}%" });
}

/// <inheritdoc />
public async Task<IReadOnlyList<LocalFile>> GetFilesAsync(string path, DateTime timestamp)
{
Expand Down
3 changes: 2 additions & 1 deletion Parallel.Core/Database/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,12 @@ public interface IDatabase
/// <returns></returns>
Task<LocalFile?> GetFileAsync(string path);

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

/// <summary>
/// Gets a list of directories in ascending order.
Expand Down
5 changes: 0 additions & 5 deletions Parallel.Core/Diagnostics/IProgressReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ public interface IProgressReporter
/// </summary>
void Report(ProgressOperation operation, LocalFile file);

/// <summary>
/// Resets the ticking.
/// </summary>
void Reset();

/// <summary>
/// Reports a failed update.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
// Copyright 2026 Kyle Ebbinga

using System.Diagnostics;
using Parallel.Core.Models;
using Parallel.Core.Settings;

namespace Parallel.Core.Diagnostics
{
/// <summary>
/// Represents a null <see cref="IProgressReporter"/>.
/// </summary>
public class NullProgressReporter : IProgressReporter
public class LoggingProgressReporter : IProgressReporter
{
private readonly LocalVaultConfig _localVault;

public LoggingProgressReporter(LocalVaultConfig localVault)
{
_localVault = localVault;
}

/// <inheritdoc />
public void Report(ProgressOperation operation, LocalFile file) { }

/// <inheritdoc />
public void Reset() { }
public void Report(ProgressOperation operation, LocalFile file)
{
Log.Information($"[{_localVault.Id}] {operation}: {file.Fullname}");
}

/// <inheritdoc />
public void Failed(LocalFile file, string message)
Expand Down
12 changes: 12 additions & 0 deletions Parallel.Core/IO/PathBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ public static string TempDirectory

public static string TempFile => Path.Combine(TempDirectory, UnixTime.Now.TotalMilliseconds + ".tmp");

public static string LogDirectory
{
get
{
string logDir = Path.Combine(PathBuilder.TempDirectory, "Logs");
if (!Directory.Exists(logDir)) Directory.CreateDirectory(logDir);
return logDir;
}
}

public static string LogFile => Path.Combine(LogDirectory, UnixTime.Now.TotalMilliseconds + ".log");

/// <summary>
/// Gets the corresponding directory for program data based on the <see cref="OSPlatform"/>.
/// </summary>
Expand Down
Loading
Loading