From a0d5096ebc56ab81bc2555fd5b39b4e47b0884c8 Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Tue, 18 Feb 2025 09:25:20 +0100 Subject: [PATCH] Reworked the "Volume loop" of the restore network to handle Backends themselves, rather than using the BackendManager. This allows for tighter control over which process performs which action, allowing them more easily to run in parallel. --- .../Main/Operation/Restore/Channels.cs | 5 +- .../Main/Operation/Restore/VolumeDecryptor.cs | 90 ++++++++++++++++++- .../Operation/Restore/VolumeDownloader.cs | 21 +++-- .../Main/Operation/Restore/VolumeManager.cs | 15 +--- .../Library/Main/Operation/RestoreHandler.cs | 4 +- 5 files changed, 110 insertions(+), 25 deletions(-) diff --git a/Duplicati/Library/Main/Operation/Restore/Channels.cs b/Duplicati/Library/Main/Operation/Restore/Channels.cs index 4d14ee055..9b83773da 100644 --- a/Duplicati/Library/Main/Operation/Restore/Channels.cs +++ b/Duplicati/Library/Main/Operation/Restore/Channels.cs @@ -19,7 +19,6 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System.Threading.Tasks; using CoCoL; using Duplicati.Library.Main.Volumes; using Duplicati.Library.Utility; @@ -46,12 +45,12 @@ namespace Duplicati.Library.Main.Operation.Restore /// /// Channel between and . /// - public readonly IChannel<(long, Task)> DownloadRequest = ChannelManager.CreateChannel<(long, Task)>(buffersize: BufferSize); + public readonly IChannel DownloadRequest = ChannelManager.CreateChannel(buffersize: BufferSize); /// /// Channel between and /// - public readonly IChannel<(long, TempFile)> DecryptRequest = ChannelManager.CreateChannel<(long, TempFile)>(buffersize: BufferSize); + public readonly IChannel<(long, string, TempFile)> DecryptRequest = ChannelManager.CreateChannel<(long, string, TempFile)>(buffersize: BufferSize); /// /// Channel between and diff --git a/Duplicati/Library/Main/Operation/Restore/VolumeDecryptor.cs b/Duplicati/Library/Main/Operation/Restore/VolumeDecryptor.cs index 2dc5fa8e8..59576fac9 100644 --- a/Duplicati/Library/Main/Operation/Restore/VolumeDecryptor.cs +++ b/Duplicati/Library/Main/Operation/Restore/VolumeDecryptor.cs @@ -21,6 +21,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using CoCoL; using Duplicati.Library.Main.Volumes; @@ -57,15 +58,23 @@ namespace Duplicati.Library.Main.Operation.Restore Stopwatch sw_decrypt = options.InternalProfiling ? new() : null; try { + // Taken from BackendManager.GetOperation + using var encryption = options.NoEncryption + ? null + : (DynamicLoader.EncryptionLoader.GetModule(options.EncryptionModule, options.Passphrase, options.RawOptions) + ?? throw new Exception(Strings.BackendMananger.EncryptionModuleNotFound(options.EncryptionModule)) + ); + while (true) { // Get the block request and volume from the `VolumeDownloader` process. sw_read?.Start(); - var (volume_id, volume) = await self.Input.ReadAsync(); + var (volume_id, volume_name, volume) = await self.Input.ReadAsync().ConfigureAwait(false); sw_read?.Stop(); sw_decrypt?.Start(); - var bvr = new BlockVolumeReader(options.CompressionModule, volume, options); + var tmpfile = DecryptFile(volume, DetectEncryptionModule(volume_name, options, encryption)); + var bvr = new BlockVolumeReader(options.CompressionModule, tmpfile, options); sw_decrypt?.Stop(); sw_write?.Start(); @@ -90,6 +99,83 @@ namespace Duplicati.Library.Main.Operation.Restore } }); } + + // Taken from BackendManager.GetOperation + public static Library.Utility.TempFile DecryptFile(Library.Utility.TempFile tempFile, Interface.IEncryption? decrypter) + { + // Support no encryption + if (decrypter == null) + return tempFile; + + Library.Utility.TempFile? decryptTarget = null; + + // Always dispose the source file + using (tempFile) + using (new Logging.Timer(LOGTAG, "DecryptFile", "Decrypting " + tempFile)) + { + try + { + decryptTarget = new Library.Utility.TempFile(); + try { decrypter.Decrypt(tempFile, decryptTarget); } + // If we fail here, make sure that we throw a crypto exception + catch (System.Security.Cryptography.CryptographicException) { throw; } + catch (Exception ex) { throw new System.Security.Cryptography.CryptographicException(ex.Message, ex); } + + var result = decryptTarget; + decryptTarget = null; + return result; + } + finally + { + // Remove temp files on failure + decryptTarget?.Dispose(); + } + } + } + + private static Interface.IEncryption? DetectEncryptionModule(string remotefilename, Options options, Interface.IEncryption? encryption) + { + try + { + // Auto-guess the encryption module + var ext = (System.IO.Path.GetExtension(remotefilename) ?? "").TrimStart('.'); + if (!ext.Equals(encryption?.FilenameExtension, StringComparison.OrdinalIgnoreCase)) + { + // Check if the file is not encrypted + if (DynamicLoader.CompressionLoader.Keys.Contains(ext, StringComparer.OrdinalIgnoreCase)) + { + if (encryption != null) + Logging.Log.WriteVerboseMessage(LOGTAG, "AutomaticDecryptionDetection", "Filename extension \"{0}\" does not match encryption module \"{1}\", guessing that it is not encrypted", ext, options.EncryptionModule); + return null; + } + // Check if the file is encrypted with something else + else if (DynamicLoader.EncryptionLoader.Keys.Contains(ext, StringComparer.OrdinalIgnoreCase)) + { + Logging.Log.WriteVerboseMessage(LOGTAG, "AutomaticDecryptionDetection", "Filename extension \"{0}\" does not match encryption module \"{1}\", attempting to use matching encryption module", ext, options.EncryptionModule); + + try + { + return DynamicLoader.EncryptionLoader.GetModule(ext, options.Passphrase, options.RawOptions) + ?? encryption; + } + catch (Exception ex) + { + Logging.Log.WriteWarningMessage(LOGTAG, "AutomaticDecryptionDetection", ex, "Failed to load encryption module \"{0}\", using specified encryption module \"{1}\"", ext, options.EncryptionModule); + } + } + // Fallback, lets see what happens... + else + { + Logging.Log.WriteVerboseMessage(LOGTAG, "AutomaticDecryptionDetection", "Filename extension \"{0}\" does not match encryption module \"{1}\", attempting to use specified encryption module as no others match", ext, options.EncryptionModule); + } + } + + return encryption; + } + // If we fail here, make sure that we throw a crypto exception + catch (System.Security.Cryptography.CryptographicException) { throw; } + catch (Exception ex) { throw new System.Security.Cryptography.CryptographicException(ex.Message, ex); } + } } } \ No newline at end of file diff --git a/Duplicati/Library/Main/Operation/Restore/VolumeDownloader.cs b/Duplicati/Library/Main/Operation/Restore/VolumeDownloader.cs index 40a28d772..65c20c92a 100644 --- a/Duplicati/Library/Main/Operation/Restore/VolumeDownloader.cs +++ b/Duplicati/Library/Main/Operation/Restore/VolumeDownloader.cs @@ -22,8 +22,10 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using CoCoL; +using Duplicati.Library.Interface; using Duplicati.Library.Main.Database; using Duplicati.Library.Utility; @@ -43,9 +45,12 @@ namespace Duplicati.Library.Main.Operation.Restore /// /// Runs the volume downloader process. /// + /// The named channels for the restore operation. + /// The local restore database, used to find volume information given the volume ID. + /// The backend to use for downloading the volumes. /// The restore options. /// The restore results. - public static Task Run(Channels channels, LocalRestoreDatabase db, Options options, RestoreResults results) + public static Task Run(Channels channels, LocalRestoreDatabase db, IBackend backend, Options options, RestoreResults results) { return AutomationExtensions.RunTask( new @@ -65,19 +70,19 @@ namespace Duplicati.Library.Main.Operation.Restore { // Get the block request from the `BlockManager` process. sw_read?.Start(); - var (volume_id, waittask) = await self.Input.ReadAsync(); + var volume_id = await self.Input.ReadAsync().ConfigureAwait(false); sw_read?.Stop(); // Trigger the download. sw_wait?.Start(); - TempFile f = null; + TempFile f = new(); + var (volume_name, size, hash) = db.GetVolumeInfo(volume_id).First(); try { - f = await waittask.ConfigureAwait(false); + await backend.GetAsync(volume_name, f.Name, CancellationToken.None).ConfigureAwait(false); } catch (Exception) { - var (volume_name, _, _) = db.GetVolumeInfo(volume_id).First(); lock (results) results.BrokenRemoteFiles.Add(volume_name); @@ -87,7 +92,7 @@ namespace Duplicati.Library.Main.Operation.Restore // Pass the download handle (which may or may not have downloaded already) to the `VolumeDecryptor` process. sw_write?.Start(); - await self.Output.WriteAsync((volume_id, f)); + await self.Output.WriteAsync((volume_id, volume_name, f)).ConfigureAwait(false); sw_write?.Stop(); } } @@ -105,6 +110,10 @@ namespace Duplicati.Library.Main.Operation.Restore Logging.Log.WriteErrorMessage(LOGTAG, "DownloadError", ex, "Error during download"); throw; } + finally + { + backend.Dispose(); + } }); } } diff --git a/Duplicati/Library/Main/Operation/Restore/VolumeManager.cs b/Duplicati/Library/Main/Operation/Restore/VolumeManager.cs index ba14a643d..0c636973c 100644 --- a/Duplicati/Library/Main/Operation/Restore/VolumeManager.cs +++ b/Duplicati/Library/Main/Operation/Restore/VolumeManager.cs @@ -20,7 +20,6 @@ // DEALINGS IN THE SOFTWARE. using CoCoL; -using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Volumes; using Duplicati.Library.Utility; using System; @@ -46,11 +45,9 @@ namespace Duplicati.Library.Main.Operation.Restore /// /// Runs the volume manager process. /// - /// The local restore database, used to find the volume where a block is stored. - /// The backend manager, used to fetch the volumes from the backend. + /// The named channels for the restore operation. /// The restore options. - /// The restore results. - public static Task Run(Channels channels, LocalRestoreDatabase db, IBackendManager backend, Options options, RestoreResults results) + public static Task Run(Channels channels, Options options) { return AutomationExtensions.RunTask( new @@ -106,13 +103,7 @@ namespace Duplicati.Library.Main.Operation.Restore } else { - sw_query?.Start(); - var (volume_name, volume_size, volume_hash) = db.GetVolumeInfo(request.VolumeID).First(); - sw_query?.Stop(); - sw_backend?.Start(); - var handle = backend.GetAsync(volume_name, volume_hash, volume_size, results.TaskControl.TransferToken); - sw_backend?.Stop(); - await self.DownloadRequest.WriteAsync((request.VolumeID, handle)); + await self.DownloadRequest.WriteAsync(request.VolumeID).ConfigureAwait(false); in_flight[request.VolumeID] = [request]; } } diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs index 86fd0030c..c119e47a3 100644 --- a/Duplicati/Library/Main/Operation/RestoreHandler.cs +++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs @@ -339,8 +339,8 @@ namespace Duplicati.Library.Main.Operation var filelister = Restore.FileLister.Run(channels, database, m_options, m_result); var fileprocessors = Enumerable.Range(0, m_options.RestoreFileProcessors).Select(i => Restore.FileProcessor.Run(channels, database, fileprocessor_requests[i], fileprocessor_responses[i], m_options, m_result)).ToArray(); var blockmanager = Restore.BlockManager.Run(channels, database, m_options, fileprocessor_requests, fileprocessor_responses); - var volumecache = Restore.VolumeManager.Run(channels, database, backendManager, m_options, m_result); - var volumedownloaders = Enumerable.Range(0, m_options.RestoreVolumeDownloaders).Select(i => Restore.VolumeDownloader.Run(channels, database, m_options, m_result)).ToArray(); + var volumecache = Restore.VolumeManager.Run(channels, m_options); + var volumedownloaders = Enumerable.Range(0, m_options.RestoreVolumeDownloaders).Select(i => Restore.VolumeDownloader.Run(channels, database, backendManager.GetBackend(), m_options, m_result)).ToArray(); var volumedecryptors = Enumerable.Range(0, m_options.RestoreVolumeDecryptors).Select(i => Restore.VolumeDecryptor.Run(channels, m_options)).ToArray(); var volumedecompressors = Enumerable.Range(0, m_options.RestoreVolumeDecompressors).Select(i => Restore.VolumeDecompressor.Run(channels, m_options)).ToArray();