// 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;
namespace Duplicati.Library.Backend.AzureBlob
{
///
/// Azure blob storage facade.
///
public class AzureBlobWrapper
{
private readonly BlobContainerClient _container;
private readonly TimeoutOptionsHelper.Timeouts _timeouts;
///
/// Gets an array of DNS names associated with the blob container.
///
/// An array of DNS hostnames for the primary and secondary URIs of the container.
public string[] DnsNames
{
get
{
var lst = new List();
if (_container != null && _container.Uri != null) lst.Add(_container.Uri.Host);
return lst.ToArray();
}
}
///
/// Initializes a new instance of the AzureBlobWrapper class.
///
/// The Azure storage account name.
/// The access key for the storage account.
/// The Shared Access Signature (SAS) token for authentication.
/// The name of the blob container.
/// The timeout options.
public AzureBlobWrapper(string accountName, string? accessKey, string? sasToken, string containerName, TimeoutOptionsHelper.Timeouts timeouts)
{
BlobServiceClient blobServiceClient;
if (sasToken != null)
{
var sasUri = new Uri($"https://{accountName}.blob.core.windows.net/?{sasToken}");
blobServiceClient = new BlobServiceClient(sasUri);
}
else
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accessKey};EndpointSuffix=core.windows.net";
blobServiceClient = new BlobServiceClient(connectionString);
}
_container = blobServiceClient.GetBlobContainerClient(containerName);
_timeouts = timeouts;
}
///
/// Creates a new blob container asynchronously and sets its access permissions to private.
///
/// A token to monitor for cancellation requests.
/// A task that represents the asynchronous operation.
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);
}
///
/// Downloads a blob to a stream asynchronously.
///
/// The name of the blob to download.
/// The stream to download the blob to.
/// A token to monitor for cancellation requests.
/// A task that represents the asynchronous download operation.
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);
}
///
/// Uploads a stream to a blob asynchronously.
///
/// The name to give the uploaded blob.
/// The stream containing the data to upload.
/// A token to monitor for cancellation requests.
/// A task that represents the asynchronous upload operation.
public async Task AddFileStream(string keyName, Stream source, CancellationToken cancelToken)
{
var blobClient = _container.GetBlobClient(keyName);
using var timeoutStream = source.ObserveReadTimeout(_timeouts.ReadWriteTimeout, false);
await blobClient.UploadAsync(timeoutStream, true, cancelToken).ConfigureAwait(false);
}
///
/// Deletes a blob if it exists asynchronously.
///
/// The name of the blob to delete.
/// A token to monitor for cancellation requests.
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);
}
///
/// List container files.
///
/// A token to monitor for cancellation requests.
///
/// Thrown when the container is not found
public virtual async IAsyncEnumerable 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;
yield return new FileEntry(
blobName,
blob.Properties.ContentLength ?? 0,
lastModified,
lastModified
);
}
}
}
}
}