Browse Source

dynamic password retrieval via a distributed cache in BunnyClientFactory

pull/22004/head
Suhaib 2 years ago
parent
commit
6d70e78cbd
  1. 4
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj
  2. 10
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs
  3. 97
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs
  4. 229
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs
  5. 153
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs
  6. 10
      framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs

4
framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj

@ -15,10 +15,12 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" />
</ItemGroup>
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BunnyCDN.Net.Storage" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>
</Project>

10
framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs

@ -1,11 +1,17 @@
using Volo.Abp.Modularity;
using System;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
namespace Volo.Abp.BlobStoring.Bunny;
[DependsOn(typeof(AbpBlobStoringModule))]
[DependsOn(
typeof(AbpBlobStoringModule),
typeof(AbpCachingModule))]
public class AbpBlobStoringBunnyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClient();
}
}

97
framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs

@ -12,13 +12,16 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
{
protected IBunnyBlobNameCalculator BunnyBlobNameCalculator { get; }
protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; }
protected IBunnyClientFactory BunnyClientFactory { get; }
public BunnyBlobProvider(
IBunnyBlobNameCalculator bunnyBlobNameCalculator,
IBlobNormalizeNamingService blobNormalizeNamingService)
IBlobNormalizeNamingService blobNormalizeNamingService,
IBunnyClientFactory bunnyClientFactory)
{
BunnyBlobNameCalculator = bunnyBlobNameCalculator;
BlobNormalizeNamingService = blobNormalizeNamingService;
BunnyClientFactory = bunnyClientFactory;
}
public override async Task SaveAsync(BlobProviderSaveArgs args)
@ -27,14 +30,14 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
var containerName = GetContainerName(args);
var blobName = BunnyBlobNameCalculator.Calculate(args);
using var bunnyClient = GetBunnyClient(args);
await ValidateContainerExistsAsync(bunnyClient, containerName, configuration);
await ValidateContainerExistsAsync(containerName, configuration);
if (!args.OverrideExisting && await BlobExistsAsync(bunnyClient, containerName, blobName))
var bunnyStorage = await GetBunnyCDNStorageAsync(args);
if (!args.OverrideExisting && await BlobExistsAsync(bunnyStorage, containerName, blobName))
{
throw new BlobAlreadyExistsException(
$"BLOB '{args.BlobName}' already exists in container '{containerName}'. " +
$"Blob '{args.BlobName}' already exists in container '{containerName}'. " +
$"Set {nameof(args.OverrideExisting)} to true to overwrite.");
}
@ -42,57 +45,53 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
await args.BlobStream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
await bunnyClient.UploadAsync(memoryStream, $"{containerName}/{blobName}");
await bunnyStorage.UploadAsync(memoryStream, $"{containerName}/{blobName}");
}
public override async Task<bool> DeleteAsync(BlobProviderDeleteArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var configuration = args.Configuration.GetBunnyConfiguration();
var containerName = GetContainerName(args);
using var bunnyClient = GetBunnyClient(args);
if (!await BlobExistsAsync(bunnyClient, containerName, blobName))
var bunnyStorage = await GetBunnyCDNStorageAsync(args);
if (!await BlobExistsAsync(bunnyStorage, containerName, blobName))
{
return false;
}
try
{
await bunnyClient.DeleteObjectAsync($"{containerName}/{blobName}");
return true;
return await bunnyStorage.DeleteObjectAsync($"{containerName}/{blobName}");
}
catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404"))
{
return false;
}
}
public override async Task<bool> ExistsAsync(BlobProviderExistsArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var containerName = GetContainerName(args);
var configuration = args.Configuration.GetBunnyConfiguration();
using var bunnyClient = GetBunnyClient(args);
var bunnyStorage = await GetBunnyCDNStorageAsync(args);
return await BlobExistsAsync(bunnyClient, containerName, blobName);
return await BlobExistsAsync(bunnyStorage, containerName, blobName);
}
public override async Task<Stream?> GetOrNullAsync(BlobProviderGetArgs args)
{
var blobName = BunnyBlobNameCalculator.Calculate(args);
var containerName = GetContainerName(args);
var configuration = args.Configuration.GetBunnyConfiguration();
using var bunnyClient = GetBunnyClient(args);
var bunnyStorage = await GetBunnyCDNStorageAsync(args);
if (!await BlobExistsAsync(bunnyClient, containerName, blobName))
if (!await BlobExistsAsync(bunnyStorage, containerName, blobName))
{
return null;
}
try
{
return await bunnyClient.DownloadObjectAsStreamAsync($"{containerName}/{blobName}");
return await bunnyStorage.DownloadObjectAsStreamAsync($"{containerName}/{blobName}");
}
catch (WebException ex) when ((HttpStatusCode)ex.Status == HttpStatusCode.NotFound)
{
@ -100,14 +99,11 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
}
}
protected virtual async Task<bool> BlobExistsAsync(BunnyClient bunnyClient, string containerName, string blobName)
protected virtual async Task<bool> BlobExistsAsync(BunnyCDNStorage bunnyStorage, string containerName, string blobName)
{
try
{
// Combine container name and blob name to create the full path
var fullBlobPath = $"/{containerName}/{blobName}";
// Extract the directory path
var directoryPath = Path.GetDirectoryName(fullBlobPath)?.Replace('\\', '/') + "/";
if (string.IsNullOrWhiteSpace(directoryPath))
@ -115,9 +111,7 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
throw new Exception("Invalid directory path generated from blob name.");
}
var objects = await bunnyClient.GetStorageObjectsAsync(directoryPath);
// Check if the specific blob exists in the returned objects
var objects = await bunnyStorage.GetStorageObjectsAsync(directoryPath);
return objects?.Any(o => o.FullPath == fullBlobPath) == true;
}
catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404"))
@ -129,16 +123,19 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
throw new Exception($"Error while checking blob existence: {ex.Message}", ex);
}
}
protected virtual BunnyClient GetBunnyClient(BlobProviderArgs args)
protected virtual async Task<BunnyCDNStorage> GetBunnyCDNStorageAsync(BlobProviderArgs args)
{
var containerName = GetContainerName(args);
var configuration = args.Configuration.GetBunnyConfiguration();
if (configuration.Region.IsNullOrEmpty())
{
return new BunnyClient(configuration.AccessKey, containerName);
}
return new BunnyClient(configuration.AccessKey, containerName, configuration.Region);
var containerName = GetContainerName(args);
var region = configuration.Region ?? "de";
return await BunnyClientFactory.CreateAsync(
configuration.AccessKey,
containerName,
region);
}
protected virtual string GetContainerName(BlobProviderArgs args)
{
var configuration = args.Configuration.GetBunnyConfiguration();
@ -146,23 +143,25 @@ public class BunnyBlobProvider : BlobProviderBase, ITransientDependency
? args.ContainerName
: BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName!);
}
protected virtual async Task ValidateContainerExistsAsync(
BunnyClient client,
string containerName,
BunnyBlobProviderConfiguration configuration)
string containerName,
BunnyBlobProviderConfiguration configuration
)
{
if (configuration.CreateContainerIfNotExists)
try
{
try
{
await client.EnsureStorageZoneExistsAsync(containerName);
}
catch (BunnyApiException ex)
{
throw new AbpException(
$"Failed to ensure Bunny storage zone '{containerName}' exists: {ex.Message}",
ex);
}
await BunnyClientFactory.EnsureStorageZoneExistsAsync(
configuration.AccessKey,
containerName,
configuration.Region ?? "de",
configuration.CreateContainerIfNotExists);
}
catch (Exception ex)
{
throw new AbpException(
$"Failed to validate storage zone '{containerName}': {ex.Message}",
ex);
}
}
}
}

229
framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClient.cs

@ -1,229 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using BunnyCDN.Net.Storage;
using BunnyCDN.Net.Storage.Models;
namespace Volo.Abp.BlobStoring.Bunny;
public class BunnyClient : IDisposable
{
private readonly string _region;
private readonly string _storageZoneName;
private readonly HttpClient _httpClient;
private static BunnyCDNStorage? BunnyCDNStorage = null;
public BunnyClient(string accessKey, string storageZoneName, string region = "de", HttpMessageHandler? handler = null)
{
if (string.IsNullOrWhiteSpace(accessKey))
{
throw new ArgumentException("Account access key must not be null or empty.", nameof(accessKey));
}
_region = region;
_storageZoneName = storageZoneName;
_httpClient = handler != null ? new HttpClient(handler) : new HttpClient();
_httpClient.Timeout = TimeSpan.FromSeconds(120);
_httpClient.DefaultRequestHeaders.Add("AccessKey", accessKey);
}
public async Task<bool> DoesStorageZoneExistAsync(string zoneName)
{
if (string.IsNullOrWhiteSpace(zoneName))
{
throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName));
}
try
{
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://api.bunny.net/storagezone?page=0&perPage=1000&includeDeleted=false"),
Headers = { { "accept", "application/json" } }
};
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var storageZones = JsonSerializer.Deserialize<List<StorageZone>>(responseBody);
return storageZones?.Any(zone => zone.Name!.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) == true;
}
catch (HttpRequestException ex)
{
throw new BunnyApiException("Error occurred while checking storage zone existence.", ex);
}
catch (JsonException ex)
{
throw new BunnyApiException("Failed to parse the response from Bunny API.", ex);
}
}
// Ensure the storage zone exists, creating it if not found, and return the zone
public async Task<StorageZone> EnsureStorageZoneExistsAsync(string zoneName, string? originUrl = "")
{
try
{
if (!await DoesStorageZoneExistAsync(zoneName))
{
return await CreateStorageZoneAsync(zoneName, originUrl);
}
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://api.bunny.net/storagezone?page=0&perPage=1000&includeDeleted=false"),
Headers = { { "accept", "application/json" } }
};
using var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var storageZones = JsonSerializer.Deserialize<List<StorageZone>>(responseBody);
return storageZones?.FirstOrDefault(zone => zone.Name!.Equals(zoneName, StringComparison.OrdinalIgnoreCase))
?? throw new BunnyApiException($"Storage zone '{zoneName}' not found even though it should exist.");
}
catch (BunnyApiException ex)
{
throw new BunnyApiException($"Failed to ensure the existence of storage zone '{zoneName}'.", ex);
}
}
public async Task UploadAsync(Stream stream, string path) => await (await GetBunnyCDNStorage()).UploadAsync(stream, path);
public async Task<bool> DeleteObjectAsync(string path) => await (await GetBunnyCDNStorage()).DeleteObjectAsync(path);
public async Task<List<StorageObject>> GetStorageObjectsAsync(string path) => await (await GetBunnyCDNStorage()).GetStorageObjectsAsync(path);
public async Task<Stream> DownloadObjectAsStreamAsync(string path) => await (await GetBunnyCDNStorage()).DownloadObjectAsStreamAsync(path);
public async Task DeleteStorageZoneAsync(string zoneName)
{
if (string.IsNullOrWhiteSpace(zoneName))
{
throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName));
}
var storageZone = (await GetStorageZoneAsync());
var request = new HttpRequestMessage
{
Method = HttpMethod.Delete,
RequestUri = new Uri($"https://api.bunny.net/storagezone/{storageZone.Id}")
};
using (var response = await _httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
}
}
private async Task<StorageZone> GetStorageZoneAsync()
{
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://api.bunny.net/storagezone")
};
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var zones = JsonSerializer.Deserialize<List<StorageZone>>(content);
var targetZone = zones?.FirstOrDefault(z =>
z.Name.Equals(_storageZoneName, StringComparison.OrdinalIgnoreCase));
return targetZone
?? throw new AbpException(
$"Storage zone '{_storageZoneName}' is not found");
}
private async Task<StorageZone> CreateStorageZoneAsync(string zoneName, string? originUrl = "", int zoneTier = 0)
{
if (string.IsNullOrWhiteSpace(zoneName))
{
throw new ArgumentException("Zone name must not be null or empty.", nameof(zoneName));
}
try
{
var payload = new Dictionary<string, object>
{
{ "ZoneTier", zoneTier },
{ "Name", zoneName },
{ "Region", _region }
};
// Add OriginUrl only if it is not empty
if (!string.IsNullOrEmpty(originUrl))
{
payload.Add("OriginUrl", originUrl!);
}
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://api.bunny.net/storagezone"),
Headers = { { "accept", "application/json" } },
Content = new StringContent(JsonSerializer.Serialize(payload))
{
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
}
};
using var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
throw new BunnyApiException($"Failed to create storage zone. Response: {responseBody}");
}
var createdZoneJson = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<StorageZone>(createdZoneJson)
?? throw new BunnyApiException("Failed to deserialize the created storage zone response.");
}
catch (HttpRequestException ex)
{
throw new BunnyApiException("Error occurred while creating storage zone.", ex);
}
catch (JsonException ex)
{
throw new BunnyApiException("Failed to parse the response for the created storage zone.", ex);
}
}
private async Task<BunnyCDNStorage> GetBunnyCDNStorage()
{
if (BunnyCDNStorage != null) {
return BunnyCDNStorage;
}
var storageZonePassword = (await GetStorageZoneAsync()).Password;
BunnyCDNStorage = new BunnyCDNStorage(_storageZoneName, storageZonePassword, _region);
return BunnyCDNStorage;
}
public void Dispose() => _httpClient?.Dispose();
public class StorageZone
{
public int Id { get; set; }
public string Password { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Region { get; set; } = null!;
public bool Deleted { get; set; }
}
}
public class BunnyApiException : Exception
{
public BunnyApiException(string message) : base(message) { }
public BunnyApiException(string message, Exception innerException) : base(message, innerException) { }
}

153
framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyClientFactory.cs

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using BunnyCDN.Net.Storage;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BlobStoring.Bunny;
public class BunnyClientFactory : IBunnyClientFactory, ITransientDependency
{
private readonly IDistributedCache<StorageZone> _cache;
private readonly IHttpClientFactory _httpClientFactory;
private const string CacheKeyPrefix = "BunnyStorageZone:";
private static TimeSpan CacheDuration = TimeSpan.FromHours(12);
public BunnyClientFactory(
IHttpClientFactory httpClient,
IDistributedCache<StorageZone> cache)
{
_cache = cache;
_httpClientFactory = httpClient;
}
public async Task<BunnyCDNStorage> CreateAsync(string accessKey, string containerName, string region = "de")
{
var cacheKey = $"{CacheKeyPrefix}{containerName}";
var storageZoneInfo = await _cache.GetOrAddAsync(
cacheKey,
async () => {
var result = await GetStorageZoneAsync(accessKey, containerName);
if (result == null)
{
throw new AbpException($"Storage zone '{containerName}' not found");
}
return result;
},
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.Add(CacheDuration)
}
);
if (storageZoneInfo == null)
{
throw new AbpException($"Could not retrieve storage zone information for container '{containerName}'");
}
return new BunnyCDNStorage(containerName, storageZoneInfo.Password, region);
}
public async Task EnsureStorageZoneExistsAsync(
string accessKey,
string containerName,
string region = "de",
bool createIfNotExists = false)
{
var storageZone = await GetStorageZoneAsync(accessKey, containerName);
if (storageZone == null)
{
if (!createIfNotExists)
{
throw new AbpException(
$"Storage zone '{containerName}' does not exist. " +
"Set createIfNotExists to true to create it automatically.");
}
await CreateStorageZoneAsync(accessKey, containerName, region);
// Clear the cache to force a refresh of the storage zone info
var cacheKey = $"{CacheKeyPrefix}{containerName}";
await _cache.RemoveAsync(cacheKey);
}
}
private async Task<StorageZone> CreateStorageZoneAsync(
string accessKey,
string containerName,
string region)
{
using var _client = _httpClientFactory.CreateClient("BunnyApiClient");
_client.DefaultRequestHeaders.Add("AccessKey", accessKey);
var payload = new Dictionary<string, object>
{
{ "Name", containerName },
{ "Region", region },
{ "ZoneTier", 0 }
};
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json");
var response = await _client.PostAsync(
"https://api.bunny.net/storagezone",
content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new AbpException(
$"Failed to create storage zone '{containerName}'. " +
$"Status: {response.StatusCode}, Error: {errorContent}");
}
var responseContent = await response.Content.ReadAsStringAsync();
var createdZone = JsonSerializer.Deserialize<StorageZone>(responseContent);
if (createdZone == null)
{
throw new AbpException(
$"Failed to deserialize the created storage zone response for '{containerName}'");
}
return createdZone;
}
private async Task<StorageZone?> GetStorageZoneAsync(string accessKey, string containerName)
{
using var _client = _httpClientFactory.CreateClient("BunnyApiClient");
_client.DefaultRequestHeaders.Add("AccessKey", accessKey);
var response = await _client.GetAsync("https://api.bunny.net/storagezone");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var zones = JsonSerializer.Deserialize<StorageZone[]>(content);
return Array.Find(zones!, z =>
z.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !z.Deleted);
}
[Serializable]
public class StorageZone
{
public int Id { get; set; }
public string Password { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Region { get; set; }
public bool Deleted { get; set; }
}
}
public class BunnyApiException : Exception
{
public BunnyApiException(string message) : base(message) { }
public BunnyApiException(string message, Exception innerException) : base(message, innerException) { }
}

10
framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using BunnyCDN.Net.Storage;
namespace Volo.Abp.BlobStoring.Bunny;
public interface IBunnyClientFactory
{
Task<BunnyCDNStorage> CreateAsync(string accessKey, string containerName, string region = "de");
Task EnsureStorageZoneExistsAsync(string accessKey, string containerName, string region = "de", bool createIfNotExists = false);
}
Loading…
Cancel
Save