mirror of https://github.com/abpframework/abp.git
6 changed files with 222 additions and 281 deletions
@ -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; |
namespace Volo.Abp.BlobStoring.Bunny; |
||||
|
|
||||
[DependsOn(typeof(AbpBlobStoringModule))] |
[DependsOn( |
||||
|
typeof(AbpBlobStoringModule), |
||||
|
typeof(AbpCachingModule))] |
||||
public class AbpBlobStoringBunnyModule : AbpModule |
public class AbpBlobStoringBunnyModule : AbpModule |
||||
{ |
{ |
||||
public override void ConfigureServices(ServiceConfigurationContext context) |
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
{ |
{ |
||||
|
context.Services.AddHttpClient(); |
||||
} |
} |
||||
} |
} |
||||
@ -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) { } |
|
||||
} |
|
||||
@ -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) { } |
||||
|
} |
||||
@ -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…
Reference in new issue