Files
duplicati/Duplicati/Library/Main/IPC/ControllerRpcProxy.cs
T
Kenneth Skovhede 8663f55e8b Fixed case-insensitive search
This PR fixes a bug that caused backup searching to be case-sensitive.

The PR also exposes the case sensitive-flag in the API endpoint so the UI can toggle case-sensitive searching.
2026-06-17 10:15:08 +02:00

672 lines
26 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Threading.Tasks;
using Duplicati.Library.Interface;
using Duplicati.Library.Main.IPC.Dto;
using Duplicati.Library.Utility;
using StreamJsonRpc;
#nullable enable
namespace Duplicati.Library.Main.IPC;
/// <summary>
/// Client-side proxy for the controller that runs in a separate spawned process
/// </summary>
public class ControllerRpcProxy : IController, IDisposable, IControllerRpcCallbacks
{
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<ControllerRpcProxy>();
private Process? _process;
private JsonRpc? _rpc;
private readonly IMessageSink _messageSink;
private readonly string _backendUrl;
private readonly Dictionary<string, string?> _options;
private JsonRpc Rpc => _rpc ?? throw new InvalidOperationException("RPC connection not established");
/// <inheritdoc />
public Action<IBasicResults>? OnOperationStarted { get; set; }
/// <inheritdoc />
public Action<IBasicResults, Exception>? OnOperationCompleted { get; set; }
/// <inheritdoc />
public Task SetLastCompactAsync(DateTime lastCompact)
=> Rpc.InvokeAsync(nameof(IController.SetLastCompactAsync), lastCompact).WaitAsync(TimeSpan.FromSeconds(5));
/// <inheritdoc />
public Task SetLastVacuumAsync(DateTime lastVacuum)
=> Rpc.InvokeAsync(nameof(IController.SetLastVacuumAsync), lastVacuum).WaitAsync(TimeSpan.FromSeconds(5));
/// <summary>
/// Creates a new controller proxy
/// </summary>
private ControllerRpcProxy(
string backendUrl,
Dictionary<string, string?> options,
IMessageSink messageSink)
{
_backendUrl = backendUrl;
_options = options;
_messageSink = messageSink;
}
/// <summary>
/// Creates a new controller proxy
/// </summary>
/// <param name="backendUrl">The backend URL</param>
/// <param name="options">The options to pass</param>
/// <param name="messageSink">The message sink to use</param>
/// <returns>The configured proxy</returns>
public static async Task<ControllerRpcProxy> CreateProxyAsync(string backendUrl, Dictionary<string, string?> options, IMessageSink messageSink)
{
var proxy = new ControllerRpcProxy(backendUrl, options, messageSink);
try
{
await proxy.StartRemoteProcessAsync().ConfigureAwait(false);
// For debugging purposes, we can also start a mocked remote process
// await proxy.StartMockedRemoteProcessAsync().ConfigureAwait(false);
}
catch
{
try { proxy.Dispose(); }
catch { }
throw;
}
return proxy;
}
/// <summary>
/// Starts running the remote controller process
/// </summary>
/// <returns>An awaitable task</returns>
private async Task StartRemoteProcessAsync()
{
var exePath = GetControllerExecutablePath();
if (!File.Exists(exePath))
throw new FileNotFoundException($"Controller executable not found: {exePath}");
var psi = new ProcessStartInfo(exePath, "controller-server")
{
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(exePath)
};
_process = Process.Start(psi)
?? throw new InvalidOperationException("Failed to start controller process");
// Setup StreamJsonRpc over stdio
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RpcJsonOptions.Options };
var handler = new NewLineDelimitedMessageHandler(
_process.StandardInput.BaseStream,
_process.StandardOutput.BaseStream,
formatter);
_rpc = new JsonRpc(handler, this);
_rpc.StartListening();
// Start stderr reader for logging
_ = Task.Run(ReadStderrAsync);
// Initialize the remote controller
await _rpc.InvokeAsync("InitializeAsync", _backendUrl, _options).ConfigureAwait(false);
}
#if DEBUG
/// <summary>
/// Starts a mocked remote process for testing purposes.
/// The code lives in the the same process, but uses the RPC interface to communicate with the server.
/// </summary>
/// <returns>An awaitable task</returns>
private async Task StartMockedRemoteProcessAsync()
{
var pipeName = $"dupl-{Guid.NewGuid():N}"[..9];
var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
_ = Task.Run(async () =>
{
try
{
await pipeServer.WaitForConnectionAsync().ConfigureAwait(false);
var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RpcJsonOptions.Options };
var handler = new NewLineDelimitedMessageHandler(
pipeServer,
pipeServer,
formatter);
var server = new ControllerRpcServer(this);
var rpc = new JsonRpc(handler, server);
rpc.StartListening();
try
{
await rpc.Completion.ConfigureAwait(false);
}
catch { }
finally
{
await server.DisposeAsync().ConfigureAwait(false);
await pipeServer.DisposeAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
Library.Logging.Log.WriteWarningMessage(LOGTAG, "MockServerError", ex, "Mocked RPC server encountered an error");
}
});
var pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
await pipeClient.ConnectAsync().ConfigureAwait(false);
var clientFormatter = new SystemTextJsonFormatter { JsonSerializerOptions = RpcJsonOptions.Options };
var clientHandler = new NewLineDelimitedMessageHandler(
pipeClient,
pipeClient,
clientFormatter);
_rpc = new JsonRpc(clientHandler, this);
_rpc.StartListening();
// Initialize the remote controller
await _rpc.InvokeAsync(nameof(ControllerRpcServer.InitializeAsync), _backendUrl, _options).ConfigureAwait(false);
}
#endif
private async Task ReadStderrAsync()
{
try
{
if (_process?.StandardError == null) return;
string? line;
while ((line = await _process.StandardError.ReadLineAsync()) != null)
{
Library.Logging.Log.WriteInformationMessage(LOGTAG, "ControllerStderr", line);
}
}
catch (Exception ex)
{
Library.Logging.Log.WriteWarningMessage(LOGTAG, "StderrReadError", ex, "Error reading controller stderr");
}
}
#if DEBUG
private static string? GetControllerExecutablePath()
{
// Look in the source tree
var baseDir = System.Reflection.Assembly.GetEntryAssembly()!.Location;
// Go from ?/Executables/Project1/bin/Debug/net10/project-filename(.exe)
// to ?/Executables/Duplicati.CommandLine/bin/Debug/Duplicati.CommandLine(.exe)
var parts = baseDir.Split(Path.DirectorySeparatorChar);
parts[parts.Length - 1] = "Duplicati.CommandLine";
if (OperatingSystem.IsWindows())
parts[parts.Length - 1] += ".exe";
// Deal with ?/Duplicati/UnitTest/bin/Debug/net10/testhost.dll
if (!string.Equals(parts[parts.Length - 6], "Executables", StringComparison.OrdinalIgnoreCase))
parts[parts.Length - 6] = "Executables";
parts[parts.Length - 5] = "Duplicati.CommandLine";
var targetPath = string.Join(Path.DirectorySeparatorChar, parts);
if (File.Exists(targetPath))
return targetPath;
return GetControllerExecutablePathInner();
}
#else
private static string GetControllerExecutablePath() => GetControllerExecutablePathInner();
#endif
private static string GetControllerExecutablePathInner()
{
var installDir = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
var exeName = Library.AutoUpdater.PackageHelper.GetExecutableName(
Library.AutoUpdater.PackageHelper.NamedExecutable.CommandLine);
return Path.Combine(installDir, exeName);
}
#region IControllerRpcCallbacks Implementation
/// <summary>
/// Called by the RPC server when a log message is written
/// </summary>
public Task OnLogMessageAsync(LogEntryDto entry)
{
if (_messageSink != null && entry != null)
{
try
{
var logEntry = entry.ToLogEntry();
_messageSink.WriteMessage(logEntry);
}
catch (Exception ex)
{
Library.Logging.Log.WriteWarningMessage(LOGTAG, "LogForwardError", ex, "Error forwarding log message");
}
}
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server for backend events
/// </summary>
public Task OnBackendEventAsync(BackendActionType action, BackendEventType type, string path, long size)
{
_messageSink?.BackendEvent(action, type, path, size);
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server when an operation starts
/// </summary>
public Task OnOperationStartedAsync(OperationMode operation)
{
// Note: The actual result object is created locally in the remote process
// For simplicity, we don't proxy the full callback mechanism
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server when an operation completes
/// </summary>
public Task OnOperationCompletedAsync(OperationResultDto result, ExceptionDto exception)
{
// The operation completion is handled by the return value of the RPC call
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server when backend progress updates
/// </summary>
public Task OnBackendProgressAsync(BackendProgressDto progress)
{
if (_messageSink != null && progress != null)
_messageSink.SetBackendProgress(new BackendProgressWrapper(progress));
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server when operation progress updates
/// </summary>
public Task OnOperationProgressAsync(OperationProgressDto progress)
{
if (_messageSink != null && progress != null)
{
if (_operationProgressWrapper == null)
{
_operationProgressWrapper = new OperationProgressWrapper();
_messageSink.SetOperationProgress(_operationProgressWrapper);
}
_operationProgressWrapper.UpdateFromDto(progress);
}
return Task.CompletedTask;
}
/// <summary>
/// Called by the RPC server when the operation phase changes
/// </summary>
public Task OnPhaseChangedAsync(OperationPhase phase, OperationPhase previousPhase)
{
_operationProgressWrapper?.SetPhase(phase, previousPhase);
return Task.CompletedTask;
}
#endregion
#region IController Implementation
/// <inheritdoc />
public async Task<IBackupResults> BackupAsync(string[] inputsources, IFilter? filter = null)
=> await Rpc.InvokeAsync<IBackupResults>(nameof(IController.BackupAsync), inputsources, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IRestoreResults> RestoreAsync(string[]? paths, IFilter? filter = null)
=> await Rpc.InvokeAsync<IRestoreResults>(nameof(IController.RestoreAsync), paths, filter).ConfigureAwait(false);
/// <inheritdoc />
public Task<IListResults> ListAsync()
=> ListAsync(null, null);
/// <inheritdoc />
public Task<IListResults> ListAsync(string? filterstring)
=> ListAsync(filterstring == null ? null : new[] { filterstring }, null);
/// <inheritdoc />
public async Task<IListResults> ListAsync(IEnumerable<string>? filterstrings, IFilter? filter)
=> await Rpc.InvokeAsync<IListResults>(nameof(IController.ListAsync), filterstrings, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IDeleteResults> DeleteAsync()
=> await Rpc.InvokeAsync<IDeleteResults>(nameof(IController.DeleteAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IRepairResults> RepairAsync(IFilter? filter = null)
=> await Rpc.InvokeAsync<IRepairResults>(nameof(IController.RepairAsync), filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ICompactResults> CompactAsync()
=> await Rpc.InvokeAsync<ICompactResults>(nameof(IController.CompactAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IVacuumResults> VacuumAsync()
=> await Rpc.InvokeAsync<IVacuumResults>(nameof(IController.VacuumAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ITestResults> TestAsync(long samples = 1)
=> await Rpc.InvokeAsync<ITestResults>(nameof(IController.TestAsync), samples).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListFilesetResults> ListFilesetsAsync()
=> await Rpc.InvokeAsync<IListFilesetResults>(nameof(IController.ListFilesetsAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListFolderResults> ListFolderAsync(string[]? folders, long offset, long limit, bool extendedData)
=> await Rpc.InvokeAsync<IListFolderResults>(nameof(IController.ListFolderAsync), folders, offset, limit, extendedData).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListFileVersionsResults> ListFileVersionsAsync(string[]? files, long offset, long limit)
=> await Rpc.InvokeAsync<IListFileVersionsResults>(nameof(IController.ListFileVersionsAsync), files, offset, limit).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ISearchFilesResults> SearchEntriesAsync(string[]? pathprefixes, IFilter? filter, bool caseSensitive, long offset, long limit, bool extendedData)
=> await Rpc.InvokeAsync<ISearchFilesResults>(nameof(IController.SearchEntriesAsync), pathprefixes, filter, caseSensitive, offset, limit, extendedData).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ICreateLogDatabaseResults> CreateLogDatabaseAsync(string targetpath)
=> await Rpc.InvokeAsync<ICreateLogDatabaseResults>(nameof(IController.CreateLogDatabaseAsync), targetpath).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListRemoteResults> ListRemoteAsync()
=> await Rpc.InvokeAsync<IListRemoteResults>(nameof(IController.ListRemoteAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListRemoteResults> DeleteAllRemoteFilesAsync()
=> await Rpc.InvokeAsync<IListRemoteResults>(nameof(IController.DeleteAllRemoteFilesAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ISetLockResults> SetLocksAsync()
=> await Rpc.InvokeAsync<ISetLockResults>(nameof(IController.SetLocksAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IReadLockInfoResults> ReadLockInfoAsync()
=> await Rpc.InvokeAsync<IReadLockInfoResults>(nameof(IController.ReadLockInfoAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IRecreateDatabaseResults> UpdateDatabaseWithVersionsAsync(IFilter? filter = null)
=> await Rpc.InvokeAsync<IRecreateDatabaseResults>(nameof(IController.UpdateDatabaseWithVersionsAsync), filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IPurgeFilesResults> PurgeFilesAsync(IFilter? filter)
=> await Rpc.InvokeAsync<IPurgeFilesResults>(nameof(IController.PurgeFilesAsync), filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListBrokenFilesResults> ListBrokenFilesAsync(IFilter? filter, Func<long, DateTime, long, string, long, bool>? callbackhandler = null)
{
// Note: callbacks for progress are not supported in the RPC model
return await Rpc.InvokeAsync<IListBrokenFilesResults>(nameof(IController.ListBrokenFilesAsync), filter).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IPurgeBrokenFilesResults> PurgeBrokenFilesAsync(IFilter? filter)
=> await Rpc.InvokeAsync<IPurgeBrokenFilesResults>(nameof(IController.PurgeBrokenFilesAsync), filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ISendMailResults> SendMailAsync()
=> await Rpc.InvokeAsync<ISendMailResults>(nameof(IController.SendMailAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ISystemInfoResults> SystemInfoAsync()
=> await Rpc.InvokeAsync<ISystemInfoResults>(nameof(IController.SystemInfoAsync)).ConfigureAwait(false);
/// <inheritdoc />
public async Task<ITestFilterResults> TestFilterAsync(string[] paths, IFilter? filter = null)
=> await Rpc.InvokeAsync<ITestFilterResults>(nameof(IController.TestFilterAsync), paths, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListChangesResults> ListChangesAsync(string baseVersion, string targetVersion, IEnumerable<string>? filterstrings = null, IFilter? filter = null, Action<IListChangesResults, IEnumerable<Tuple<ListChangesChangeType, ListChangesElementType, string>>>? callback = null)
=> await Rpc.InvokeAsync<IListChangesResults>(nameof(IController.ListChangesAsync), baseVersion, targetVersion, filterstrings, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListAffectedResults> ListAffectedAsync(List<string>? args, Action<IListAffectedResults>? callback = null)
=> await Rpc.InvokeAsync<IListAffectedResults>(nameof(IController.ListAffectedAsync), args).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IRestoreControlFilesResults> RestoreControlFilesAsync(IEnumerable<string>? files = null, IFilter? filter = null)
=> await Rpc.InvokeAsync<IRestoreControlFilesResults>(nameof(IController.RestoreControlFilesAsync), files, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task<IListResults> ListControlFilesAsync(IEnumerable<string>? filterstrings, IFilter? filter)
=> await Rpc.InvokeAsync<IListResults>(nameof(IController.ListControlFilesAsync), filterstrings, filter).ConfigureAwait(false);
/// <inheritdoc />
public async Task AbortAsync()
{
if (_rpc != null)
{
try
{
await _rpc.InvokeAsync(nameof(IController.AbortAsync)).WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
catch
{
// Ignore RPC errors during abort
}
}
// Force kill the process
try
{
if (_process != null && !_process.HasExited)
{
_process.Kill();
_process.WaitForExit(5000);
}
}
catch
{
// Process may already be dead
}
}
/// <inheritdoc />
public async Task StopAsync()
{
if (_rpc != null)
await _rpc.InvokeAsync(nameof(IController.StopAsync)).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task PauseAsync(bool alsoTransfers)
{
if (_rpc != null)
await _rpc.InvokeAsync(nameof(IController.PauseAsync), alsoTransfers).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task ResumeAsync()
{
if (_rpc != null)
await _rpc.InvokeAsync(nameof(IController.ResumeAsync)).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task SetThrottleSpeedsAsync(long maxUploadPrSecond, long maxDownloadPrSecond)
{
if (_rpc != null)
await _rpc.InvokeAsync(nameof(IController.SetThrottleSpeedsAsync), maxUploadPrSecond, maxDownloadPrSecond).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task SetSecretProviderAsync(ISecretProvider? secretProvider)
{
if (_rpc != null)
await _rpc.InvokeAsync(nameof(IController.SetSecretProviderAsync), secretProvider).ConfigureAwait(false);
}
/// <inheritdoc />
public Task AppendSinkAsync(IMessageSink sink)
{
// In out-of-process mode, additional sinks cannot be added dynamically
// This is a limitation of the RPC model
Library.Logging.Log.WriteWarningMessage(LOGTAG, "AppendSinkNotSupported", null, "AppendSink is not supported in out-of-process mode");
return Task.CompletedTask;
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
try
{
if (_rpc != null)
{
try
{
_rpc.InvokeAsync("DisposeAsync").WaitAsync(TimeSpan.FromSeconds(5)).Await();
}
catch
{
// Ignore disposal errors
}
_rpc.Dispose();
}
}
catch { }
try
{
if (_process != null && !_process.HasExited)
{
_process.Kill();
_process.WaitForExit(5000);
}
}
catch { }
_process?.Dispose();
}
#endregion
private OperationProgressWrapper? _operationProgressWrapper;
/// <summary>
/// Wrapper for backend progress DTO that implements IBackendProgress
/// </summary>
private class BackendProgressWrapper : IBackendProgress
{
private readonly BackendProgressDto _dto;
public BackendProgressWrapper(BackendProgressDto dto) => _dto = dto;
public BackendActionProgress[] GetActiveTransfers()
=> _dto.Transfers?.Select(t => new BackendActionProgress(t.Action, t.Path, t.Size, t.Progress, t.BytesPerSecond, t.IsBlocking)).ToArray() ?? [];
}
/// <summary>
/// Wrapper for operation progress DTO that implements IOperationProgress
/// </summary>
private class OperationProgressWrapper : IOperationProgress
{
private OperationPhase _phase;
private float _progress;
private string? _currentFilename;
private long _currentFileSize;
private long _currentFileOffset;
private bool _currentFileComplete;
private long _filesProcessed;
private long _fileSizeProcessed;
private long _fileCount;
private long _fileSize;
private bool _countingFiles;
private int _remoteSyncDestinationIndex;
private int _remoteSyncDestinationCount;
public event PhaseChangedDelegate? PhaseChanged;
public void UpdateFromDto(OperationProgressDto dto)
{
_phase = dto.Phase;
_progress = dto.Progress;
_filesProcessed = dto.FilesProcessed;
_fileSizeProcessed = dto.FileSizeProcessed;
_fileCount = dto.FileCount;
_fileSize = dto.FileSize;
_countingFiles = dto.CountingFiles;
_currentFilename = dto.CurrentFilename;
_currentFileSize = dto.CurrentFileSize;
_currentFileOffset = dto.CurrentFileOffset;
_currentFileComplete = dto.CurrentFileComplete;
_remoteSyncDestinationIndex = dto.RemoteSyncDestinationIndex;
_remoteSyncDestinationCount = dto.RemoteSyncDestinationCount;
}
public void SetPhase(OperationPhase phase, OperationPhase previousPhase)
{
_phase = phase;
PhaseChanged?.Invoke(phase, previousPhase);
}
public void UpdateOverall(out OperationPhase phase, out float progress, out long filesprocessed, out long filesizeprocessed, out long filecount, out long filesize, out bool countingfiles)
{
phase = _phase;
progress = _progress;
filesprocessed = _filesProcessed;
filesizeprocessed = _fileSizeProcessed;
filecount = _fileCount;
filesize = _fileSize;
countingfiles = _countingFiles;
}
public void UpdateFile(out string filename, out long filesize, out long fileoffset, out bool filecomplete)
{
filename = _currentFilename ?? string.Empty;
filesize = _currentFileSize;
fileoffset = _currentFileOffset;
filecomplete = _currentFileComplete;
}
public void UpdateRemoteSyncDestination(out int destinationIndex, out int destinationCount)
{
destinationIndex = _remoteSyncDestinationIndex;
destinationCount = _remoteSyncDestinationCount;
}
}
}