2025-01-28 08:54:50 +01:00
using System ;
using System.Collections.Generic ;
using System.Data ;
using System.Linq ;
using System.Runtime.CompilerServices ;
using System.Threading ;
using System.Threading.Tasks ;
using CoCoL ;
using Duplicati.Library.Interface ;
using Duplicati.Library.Main.Database ;
using Duplicati.Library.Main.Operation.Common ;
using Duplicati.Library.Main.Volumes ;
using Duplicati.Library.Utility ;
2025-04-22 22:54:48 +02:00
using Duplicati.StreamUtil ;
2025-01-28 08:54:50 +01:00
namespace Duplicati.Library.Main.Backend ;
#nullable enable
/// <summary>
/// The backend manager
/// </summary>
internal partial class BackendManager : IBackendManager
{
/// <summary>
/// The log tag for the class
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < BackendManager >();
2025-02-18 09:10:13 +01:00
2025-01-28 08:54:50 +01:00
/// <summary>
/// The channel for issuing and handling requests
/// </summary>
private readonly IChannel < PendingOperationBase > requestChannel = ChannelManager . CreateChannel < PendingOperationBase >( name : "BackendManager" );
/// <summary>
/// The queue runner task
/// </summary>
private readonly Task queueRunner ;
/// <summary>
/// The execution context
/// </summary>
private readonly ExecuteContext context ;
2025-12-14 17:17:48 +01:00
/// <summary>
/// Indicates whether the backend supports object locking
/// </summary>
private readonly Lazy < bool > supportsObjectLocking ;
2025-01-28 08:54:50 +01:00
/// <summary>
/// Flag keeping track of whether the object has been disposed
/// </summary>
private bool isDisposed = false ;
/// <summary>
/// Initializes a new instance of the <see cref="BackendManager"/> class.
/// </summary>
/// <param name="backendUrl">The backend URL</param>
/// <param name="options">The options</param>
/// <param name="backendWriter">The backend writer</param>
/// <param name="taskReader">The task reader</param>
public BackendManager ( string backendUrl , Options options , IBackendWriter backendWriter , ITaskReader taskReader )
{
if ( string . IsNullOrWhiteSpace ( backendUrl ))
throw new ArgumentNullException ( nameof ( backendUrl ));
2025-05-20 19:06:35 +02:00
var isThrottleDisabled = options . DisableThrottle || options . ThrottleDisabledBackends . Contains ( Library . Utility . Utility . GuessScheme ( backendUrl ) ?? string . Empty );
2025-01-28 08:54:50 +01:00
// To avoid excessive parameter passing, the context is captured here
context = new ExecuteContext (
2025-04-22 22:54:48 +02:00
new ProgressHandler ( backendWriter , taskReader ),
2025-01-28 08:54:50 +01:00
backendWriter ?? throw new ArgumentNullException ( nameof ( backendWriter )),
new DatabaseCollector (),
2025-05-20 19:06:35 +02:00
new ThrottleManager () { Limit = isThrottleDisabled ? 0 : options . MaxUploadPrSecond },
new ThrottleManager () { Limit = isThrottleDisabled ? 0 : options . MaxDownloadPrSecond },
2025-01-28 08:54:50 +01:00
taskReader ?? throw new ArgumentNullException ( nameof ( taskReader )),
2025-05-20 19:06:35 +02:00
isThrottleDisabled ,
2025-01-28 08:54:50 +01:00
options ?? throw new ArgumentNullException ( nameof ( options ))
);
2025-12-14 17:17:48 +01:00
supportsObjectLocking = new Lazy < bool >(() =>
{
var backend = DynamicLoader . BackendLoader . GetBackend ( backendUrl , context . Options . RawOptions );
if ( backend == null )
return false ;
try
{
return backend is ILockingBackend ;
}
finally
{
( backend as IDisposable )?. Dispose ();
}
}, isThreadSafe : true );
2025-01-28 08:54:50 +01:00
// The BackendManager class is a wrapper that essentially sends
// requests into a queue and processes them in order.
// The Handler class is the one that actually processes the requests.
queueRunner = Handler . RunHandlerAsync (
requestChannel ,
backendUrl ,
context );
}
2025-12-14 17:17:48 +01:00
/// <summary>
/// Gets a value indicating whether the configured backend supports object locking
/// </summary>
public bool SupportsObjectLocking => supportsObjectLocking . Value ;
2025-01-28 08:54:50 +01:00
/// <summary>
/// Enters a task into the queue for processing.
/// </summary>
/// <param name="op">The operation to queue</param>
/// <returns>An awaitable task</returns>
private async Task QueueTask ( PendingOperationBase op )
{
if ( queueRunner . IsCompleted )
{
if ( queueRunner . IsFaulted )
await queueRunner . ConfigureAwait ( false );
if ( queueRunner . IsCanceled )
throw new OperationCanceledException ( "Backend manager is stopped" , queueRunner . Exception );
throw new InvalidOperationException ( "Backend manager is stopped" );
}
try
{
await requestChannel . WriteAsync ( op ). ConfigureAwait ( false );
}
catch ( RetiredException ex )
{
// Try to get a better error message
if ( queueRunner . IsFaulted )
await queueRunner . ConfigureAwait ( false );
throw new InvalidOperationException ( "Backend manager is stopped" , ex );
}
}
/// <summary>
/// Calculates the hash of a file
/// </summary>
/// <param name="filename">The filename</param>
/// <param name="options">The options</param>
/// <returns>The hash</returns>
protected static string CalculateFileHash ( string filename , Options options )
{
using ( var fs = System . IO . File . OpenRead ( filename ))
using ( var hasher = HashFactory . CreateHasher ( options . FileHashAlgorithm ))
return Convert . ToBase64String ( hasher . ComputeHash ( fs ));
}
2025-02-19 16:08:18 +01:00
/// <summary>
/// Decrypts a file using the specified options
/// </summary>
/// <param name="tmpfile">The file to decrypt</param>
/// <param name="filename">The name of the file. Used for detecting encryption algorithm if not specified in options or if it differs from the options</param>
/// <param name="options">The Duplicati options</param>
/// <returns>The decrypted file</returns>
public TempFile DecryptFile ( TempFile volume , string volume_name , Options options )
{
return GetOperation . DecryptFile ( volume , volume_name , options );
}
2025-01-28 08:54:50 +01:00
/// <summary>
/// Deletes a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="size">The size of the remote file, for statistics</param>
/// <param name="waitForComplete">True if the operation should wait for the file to actually be deleted. If this argument is false, the task will complete once the operation is queued</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task DeleteAsync ( string remotename , long size , bool waitForComplete , CancellationToken cancelToken )
{
var op = new DeleteOperation ( remotename , size , context , waitForComplete , cancelToken );
await QueueTask ( op ). ConfigureAwait ( false );
await op . GetResult (). ConfigureAwait ( false );
}
2025-12-14 17:17:48 +01:00
/// <summary>
/// Applies or updates an object lock on a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="lockUntilUtc">The UTC time until which the lock should remain in effect</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task SetObjectLockUntilAsync ( string remotename , DateTime lockUntilUtc , CancellationToken cancelToken )
{
var op = new SetObjectLockOperation ( remotename , lockUntilUtc , context , cancelToken );
await QueueTask ( op ). ConfigureAwait ( false );
await op . GetResult (). ConfigureAwait ( false );
}
/// <summary>
/// Gets the object lock expiration time for a remote file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The UTC time the lock expires, or null if no lock is set</returns>
public async Task < DateTime ?> GetObjectLockUntilAsync ( string remotename , CancellationToken cancelToken )
{
var op = new GetObjectLockOperation ( remotename , context , cancelToken );
await QueueTask ( op ). ConfigureAwait ( false );
return await op . GetResult (). ConfigureAwait ( false );
}
2025-01-28 08:54:50 +01:00
/// <summary>
/// Gets a file from the remote location
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, for verification</param>
/// <param name="size">The size of the remote file, for verification</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A temporary file with the contents of the remote file</returns>
public async Task < TempFile > GetAsync ( string remotename , string hash , long size , CancellationToken cancelToken )
{
var op = new GetOperation ( remotename , size , context , cancelToken )
{
2025-02-19 16:11:55 +01:00
Hash = hash ,
Decrypt = true
2025-01-28 08:54:50 +01:00
};
await QueueTask ( op ). ConfigureAwait ( false );
2025-04-23 15:39:32 +02:00
( var file , var _ , var _ ) = await op . GetResult (). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
return file ;
}
2025-02-19 16:11:55 +01:00
/// <summary>
/// Gets a file from the remote location without decrypting it
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, for verification</param>
/// <param name="size">The size of the remote file, for verification</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A temporary file with the contents of the remote file</returns>
public async Task < TempFile > GetDirectAsync ( string remotename , string hash , long size , CancellationToken cancelToken )
2025-02-18 09:10:13 +01:00
{
2025-02-19 16:11:55 +01:00
var op = new GetOperation ( remotename , size , context , cancelToken )
{
Hash = hash ,
Decrypt = false
};
await QueueTask ( op ). ConfigureAwait ( false );
2025-04-23 15:39:32 +02:00
( var file , var _ , var _ ) = await op . GetResult (). ConfigureAwait ( false );
2025-02-19 16:11:55 +01:00
return file ;
2025-02-18 09:10:13 +01:00
}
2025-01-28 08:54:50 +01:00
/// <summary>
/// Gets quota information from the backend
/// </summary>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The quota information</returns>
public async Task < IQuotaInfo ?> GetQuotaInfoAsync ( CancellationToken cancelToken )
{
var op = new QuotaInfoOperation ( context , cancelToken );
await QueueTask ( op ). ConfigureAwait ( false );
return await op . GetResult (). ConfigureAwait ( false );
}
/// <summary>
/// Gets a file from the remote location, along with the hash and size of the file
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="hash">The hash of the remote file, or null if not known</param>
/// <param name="size">The size of the remote file, or -1 if not known</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>A tuple containing the temporary file, the hash of the file, and the size of the file</returns>
public async Task <( TempFile File , string Hash , long Size )> GetWithInfoAsync ( string remotename , string hash , long size , CancellationToken cancelToken )
{
var op = new GetOperation ( remotename , size , context , cancelToken )
{
2025-02-19 16:11:55 +01:00
Hash = hash ,
Decrypt = true
2025-01-28 08:54:50 +01:00
};
await QueueTask ( op ). ConfigureAwait ( false );
( var file , var downloadHash , var downloadSize ) = await op . GetResult (). ConfigureAwait ( false );
return ( file , downloadHash , downloadSize );
}
/// <summary>
/// Lists files on the remote destination
/// </summary>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The list of files</returns>
public async Task < IEnumerable < Interface . IFileEntry >> ListAsync ( CancellationToken cancelToken )
{
var op = new ListOperation ( context , cancelToken );
await QueueTask ( op ). ConfigureAwait ( false );
return await op . GetResult (). ConfigureAwait ( false );
}
/// <summary>
/// Uploads a volume to the remote location
/// </summary>
/// <param name="volume">The volume to upload</param>
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The callback to call when the index volume is finished</param>
/// <param name="waitForComplete">True if the operation should wait for the file to actually be uploaded. If this argument is false, the task will complete once the operation is queued</param>
2025-04-23 12:11:16 +02:00
/// <param name="onDbUpdate">The callback to call when the database is updated</param>
2025-01-28 08:54:50 +01:00
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
2025-06-23 10:27:09 +02:00
public async Task PutAsync ( VolumeWriterBase volume , IndexVolumeWriter ? indexVolume , Func < Task >? indexVolumeFinished , bool waitForComplete , Func < Task >? onDbUpdate , CancellationToken cancelToken )
2025-01-28 08:54:50 +01:00
{
volume . Close ();
var op = new PutOperation ( volume . RemoteFilename , context , waitForComplete , cancelToken )
{
LocalTempfile = volume . TempFile ,
OriginalIndexFile = indexVolume ,
Unencrypted = false ,
TrackedInDb = true ,
2025-04-23 12:11:16 +02:00
IndexVolumeFinishedCallback = indexVolumeFinished ,
OnDbUpdate = onDbUpdate ?? PutOperation . OnDbUpdateDefault ,
2025-01-28 08:54:50 +01:00
};
// Prepare encryption
op . StartEncryptionAndHashing ();
await QueueTask ( op ). ConfigureAwait ( false );
await op . GetResult (). ConfigureAwait ( false );
}
/// <summary>
/// Uploads a verification file to the remote location without encryption
/// </summary>
/// <param name="remotename">The name of the remote file</param>
/// <param name="tempFile">The temporary file to upload</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task PutVerificationFileAsync ( string remotename , TempFile tempFile , CancellationToken cancelToken )
{
var op = new PutOperation ( remotename , context , true , cancelToken )
{
LocalTempfile = tempFile ,
Unencrypted = true , // Avoid encrypting
TrackedInDb = false , // Not tracked
OriginalIndexFile = null ,
2025-04-23 12:11:16 +02:00
IndexVolumeFinishedCallback = null ,
OnDbUpdate = PutOperation . OnDbUpdateDefault ,
2025-01-28 08:54:50 +01:00
};
// Sets the task as already completed
op . StartEncryptionAndHashing ();
await QueueTask ( op ). ConfigureAwait ( false );
await op . GetResult (). ConfigureAwait ( false );
}
/// <summary>
2025-06-17 09:59:22 +02:00
/// Flushes the database messages to the database.
2025-01-28 08:54:50 +01:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="database">The database to write to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that completes when the messages are flushed.</returns>
2025-05-22 08:07:23 +02:00
public async Task FlushPendingMessagesAsync ( LocalDatabase database , CancellationToken cancellationToken )
2025-01-28 08:54:50 +01:00
{
2025-06-19 11:04:09 +02:00
await context . Database . FlushPendingMessages ( database , cancellationToken ). ConfigureAwait ( false );
2025-04-23 12:11:16 +02:00
}
2025-01-28 08:54:50 +01:00
2025-04-23 13:44:46 +02:00
/// <summary>
2025-06-17 09:59:22 +02:00
/// Waits for the backend queue to be empty.
2025-04-23 13:44:46 +02:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An awaitable task.</returns>
2025-04-23 13:44:46 +02:00
public async Task WaitForEmptyAsync ( CancellationToken cancellationToken )
{
2025-01-28 08:54:50 +01:00
var op = new WaitForEmptyOperation ( context , cancellationToken );
await QueueTask ( op ). ConfigureAwait ( false );
await op . GetResult (). ConfigureAwait ( false );
2025-04-23 13:44:46 +02:00
}
2025-01-28 08:54:50 +01:00
/// <summary>
/// Waits for the backend queue to be empty and flushes the database messages
/// </summary>
2025-04-23 12:11:16 +02:00
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
2025-01-28 08:54:50 +01:00
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
2025-05-22 08:07:23 +02:00
public async Task WaitForEmptyAsync ( LocalDatabase database , CancellationToken cancellationToken )
2025-01-28 08:54:50 +01:00
{
2025-05-22 08:07:23 +02:00
await FlushPendingMessagesAsync ( database , cancellationToken ). ConfigureAwait ( false );
2025-04-23 13:44:46 +02:00
await WaitForEmptyAsync ( cancellationToken ). ConfigureAwait ( false );
2025-05-22 08:07:23 +02:00
await FlushPendingMessagesAsync ( database , cancellationToken ). ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
}
/// <summary>
2025-06-17 09:59:22 +02:00
/// Stops the backend manager and flushes any pending messages to the database.
2025-01-28 08:54:50 +01:00
/// </summary>
2025-06-17 09:59:22 +02:00
/// <param name="database">The database to write pending messages to.</param>
/// <returns>A task that completes when the runner is stopped and messages are flushed.</returns>
2025-05-22 08:07:23 +02:00
public async Task StopRunnerAndFlushMessages ( LocalDatabase database )
2025-01-28 08:54:50 +01:00
{
await requestChannel . RetireAsync (). ConfigureAwait ( false );
2025-05-22 08:07:23 +02:00
await FlushPendingMessagesAsync ( database , CancellationToken . None ). ConfigureAwait ( false );
2025-04-23 12:11:16 +02:00
2025-01-28 08:54:50 +01:00
if ( queueRunner . IsFaulted )
Logging . Log . WriteWarningMessage ( LOGTAG , "BackendManagerShutdown" , queueRunner . Exception , "Backend manager queue runner crashed" );
}
/// <summary>
/// Stops the backend manager and discards any pending messages
/// </summary>
public void StopRunnerAndDiscardMessages ()
{
requestChannel . RetireAsync (). Await ();
if ( queueRunner . IsFaulted )
Logging . Log . WriteWarningMessage ( LOGTAG , "BackendManagerShutdown" , queueRunner . Exception , "Backend manager queue runner crashed" );
context . Database . ClearPendingMessages ();
}
/// <summary>
/// Performs a download of the files specified, with pre-fetch to overlap the download and processing
/// </summary>
/// <param name="volumes">The volumes to download</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The downloaded files and the volume they came from</returns>
public async IAsyncEnumerable <( TempFile File , string Hash , long Size , string Name )> GetFilesOverlappedAsync ( IEnumerable < IRemoteVolume > volumes , [ EnumeratorCancellation ] CancellationToken cancelToken )
{
var prevVolume = volumes . FirstOrDefault ();
if ( prevVolume == null )
yield break ;
// Get the first volume, so we do not have pending parallel transfers
2025-06-12 08:34:29 +02:00
var prevResult = await GetWithInfoAsync ( prevVolume . Name , prevVolume . Hash , prevVolume . Size , cancelToken )
. ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
foreach ( var volume in volumes . Skip ( 1 ))
{
// Prepare the next volume, while processing the previous one
var nextTask = GetWithInfoAsync ( volume . Name , volume . Hash , volume . Size , cancelToken );
// Assuming we do not throw while yielding, otherwise we would need to dispose nextTask
yield return ( prevResult . File , prevResult . Hash , prevResult . Size , prevVolume . Name );
prevResult . File . Dispose ();
// Set up for next iteration
prevVolume = volume ;
2025-06-12 08:34:29 +02:00
prevResult = await nextTask . ConfigureAwait ( false );
2025-01-28 08:54:50 +01:00
}
// Return the last result
yield return ( prevResult . File , prevResult . Hash , prevResult . Size , prevVolume . Name );
prevResult . File . Dispose ();
}
2025-04-22 22:54:48 +02:00
/// <summary>
/// Updates the throttle values for upload and download
/// </summary>
/// <param name="maxUploadPrSecond">The maximum upload speed in bytes per second</param>
/// <param name="maxDownloadPrSecond">The maximum download speed in bytes per second</param>
public void UpdateThrottleValues ( long maxUploadPrSecond , long maxDownloadPrSecond )
{
2025-05-20 19:06:35 +02:00
if ( context . IsThrottleDisabled )
return ;
2025-04-22 22:54:48 +02:00
context . UploadThrottleManager . Limit = maxUploadPrSecond ;
context . DownloadThrottleManager . Limit = maxDownloadPrSecond ;
}
2025-01-28 08:54:50 +01:00
/// <summary>
/// Disposes the backend manager
/// </summary>
public void Dispose ()
{
if ( isDisposed )
return ;
isDisposed = true ;
requestChannel . RetireAsync (). Await ();
context . Database . FlushMessagesToLog ();
if (! queueRunner . IsCompleted )
{
Task . WhenAny ( queueRunner , Task . Delay ( 1000 )). Await ();
if (! queueRunner . IsCompleted )
Logging . Log . WriteWarningMessage ( LOGTAG , "BackendManagerShutdown" , null , "Backend manager queue runner did not stop" );
if ( queueRunner . IsFaulted )
Logging . Log . WriteWarningMessage ( LOGTAG , "BackendManagerShutdown" , queueRunner . Exception , "Backend manager queue runner crashed" );
}
2025-04-08 11:21:38 -03:00
if ( queueRunner . IsCompleted )
queueRunner . Dispose ();
2025-01-28 08:54:50 +01:00
}
}