This PR adds a live-reporting module that sends the current progress of backups to a user specified url. The intention is that this can be used for dashboards that want to show the current progress for backups. By default, the module is not configured and has no impact.
180 lines
7.3 KiB
C#
180 lines
7.3 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// Bridges an <see cref="IReportModule"/> to the operation's <see cref="IMessageSink"/>.
|
|
///
|
|
/// The adapter is appended to the controller's message sink so it receives backend
|
|
/// events (<see cref="BackendEvent"/>) and log entries (<see cref="WriteMessage"/>),
|
|
/// and is handed the live <see cref="IBackendProgress"/> and
|
|
/// <see cref="IOperationProgress"/> 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.
|
|
/// </summary>
|
|
internal sealed class ReportModuleAdapter : IMessageSink
|
|
{
|
|
private static readonly string LOGTAG = Log.LogTagFromType<ReportModuleAdapter>();
|
|
|
|
private readonly IReportModule m_module;
|
|
private readonly CancellationToken m_cancellationToken;
|
|
private IBackendProgress m_backendProgress;
|
|
private IOperationProgress m_operationProgress;
|
|
|
|
/// <summary>
|
|
/// The module being bridged.
|
|
/// </summary>
|
|
public IReportModule Module => m_module;
|
|
|
|
/// <summary>
|
|
/// Constructs a new adapter for the given module.
|
|
/// </summary>
|
|
/// <param name="module">The report module to forward events to.</param>
|
|
/// <param name="cancellationToken">A token used to cancel forwarded callbacks.</param>
|
|
public ReportModuleAdapter(IReportModule module, CancellationToken cancellationToken)
|
|
{
|
|
m_module = module;
|
|
m_cancellationToken = cancellationToken;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetBackendProgress(IBackendProgress progress)
|
|
=> m_backendProgress = progress;
|
|
|
|
/// <inheritdoc />
|
|
public void SetOperationProgress(IOperationProgress progress)
|
|
=> m_operationProgress = progress;
|
|
|
|
/// <inheritdoc />
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a progress snapshot from the current backend and operation progress
|
|
/// objects and invokes <see cref="IReportModule.OnProgressTickAsync"/> on the module.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public Task TickAsync()
|
|
{
|
|
var snapshot = BuildSnapshot();
|
|
return ForwardAsync(() => m_module.OnProgressTickAsync(snapshot, m_cancellationToken), "OnProgressTickAsync");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a snapshot of the current progress, mapping the live progress objects
|
|
/// (which live in <c>Duplicati.Library.Main</c>) to the interface-level
|
|
/// <see cref="ReportProgressSnapshot"/>.
|
|
/// </summary>
|
|
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<ReportBackendEvent>();
|
|
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<Task> action, string label)
|
|
=> ForwardAsync(action, label).FireAndForget();
|
|
|
|
private async Task ForwardAsync(System.Func<Task> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|