2025-08-05 11:07:19 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
2025-08-05 13:05:10 +02:00
|
|
|
using System.Linq;
|
2025-08-05 11:07:19 +02:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Duplicati.Library.Interface;
|
|
|
|
|
using Duplicati.Library.Utility;
|
|
|
|
|
|
|
|
|
|
namespace RemoteSynchronization
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
public class LightWeightBackendManager(string backendUrl, Dictionary<string, string> options, int maxRetries = 3, int retryDelay = 1000, bool autoCreateFolders = false, bool retryWithExponentialBackoff = false) : IDisposable
|
2025-08-05 11:07:19 +02:00
|
|
|
{
|
|
|
|
|
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Program>();
|
|
|
|
|
|
|
|
|
|
public IBackend? _backend = null;
|
2025-08-05 13:05:10 +02:00
|
|
|
|
|
|
|
|
private bool _anyDownloaded = false;
|
|
|
|
|
private bool _anyUploaded = false;
|
2025-08-05 11:07:19 +02:00
|
|
|
private readonly string _backendUrl = backendUrl;
|
|
|
|
|
private int _instantiations = 0;
|
|
|
|
|
private readonly int _maxRetries = maxRetries;
|
|
|
|
|
private Dictionary<string, string> _options = options;
|
2025-08-05 13:05:10 +02:00
|
|
|
private int _retryDelay = retryDelay;
|
2025-08-05 11:07:19 +02:00
|
|
|
private IStreamingBackend? _streamingBackend = null;
|
|
|
|
|
|
|
|
|
|
public Task DeleteAsync(string remotename, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
return RetryWithDelay(
|
|
|
|
|
$"Delete {remotename}",
|
2025-08-05 13:05:10 +02:00
|
|
|
() => _streamingBackend.DeleteAsync(remotename, token),
|
2025-08-05 15:07:52 +02:00
|
|
|
null,
|
|
|
|
|
false,
|
2025-08-05 13:05:10 +02:00
|
|
|
token
|
2025-08-05 11:07:19 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string DisplayName
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
Instantiate();
|
2025-08-05 11:07:19 +02:00
|
|
|
|
|
|
|
|
return _streamingBackend.DisplayName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_streamingBackend?.Dispose();
|
|
|
|
|
_streamingBackend = null;
|
|
|
|
|
_backend?.Dispose();
|
|
|
|
|
_backend = null;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", ex, "Error during Dispose", null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task GetAsync(string remotename, Stream stream, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
return RetryWithDelay(
|
|
|
|
|
$"Get {remotename}",
|
2025-08-05 15:07:52 +02:00
|
|
|
async () =>
|
|
|
|
|
{
|
|
|
|
|
await _streamingBackend.GetAsync(remotename, stream, token).ConfigureAwait(false);
|
|
|
|
|
_anyDownloaded = true;
|
|
|
|
|
},
|
|
|
|
|
stream,
|
|
|
|
|
true,
|
2025-08-05 13:05:10 +02:00
|
|
|
token
|
2025-08-05 11:07:19 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Instantiate()
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
if (_backend != null)
|
|
|
|
|
{
|
|
|
|
|
// If we already have a backend, we can just return.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 11:07:19 +02:00
|
|
|
if (_instantiations < _maxRetries)
|
|
|
|
|
{
|
|
|
|
|
_backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(_backendUrl, _options);
|
|
|
|
|
_streamingBackend = _backend as IStreamingBackend ?? throw new InvalidOperationException("Backend does not support streaming operations.");
|
|
|
|
|
_instantiations++;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Maximum number of backend instantiations reached.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IAsyncEnumerable<IFileEntry> ListAsync(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
Instantiate();
|
2025-08-05 11:07:19 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return _streamingBackend.ListAsync(token);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", ex, "Error during operation: List", null);
|
|
|
|
|
Dispose(); // Dispose current backend and streaming backend
|
2025-08-05 13:05:10 +02:00
|
|
|
|
|
|
|
|
TryRecoverFromException(ex, token).Await();
|
2025-08-05 11:07:19 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task PutAsync(string remotename, Stream stream, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
return RetryWithDelay(
|
|
|
|
|
$"Put {remotename}",
|
2025-08-05 15:07:52 +02:00
|
|
|
async () =>
|
|
|
|
|
{
|
|
|
|
|
await _streamingBackend.PutAsync(remotename, stream, token).ConfigureAwait(false);
|
|
|
|
|
_anyUploaded = true;
|
|
|
|
|
},
|
|
|
|
|
stream,
|
|
|
|
|
false,
|
2025-08-05 13:05:10 +02:00
|
|
|
token
|
2025-08-05 11:07:19 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task RenameAsync(string oldname, string newname, CancellationToken token)
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
Instantiate();
|
2025-08-05 11:07:19 +02:00
|
|
|
|
|
|
|
|
return _backend switch
|
|
|
|
|
{
|
|
|
|
|
IStreamingBackend sb =>
|
|
|
|
|
RetryWithDelay(
|
|
|
|
|
$"Rename {oldname} to {newname}",
|
|
|
|
|
async () =>
|
|
|
|
|
{
|
|
|
|
|
// Download the file, rename it, and delete the old one
|
|
|
|
|
using var downloaded = new MemoryStream();
|
2025-08-05 13:05:10 +02:00
|
|
|
await sb.GetAsync(oldname, downloaded, token).ConfigureAwait(false);
|
|
|
|
|
await sb.PutAsync(newname, downloaded, token).ConfigureAwait(false);
|
|
|
|
|
await sb.DeleteAsync(oldname, token).ConfigureAwait(false);
|
|
|
|
|
_anyUploaded = true;
|
|
|
|
|
_anyDownloaded = true;
|
|
|
|
|
},
|
2025-08-05 15:07:52 +02:00
|
|
|
null,
|
|
|
|
|
false,
|
2025-08-05 13:05:10 +02:00
|
|
|
token
|
2025-08-05 11:07:19 +02:00
|
|
|
),
|
|
|
|
|
IRenameEnabledBackend ireb =>
|
|
|
|
|
RetryWithDelay(
|
|
|
|
|
$"Rename {oldname} to {newname}",
|
2025-08-05 15:07:52 +02:00
|
|
|
async () =>
|
|
|
|
|
{
|
|
|
|
|
await ireb.RenameAsync(oldname, newname, token).ConfigureAwait(false);
|
|
|
|
|
_anyUploaded = true;
|
|
|
|
|
_anyDownloaded = true;
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
false,
|
2025-08-05 13:05:10 +02:00
|
|
|
token
|
2025-08-05 11:07:19 +02:00
|
|
|
),
|
|
|
|
|
_ => throw new InvalidOperationException("Backend does not support renaming."),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 15:07:52 +02:00
|
|
|
private async Task RetryWithDelay(string operationName, Func<Task> action, Stream? stream, bool resetStream, CancellationToken token)
|
2025-08-05 11:07:19 +02:00
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
2025-08-05 13:05:10 +02:00
|
|
|
// This will throw an exception if we've reached the max
|
|
|
|
|
// number of retries.
|
|
|
|
|
Instantiate();
|
2025-08-05 11:07:19 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-05 15:07:52 +02:00
|
|
|
await action();
|
2025-08-05 13:05:10 +02:00
|
|
|
return; // Exit the loop if the action succeeds.
|
2025-08-05 11:07:19 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", ex, "Error during operation: {0}", operationName);
|
2025-08-05 13:05:10 +02:00
|
|
|
Dispose(); // Dispose current backend and streaming backend.
|
|
|
|
|
|
2025-08-05 15:07:52 +02:00
|
|
|
// Reset the stream, as it's in a potentially faulty state.
|
|
|
|
|
stream?.Seek(0, SeekOrigin.Begin);
|
|
|
|
|
if (resetStream)
|
|
|
|
|
stream?.SetLength(0);
|
|
|
|
|
|
2025-08-05 13:05:10 +02:00
|
|
|
// Try to see if we can recover from the error.
|
|
|
|
|
await TryRecoverFromException(ex, token).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<bool> TryCreateFolder(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Instantiate();
|
|
|
|
|
|
|
|
|
|
// Attempt to create the folder
|
|
|
|
|
await _backend.CreateFolderAsync(token).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
return true; // Folder creation succeeded
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Dispose();
|
|
|
|
|
|
|
|
|
|
Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", ex, "Failed to create folder", null);
|
|
|
|
|
|
|
|
|
|
return false; // Folder creation failed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task TryRecoverFromException(Exception ex, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
// Copied from Duplicati.Library.Main.Backend.BackendManager.Handler.
|
|
|
|
|
|
|
|
|
|
// Refresh DNS name if we fail to connect in order to prevent issues with incorrect DNS entries.
|
|
|
|
|
var dnsFailure = ExceptionExtensions.FlattenException(ex)
|
|
|
|
|
.Any(x =>
|
|
|
|
|
(x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure)
|
|
|
|
|
||
|
|
|
|
|
(x is System.Net.Sockets.SocketException sockEx && sockEx.SocketErrorCode == System.Net.Sockets.SocketError.HostNotFound)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (dnsFailure)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Instantiate();
|
|
|
|
|
|
|
|
|
|
foreach (var name in await _backend.GetDNSNamesAsync(token).ConfigureAwait(false) ?? [])
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
|
|
|
System.Net.Dns.GetHostEntry(name);
|
2025-08-05 11:07:19 +02:00
|
|
|
}
|
2025-08-05 13:05:10 +02:00
|
|
|
catch { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var recovered = false;
|
|
|
|
|
|
|
|
|
|
// Check if this was a folder missing exception and we are allowed to autocreate folders
|
|
|
|
|
if (!(_anyDownloaded || _anyUploaded) && autoCreateFolders && ExceptionExtensions.FlattenException(ex).Any(x => x is FolderMissingException))
|
|
|
|
|
{
|
|
|
|
|
if (await TryCreateFolder(token).ConfigureAwait(false))
|
|
|
|
|
recovered = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finally, if we did not recover, wait the specified delay before retrying.
|
|
|
|
|
if (!recovered && _retryDelay > 0)
|
|
|
|
|
{
|
|
|
|
|
if (retryWithExponentialBackoff)
|
|
|
|
|
_retryDelay <<= 1; // Double the delay for exponential backoff
|
|
|
|
|
|
|
|
|
|
await Task.Delay(_retryDelay, token).ConfigureAwait(false);
|
2025-08-05 11:07:19 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|