// Copyright (C) 2026, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System.Threading; using System.Threading.Tasks; using CoCoL; using Duplicati.Library.Interface; using Duplicati.Library.Logging; namespace Duplicati.Library.Main { /// /// Bridges an to the operation's . /// /// The adapter is appended to the controller's message sink so it receives backend /// events () and log entries (), /// and is handed the live and /// objects so a progress ticker can build snapshots. /// Each callback is forwarded to the module and isolated from failures: exceptions /// thrown by the module are logged and do not propagate to the engine. /// internal sealed class ReportModuleAdapter : IMessageSink { private static readonly string LOGTAG = Log.LogTagFromType(); private readonly IReportModule m_module; private readonly CancellationToken m_cancellationToken; private IBackendProgress m_backendProgress; private IOperationProgress m_operationProgress; /// /// The module being bridged. /// public IReportModule Module => m_module; /// /// Constructs a new adapter for the given module. /// /// The report module to forward events to. /// A token used to cancel forwarded callbacks. public ReportModuleAdapter(IReportModule module, CancellationToken cancellationToken) { m_module = module; m_cancellationToken = cancellationToken; } /// public void BackendEvent(BackendActionType action, BackendEventType type, string path, long size) { var evt = new ReportBackendEvent(action.ToString(), type.ToString(), path, size); Forward(() => m_module.OnBackendEventAsync(evt, m_cancellationToken), "OnBackendEventAsync"); } /// public void SetBackendProgress(IBackendProgress progress) => m_backendProgress = progress; /// public void SetOperationProgress(IOperationProgress progress) => m_operationProgress = progress; /// public void WriteMessage(LogEntry entry) { if (entry == null) return; var snapshot = new ReportLogEntry( entry.AsString(true), entry.Level.ToString(), entry.FilterTag, entry.Id, entry.When, entry.Exception?.ToString()); Forward(() => m_module.OnLogEntryAsync(snapshot, m_cancellationToken), "OnLogEntryAsync"); } /// /// Builds a progress snapshot from the current backend and operation progress /// objects and invokes on the module. /// /// A task that represents the asynchronous operation. public Task TickAsync() { var snapshot = BuildSnapshot(); return ForwardAsync(() => m_module.OnProgressTickAsync(snapshot, m_cancellationToken), "OnProgressTickAsync"); } /// /// Builds a snapshot of the current progress, mapping the live progress objects /// (which live in Duplicati.Library.Main) to the interface-level /// . /// private ReportProgressSnapshot BuildSnapshot() { OperationPhase phase = default; float progress = 0; long filesProcessed = 0; long fileSizeProcessed = 0; long fileCount = 0; long fileSize = 0; bool countingFiles = false; string currentFilename = null; long currentFileSize = 0; long currentFileOffset = 0; if (m_operationProgress != null) { m_operationProgress.UpdateOverall(out phase, out progress, out filesProcessed, out fileSizeProcessed, out fileCount, out fileSize, out countingFiles); m_operationProgress.UpdateFile(out currentFilename, out currentFileSize, out currentFileOffset, out _); } ReportBackendEvent[] activeTransfers = System.Array.Empty(); if (m_backendProgress != null) { var transfers = m_backendProgress.GetActiveTransfers(); if (transfers != null && transfers.Length > 0) { activeTransfers = new ReportBackendEvent[transfers.Length]; for (int i = 0; i < transfers.Length; i++) { var t = transfers[i]; activeTransfers[i] = new ReportBackendEvent( t.Action.ToString(), "Progress", t.Path, t.Size); } } } return new ReportProgressSnapshot( phase.ToString(), progress, filesProcessed, fileSizeProcessed, fileCount, fileSize, countingFiles, currentFilename, currentFileSize, currentFileOffset, activeTransfers); } private void Forward(System.Func action, string label) => ForwardAsync(action, label).FireAndForget(); private async Task ForwardAsync(System.Func action, string label) { try { await action().ConfigureAwait(false); } catch (System.Exception ex) { Log.WriteWarningMessage(LOGTAG, $"ReportModule{label}", ex, "Report module {0} callback {1} failed: {2}", m_module.Key, label, ex.Message); } } } }