mirror of
https://github.com/duplicati/duplicati.git
synced 2025-11-28 03:20:25 +08:00
221 lines
10 KiB
C#
221 lines
10 KiB
C#
// Copyright (C) 2025, 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 Azure.Storage.Blobs;
|
|
using Azure.Storage.Blobs.Models;
|
|
using System.Runtime.CompilerServices;
|
|
using Duplicati.Library.Common.IO;
|
|
using Duplicati.Library.Interface;
|
|
using Uri = System.Uri;
|
|
using Duplicati.Library.Utility.Options;
|
|
using Duplicati.Library.Utility;
|
|
using Azure.Core.Pipeline;
|
|
|
|
namespace Duplicati.Library.Backend.AzureBlob
|
|
{
|
|
/// <summary>
|
|
/// Azure blob storage facade.
|
|
/// </summary>
|
|
public class AzureBlobWrapper
|
|
{
|
|
private readonly BlobContainerClient _container;
|
|
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
|
|
private readonly IReadOnlySet<AccessTier> _archiveClasses;
|
|
private readonly AccessTier? _accessTier;
|
|
|
|
/// <summary>
|
|
/// Gets an array of DNS names associated with the blob container.
|
|
/// </summary>
|
|
/// <returns>An array of DNS hostnames for the primary and secondary URIs of the container.</returns>
|
|
public string[] DnsNames
|
|
{
|
|
get
|
|
{
|
|
var lst = new List<string>();
|
|
if (_container != null && _container.Uri != null) lst.Add(_container.Uri.Host);
|
|
return lst.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the AzureBlobWrapper class.
|
|
/// </summary>
|
|
/// <param name="accountName">The Azure storage account name.</param>
|
|
/// <param name="accessKey">The access key for the storage account.</param>
|
|
/// <param name="sasToken">The Shared Access Signature (SAS) token for authentication.</param>
|
|
/// <param name="containerName">The name of the blob container.</param>
|
|
/// <param name="accessTier">The access tier assigned to blobs on upload.</param>
|
|
/// <param name="archiveClasses">The storage classes that are considered archive classes.</param>
|
|
/// <param name="timeouts">The timeout options.</param>
|
|
/// <param name="maxRetries">The maximum number of retries for Azure operations.</param>
|
|
public AzureBlobWrapper(string accountName, string? accessKey, string? sasToken, string containerName, AccessTier? accessTier, IReadOnlySet<AccessTier> archiveClasses, TimeoutOptionsHelper.Timeouts timeouts, int maxRetries)
|
|
{
|
|
BlobServiceClient blobServiceClient;
|
|
var maxTicks = timeouts.ReadWriteTimeout.Ticks - TimeSpan.FromSeconds(1).Ticks;
|
|
|
|
// If not infinite, then share the timeout across retries
|
|
var delay = maxTicks <= 0
|
|
? TimeSpan.FromSeconds(1)
|
|
: TimeSpan.FromTicks(maxTicks / Math.Max(1, maxRetries));
|
|
|
|
var maxDelay = maxTicks <= 0
|
|
? TimeSpan.FromSeconds(10)
|
|
: TimeSpan.FromTicks(maxTicks);
|
|
|
|
var blobServiceOptions = new BlobClientOptions()
|
|
{
|
|
Retry =
|
|
{
|
|
MaxRetries = maxRetries,
|
|
Mode = Azure.Core.RetryMode.Exponential,
|
|
Delay = delay,
|
|
MaxDelay = maxDelay
|
|
},
|
|
Transport = new HttpClientTransport(new HttpClient { Timeout = Timeout.InfiniteTimeSpan })
|
|
};
|
|
|
|
if (sasToken != null)
|
|
{
|
|
var sasUri = new Uri($"https://{accountName}.blob.core.windows.net/?{sasToken}");
|
|
blobServiceClient = new BlobServiceClient(sasUri, blobServiceOptions);
|
|
}
|
|
else
|
|
{
|
|
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accessKey};EndpointSuffix=core.windows.net";
|
|
blobServiceClient = new BlobServiceClient(connectionString, blobServiceOptions);
|
|
}
|
|
|
|
_accessTier = accessTier;
|
|
_archiveClasses = archiveClasses;
|
|
_container = blobServiceClient.GetBlobContainerClient(containerName);
|
|
_timeouts = timeouts;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new blob container asynchronously and sets its access permissions to private.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async Task AddContainerAsync(CancellationToken cancellationToken)
|
|
{
|
|
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancellationToken, async ct =>
|
|
// Even though PublicAccessType.None is by default, we set it explicitly to highlight it.
|
|
await _container.CreateAsync(PublicAccessType.None, cancellationToken: ct).ConfigureAwait(false)
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Downloads a blob to a stream asynchronously.
|
|
/// </summary>
|
|
/// <param name="keyName">The name of the blob to download.</param>
|
|
/// <param name="target">The stream to download the blob to.</param>
|
|
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
|
/// <returns>A task that represents the asynchronous download operation.</returns>
|
|
public async Task GetFileStreamAsync(string keyName, Stream target, CancellationToken cancellationToken)
|
|
{
|
|
var blobClient = _container.GetBlobClient(keyName);
|
|
using var timeoutStream = target.ObserveWriteTimeout(_timeouts.ReadWriteTimeout, false);
|
|
await blobClient.DownloadToAsync(target, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uploads a stream to a blob asynchronously.
|
|
/// </summary>
|
|
/// <param name="keyName">The name to give the uploaded blob.</param>
|
|
/// <param name="source">The stream containing the data to upload.</param>
|
|
/// <param name="cancelToken">A token to monitor for cancellation requests.</param>
|
|
/// <returns>A task that represents the asynchronous upload operation.</returns>
|
|
public async Task AddFileStream(string keyName, Stream source, CancellationToken cancelToken)
|
|
{
|
|
var blobClient = _container.GetBlobClient(keyName);
|
|
using var timeoutStream = source.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
|
|
var options = new BlobUploadOptions()
|
|
{
|
|
Conditions = null, // Overwrite any existing blob
|
|
AccessTier = _accessTier
|
|
};
|
|
|
|
await blobClient.UploadAsync(timeoutStream, options, cancelToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a blob if it exists asynchronously.
|
|
/// </summary>
|
|
/// <param name="keyName">The name of the blob to delete.</param>
|
|
/// <param name="cancelToken">A token to monitor for cancellation requests.</param>
|
|
public async Task DeleteObjectAsync(string keyName, CancellationToken cancelToken)
|
|
{
|
|
var blobClient = _container.GetBlobClient(keyName);
|
|
await Utility.Utility.WithTimeout(_timeouts.ShortTimeout, cancelToken, async ct =>
|
|
await blobClient.DeleteIfExistsAsync(cancellationToken: ct).ConfigureAwait(false)
|
|
).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// List container files.
|
|
/// </summary>
|
|
/// <param name="cancelToken">A token to monitor for cancellation requests.</param>
|
|
/// <returns></returns>
|
|
/// <exception cref="FolderMissingException">Thrown when the container is not found</exception>
|
|
public virtual async IAsyncEnumerable<IFileEntry> ListContainerEntriesAsync([EnumeratorCancellation] CancellationToken cancelToken)
|
|
{
|
|
await using var blobEnumerator = _container.GetBlobsAsync().GetAsyncEnumerator(cancelToken);
|
|
|
|
while (true)
|
|
{
|
|
bool hasNext;
|
|
|
|
try
|
|
{
|
|
hasNext = await Utility.Utility.WithTimeout(_timeouts.ListTimeout, cancelToken, async ct
|
|
=> await blobEnumerator.MoveNextAsync().ConfigureAwait(false)
|
|
).ConfigureAwait(false);
|
|
}
|
|
catch (Azure.RequestFailedException ex) when (ex.Status == 404)
|
|
{
|
|
throw new FolderMissingException(ex);
|
|
}
|
|
|
|
if (!hasNext) break;
|
|
|
|
cancelToken.ThrowIfCancellationRequested();
|
|
|
|
if (blobEnumerator.Current is { } blob)
|
|
{
|
|
var blobName = Uri.UnescapeDataString(blob.Name.Replace("+", "%2B"));
|
|
var lastModified = blob.Properties.LastModified?.UtcDateTime ?? DateTime.UtcNow;
|
|
var isArchive = blob.Properties.AccessTier.HasValue && _archiveClasses.Contains(blob.Properties.AccessTier.Value);
|
|
|
|
yield return new FileEntry(
|
|
blobName,
|
|
blob.Properties.ContentLength ?? 0,
|
|
lastModified,
|
|
lastModified
|
|
)
|
|
{
|
|
IsArchived = isArchive,
|
|
IsFolder = false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|