onebeyond/onebeyond-studio-file-storage

View on GitHub
src/OneBeyond.Studio.FileStorage.Azure/Helpers/ContainerHelper.cs

Summary

Maintainability
A
1 hr
Test Coverage
using System;
using System.Text.RegularExpressions;
using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;
using Azure.Storage.Sas;
using EnsureThat;
using Nito.AsyncEx;
using OneBeyond.Studio.FileStorage.Azure.Exceptions;
using OneBeyond.Studio.FileStorage.Azure.Options;
using OneBeyond.Studio.FileStorage.Domain;

namespace OneBeyond.Studio.FileStorage.Azure.Helpers;

internal static partial class ContainerHelper
{
    public static AsyncLazy<BlobContainerClient> CreateBlobContainerClient(
        AzureBaseStorageOptions options)
    {
        EnsureArg.IsNotNull(options, nameof(options));
        ValidateOptions(options);

        var blobServiceClient = string.IsNullOrWhiteSpace(options.AccountName)
            ? new BlobServiceClient(options.ConnectionString)
            : new BlobServiceClient(new Uri($"https://{options.AccountName}.blob.core.windows.net"), new DefaultAzureCredential());
        
        return new AsyncLazy<BlobContainerClient>(
            async () =>
            {
                var containerClient = blobServiceClient.GetBlobContainerClient(options.ContainerName!);
                await containerClient.CreateIfNotExistsAsync().ConfigureAwait(false);
                return containerClient;
            },
            AsyncLazyFlags.RetryOnFailure);
    }

    public static Uri GetSharedAccessUriFromContainer(
        string blobFileId,
        CloudStorageAction action,
        BlobContainerClient containerClient,
        TimeSpan sharedAccessDuration)
    {
        BlobHelper.ValidateBlobName(blobFileId);

        var escapedBlobName = Uri.EscapeDataString(blobFileId);
        var blobClient = containerClient.GetBlobClient(escapedBlobName);

        if (!blobClient.CanGenerateSasUri)
        {
            throw new AzureStorageException("BlobClient must be authorized with Shared Key credentials to create a service SAS.");
        }

        var startsOn = DateTime.UtcNow;

        var endsOn = startsOn.Add(sharedAccessDuration);
        var sasBuilder = new BlobSasBuilder
        {
            BlobContainerName = blobClient.GetParentBlobContainerClient().Name,
            BlobName = blobClient.Name,
            StartsOn = startsOn,
            ExpiresOn = endsOn,
            Resource = "b",
        };

        switch (action)
        {
            case CloudStorageAction.Download:
                sasBuilder.SetPermissions(BlobAccountSasPermissions.Read);
                break;
            case CloudStorageAction.Upload:
                sasBuilder.SetPermissions(BlobContainerSasPermissions.Add | BlobContainerSasPermissions.Write | BlobContainerSasPermissions.Create);
                break;
            case CloudStorageAction.Delete:
                sasBuilder.SetPermissions(BlobAccountSasPermissions.Delete);
                break;
        }

        return blobClient.GenerateSasUri(sasBuilder);
    }

    private static void ValidateOptions(AzureBaseStorageOptions options)
    {
        if (string.IsNullOrWhiteSpace(options.ConnectionString) && string.IsNullOrWhiteSpace(options.AccountName))
        {
            throw new ArgumentException("At least one connection must be provided, " +
                "either the connection string or the account name (for Azure Identity usage).");
        }

        if (!string.IsNullOrWhiteSpace(options.ConnectionString) && !string.IsNullOrWhiteSpace(options.AccountName))
        {
            throw new ArgumentException("Only one connection can be provided, " +
                "either the connection string or the account name (for Azure Identity usage).");
        }

        ValidateContainerName(options.ContainerName);
    }

    /// <summary>
    /// This is designed to improve information about what is and is not valid for a given azure container name.
    /// </summary>
    private static void ValidateContainerName(string? containerName)
    {
        if (string.IsNullOrWhiteSpace(containerName))
        {
            throw new AzureStorageException("Container name cannot be empty.");
        }

        if (containerName.Length < 3 || containerName.Length > 63)
        {
            throw new AzureStorageException("Container name must be between 3 and 63 characters in length.");
        }

        if (!ContainerCharacterRegex().IsMatch(containerName))
        {
            throw new AzureStorageException("Container name can only contain lowercase letters, numbers or hyphens.");
        }

        if (!ContainerFullValidityRegex().IsMatch(containerName))
        {
            throw new AzureStorageException("Container name must start and end with a number or letter and cannot contain multiple hyphens in sequence.");
        }
    }

    [GeneratedRegex("^[a-z0-9-]*$", RegexOptions.Compiled)]
    private static partial Regex ContainerCharacterRegex();

    [GeneratedRegex("^[a-z0-9](?!.*--)[a-z0-9-]{1,61}[a-z0-9]$", RegexOptions.Compiled)]
    private static partial Regex ContainerFullValidityRegex();
}