Skip to content

Commit

Permalink
Merge pull request #56 from mark-s/FixProgressReport
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-s authored May 7, 2024
2 parents 887eaca + 3e186fb commit e1d4d40
Show file tree
Hide file tree
Showing 22 changed files with 132 additions and 83 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ This tool is an alternative to the [QENC Decrypter](https://www.qnap.com/en-uk/u

![See it in action](https://raw.githubusercontent.com/mark-s/QnapBackupDecryptor/master/Images/ExampleDecrypt.gif)



## Installation

Binaries for Windows, Linux and Mac, are available in [Releases](https://github.com/mark-s/QnapBackupDecryptor/releases).
Expand Down
3 changes: 0 additions & 3 deletions src/QnapBackupDecryptor.Console/DecryptResult.cs

This file was deleted.

9 changes: 5 additions & 4 deletions src/QnapBackupDecryptor.Console/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

namespace QnapBackupDecryptor.Console;

internal class Options
internal sealed class Options
{
[Option('p', "password", Required = false, HelpText = "Password")]
public string? Password { get; init; }

[Option('e', "encrypted", Required = true, HelpText = "Encrypted file or folder")]
public string EncryptedSource { get; init; } = null!;
public required string EncryptedSource { get; init; }

[Option('d', "decrypted", Required = true, HelpText = "Where to place the decrypted file(s)")]
public string OutputDestination { get; init; } = null!;
public required string OutputDestination { get; init; }

[Option('s', "subfolders", Required = false, HelpText = "Include Subfolders (default: false)")]
public bool IncludeSubfolders { get; init; }
Expand All @@ -26,6 +26,8 @@ internal class Options
[Option('r', "removeencrypted", Required = false, HelpText = "Delete encrypted files when decrypted (default: false)")]
public bool RemoveEncrypted { get; init; }

[Option('i', "inplace", Required = false, HelpText = "Encrypt files in-place (default: false)")]
public bool InPlace { get; init; }

[Usage(ApplicationAlias = "QnapBackupDecryptor")]
// ReSharper disable once UnusedMember.Global // Used by the console
Expand All @@ -40,5 +42,4 @@ public static IEnumerable<Example> Examples
yield return new Example("Decrypt a folder and all subfolders, and prompt for password", new Options { EncryptedSource = "./encryptedfolder", OutputDestination = "./decryptedfolder", IncludeSubfolders = true });
}
}

}
16 changes: 8 additions & 8 deletions src/QnapBackupDecryptor.Console/Output.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using QnapBackupDecryptor.Core;
using QnapBackupDecryptor.Core.Models;
using Spectre.Console;

namespace QnapBackupDecryptor.Console;
Expand All @@ -20,7 +20,7 @@ public static void ShowResults(IReadOnlyList<DecryptResult> decryptResults, IRea

ShowTiming(swElapsed);

if (decryptResults.Any(r => r.Success == false) || deleteResults.Any(r => r.DeletedOk == false))
if (decryptResults.Any(r => r.DecryptedOk == false) || deleteResults.Any(r => r.DeletedOk == false))
Environment.ExitCode = 1;
}

Expand All @@ -36,8 +36,8 @@ private static void ShowSimpleResults(IReadOnlyList<DecryptResult> decryptResult

table.AddRow(
$"{decryptResults.Count}",
$"[green]{decryptResults.Count(r => r.Success)}[/]",
$"[red]{decryptResults.Count(r => !r.Success)}[/]",
$"[green]{decryptResults.Count(r => r.DecryptedOk)}[/]",
$"[red]{decryptResults.Count(r => !r.DecryptedOk)}[/]",
$"[green]{deleteResults.Count(r => r.DeletedOk)}[/]",
$"[red]{deleteResults.Count(r => !r.DeletedOk)}[/]"
);
Expand All @@ -53,7 +53,7 @@ private static void ShowFileListResults(IReadOnlyList<DecryptResult> decryptResu
.AddColumn("Encrypted")
.AddColumn("Decrypted");

if (decryptResults.Any(r => r.Success == false))
if (decryptResults.Any(r => r.DecryptedOk == false))
table.AddColumn("Error");

if (deleteResults.Any())
Expand Down Expand Up @@ -81,8 +81,8 @@ private static void ShowFileListResults(IReadOnlyList<DecryptResult> decryptResu

private static List<string> DecryptResultToRow(DecryptResult decryptResult)
{
var colour = decryptResult.Success ? "green" : "red";
var status = decryptResult.Success ? "OK" : "Fail";
var colour = decryptResult.DecryptedOk ? "green" : "red";
var status = decryptResult.DecryptedOk ? "OK" : "Fail";

var row = new List<string>
{
Expand All @@ -91,7 +91,7 @@ private static List<string> DecryptResultToRow(DecryptResult decryptResult)
$"[{colour}]{decryptResult.Dest.FullName}[/]"
};

if (decryptResult.Success == false)
if (decryptResult.DecryptedOk == false)
row.Add($"[{colour}]{decryptResult.ErrorMessage}[/]");

return row;
Expand Down
52 changes: 19 additions & 33 deletions src/QnapBackupDecryptor.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CommandLine;
using QnapBackupDecryptor.Core;
using QnapBackupDecryptor.Core.Models;
using Spectre.Console;
using System.Collections.Concurrent;
using System.Diagnostics;
Expand All @@ -21,6 +22,10 @@ private static void Run(Options options)
if (Prompts.EnsureDeleteWanted(options) == false)
return;

// Double check in-place change is wanted
if (Prompts.EnsureInPlaceWanted(options) == false)
return;

var password = Prompts.GetPassword(options);

var stopwatch = Stopwatch.StartNew();
Expand All @@ -36,28 +41,23 @@ private static void Run(Options options)

private static IReadOnlyList<FileJob> GetDecryptJobs(Options options)
{
var decryptJobs = new List<FileJob>();

// get file list to process
AnsiConsole.Status()
return AnsiConsole
.Status()
.Start("Getting Files...", statusContext =>
{
statusContext.Spinner(Spinner.Known.SimpleDots);
statusContext.SpinnerStyle(Style.Parse("green"));
decryptJobs.AddRange(
JobMaker.GetDecryptJobs(
return JobMaker.GetDecryptJobs(
encryptedSource: options.EncryptedSource,
decryptedTarget: options.OutputDestination,
overwrite: options.Overwrite,
includeSubFolders: options.IncludeSubfolders)
);
includeSubFolders: options.IncludeSubfolders);
});

return decryptJobs;
}

private static (IReadOnlyList<DecryptResult> DecryptResults, IReadOnlyList<DeleteResult> DeleteResults) DoDecrypt(IReadOnlyCollection<FileJob> decryptJobs, Options options, byte[] password)
private static (IReadOnlyList<DecryptResult> DecryptResults, IReadOnlyList<DeleteResult> DeleteResults)
DoDecrypt(IReadOnlyCollection<FileJob> decryptJobs, Options options, byte[] password)
{
var decryptResults = new ConcurrentBag<DecryptResult>();
var deleteResults = new ConcurrentBag<DeleteResult>();
Expand All @@ -70,32 +70,18 @@ private static (IReadOnlyList<DecryptResult> DecryptResults, IReadOnlyList<Delet
var progressTask = progressContext.AddTask("[green]Decrypting Files[/]");
progressTask.MaxValue = decryptJobs.Count;
Parallel.ForEach(
decryptJobs,
currentJob => DecryptSingleJob(options, password, currentJob, decryptResults, deleteResults, progressTask));
Parallel.ForEach(decryptJobs, job =>
{
var (decryptResult, deleteResult) = DecryptorService.Decrypt(options.RemoveEncrypted, password, job, progressTask.Increment);
decryptResults.Add(decryptResult);
if (deleteResult != null)
deleteResults.Add(deleteResult);
});
});

return (decryptResults.ToList(), deleteResults.ToList());

}

private static void DecryptSingleJob(
Options options, byte[] password, FileJob currentJob, ConcurrentBag<DecryptResult> decryptResults,
ConcurrentBag<DeleteResult> deleteResults, ProgressTask progressTask)
{
if (currentJob.IsValid)
{
var decryptionResult = OpenSsl.Decrypt(new FileInfo(currentJob.EncryptedFile.FullName), password, new FileInfo(currentJob.OutputFile.FullName));

decryptResults.Add(new DecryptResult(currentJob.EncryptedFile, currentJob.OutputFile, decryptionResult.IsSuccess, decryptionResult.ErrorMessage));

// Delete encrypted file only if success and option chosen
if (decryptionResult.IsSuccess && options.RemoveEncrypted)
deleteResults.Add(FileService.TryDelete(currentJob.EncryptedFile));
}
else
decryptResults.Add(new DecryptResult(currentJob.EncryptedFile, currentJob.OutputFile, currentJob.IsValid, currentJob.ErrorMessage));

progressTask.Increment(decryptResults.Count);
}
}
43 changes: 35 additions & 8 deletions src/QnapBackupDecryptor.Console/Prompts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ namespace QnapBackupDecryptor.Console;

internal static class Prompts
{
public static byte[] GetPassword(Options opts)
private const string YES = "y";
private const string NO = "n";
private const string INVALID_OPTION_ENTERED = "[yellow]That's not a valid option[/]";

public static byte[] GetPassword(Options options)
{
if (string.IsNullOrEmpty(opts.Password) == false)
return Encoding.UTF8.GetBytes(opts.Password);
if (string.IsNullOrEmpty(options.Password) == false)
return Encoding.UTF8.GetBytes(options.Password);
else
{
var password = AnsiConsole.Prompt(
Expand All @@ -26,11 +30,34 @@ public static bool EnsureDeleteWanted(Options options)

var response = AnsiConsole.Prompt(
new TextPrompt<string>("[bold]>> Are you sure you want to delete the encrypted files?[/]")
.InvalidChoiceMessage("[yellow]That's not a valid option[/]")
.DefaultValue("n")
.AddChoice("y")
.AddChoice("n"));
.WithYesNoOptions(defaultOption: NO));

return response.Equals("y", StringComparison.InvariantCultureIgnoreCase);
return response.IsYes();
}

public static bool EnsureInPlaceWanted(Options options)
{
if (options.InPlace == false)
return true;

var initialResponse = AnsiConsole.Prompt(
new TextPrompt<string>("[bold]>> Are you sure you want to decrypt the files in-place? If a decrypt produces a bad file - you will lose that file![/]")
.WithYesNoOptions(defaultOption: NO));

var areYouSureResponse = AnsiConsole.Prompt(
new TextPrompt<string>("[bold]>> Are you really sure? Do you have a backup in case anything goes wrong?[/]")
.WithYesNoOptions(defaultOption: NO));

return initialResponse.IsYes() && areYouSureResponse.IsYes();
}

private static TextPrompt<string> WithYesNoOptions(this TextPrompt<string> prompt, string defaultOption)
=> prompt.InvalidChoiceMessage(INVALID_OPTION_ENTERED)
.DefaultValue(defaultOption)
.AddChoice(YES)
.AddChoice(NO);

private static bool IsYes(this string? value)
=> value?.Equals(YES, StringComparison.OrdinalIgnoreCase) ?? false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion src/QnapBackupDecryptor.Core.Tests/DecryptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public void OpenSSLDecrypt_ValidPassword_OkResult()
sslDecrypt.IsSuccess.ShouldBeTrue();
}


[Test]
public void OpenSSLDecrypt_Text()
{
Expand Down
1 change: 0 additions & 1 deletion src/QnapBackupDecryptor.Core.Tests/FileHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ public void IsOpenSslEncrypted_NotOpenSslFile_False()
// Assert
result.Data.ShouldBeFalse();
}

}
1 change: 0 additions & 1 deletion src/QnapBackupDecryptor.Core.Tests/JobMakerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,4 @@ public void GetDecryptJobs_EncryptedFileExists_TargetExists_OverwriteFalse_Produ
result[0].IsValid.ShouldBeFalse();
result[0].ErrorMessage.ShouldBe("Output file already exists, use --overwrite to overwrite files.");
}

}
3 changes: 0 additions & 3 deletions src/QnapBackupDecryptor.Core/DecryptResult.cs

This file was deleted.

40 changes: 40 additions & 0 deletions src/QnapBackupDecryptor.Core/DecryptorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using QnapBackupDecryptor.Core.Models;

namespace QnapBackupDecryptor.Core;

public class DecryptorService
{
public static (DecryptResult decryptResult, DeleteResult? deleteResult) Decrypt(
bool removeEncrypted,
byte[] password,
FileJob job,
Action<double> progressUpdate)
{

DecryptResult decrypted;
DeleteResult? deleted =null;

if (job.IsValid)
{
var decryptionResult = OpenSsl.Decrypt(
encryptedFile: new FileInfo(job.EncryptedFile.FullName),
password: password,
outputFile: new FileInfo(job.OutputFile.FullName));

decrypted = new DecryptResult(job.EncryptedFile, job.OutputFile, decryptionResult.IsSuccess, decryptionResult.ErrorMessage);

// Delete encrypted file only if success and option chosen
if (decryptionResult.IsSuccess && removeEncrypted)
deleted = FileService.TryDelete(job.EncryptedFile);
}
else
{
decrypted = new DecryptResult(job.EncryptedFile, job.OutputFile, job.IsValid, job.ErrorMessage);
}

progressUpdate(1);

return (decrypted, deleted);

}
}
1 change: 1 addition & 0 deletions src/QnapBackupDecryptor.Core/FileInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ internal static bool TryDelete(this FileInfo fileInfo)
{
try
{
fileInfo.Refresh();
if (fileInfo.Exists)
fileInfo.Delete();

Expand Down
4 changes: 3 additions & 1 deletion src/QnapBackupDecryptor.Core/FileJobExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace QnapBackupDecryptor.Core;
using QnapBackupDecryptor.Core.Models;

namespace QnapBackupDecryptor.Core;

internal static class FileJobExtensions
{
Expand Down
5 changes: 3 additions & 2 deletions src/QnapBackupDecryptor.Core/FileService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace QnapBackupDecryptor.Core;
using QnapBackupDecryptor.Core.Models;

namespace QnapBackupDecryptor.Core;

public static class FileService
{
Expand All @@ -14,5 +16,4 @@ public static DeleteResult TryDelete(FileSystemInfo toDelete)
return new DeleteResult(toDelete, false, ex.Message);
}
}

}
10 changes: 4 additions & 6 deletions src/QnapBackupDecryptor.Core/JobMaker.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
namespace QnapBackupDecryptor.Core;
using QnapBackupDecryptor.Core.Models;

namespace QnapBackupDecryptor.Core;

public static class JobMaker
{
public static List<FileJob> GetDecryptJobs(string encryptedSource, string decryptedTarget, bool overwrite, bool includeSubFolders)
public static IReadOnlyList<FileJob> GetDecryptJobs(string encryptedSource, string decryptedTarget, bool overwrite, bool includeSubFolders)
{
if (Directory.Exists(encryptedSource) == false && File.Exists(encryptedSource) == false)
return new FileJob(new DirectoryInfo(encryptedSource), new FileInfo(decryptedTarget), false, "Source does not exist").ToList();
Expand All @@ -25,7 +27,6 @@ public static List<FileJob> GetDecryptJobs(string encryptedSource, string decryp
return GetFileToFileJob(new FileInfo(encryptedSource), new FileInfo(decryptedTarget), overwrite).ToList();
}


private static FileJob GetFileToFileJob(FileInfo encrytedFile, FileInfo outputFile, bool overwrite)
{
if (encrytedFile.Exists == false)
Expand Down Expand Up @@ -63,7 +64,4 @@ private static List<FileJob> GetFolderToFolderJobs(DirectoryInfo encrytedFolder,
.Select(encrytedFile => GetFileToFolderJob(encrytedFile, outputFolder, overwrite))
.ToList();
}



}
Loading

0 comments on commit e1d4d40

Please sign in to comment.