Browse Source

AzureBlob support

pull/85/head
Sebastian Stehle 9 years ago
parent
commit
ebf1af3919
  1. 112
      src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs
  2. 5
      src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj
  3. 22
      src/Squidex.Infrastructure.Azure/Storage/IStorageAccountManager.cs
  4. 57
      src/Squidex.Infrastructure.Azure/Storage/StorageAccountManager.cs
  5. 23
      src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs
  6. 27
      src/Squidex/Config/Domain/AssetStoreModule.cs
  7. 8
      src/Squidex/appsettings.json
  8. 7
      tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs

112
src/Squidex.Infrastructure.Azure/Storage/AzureBlobAssetStore.cs → src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs

@ -11,102 +11,112 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Squidex.Infrastructure.Assets;
namespace Squidex.Infrastructure.Azure.Storage
namespace Squidex.Infrastructure.Assets
{
public class AzureBlobAssetStore : IAssetStore, IExternalSystem
{
private readonly IStorageAccountManager azureStorageAccount;
private readonly string containerName;
private CloudBlobContainer blobContainer;
private const string AssetVersion = "AssetVersion";
private const string AssetId = "AssetId";
private readonly string containerName;
private readonly string connectionString;
private CloudBlobContainer blobContainer;
public AzureBlobAssetStore(IStorageAccountManager azureStorageAccount, string containerName)
public AzureBlobAssetStore(string connectionString, string containerName)
{
Guard.NotNullOrEmpty(containerName, nameof(containerName));
Guard.NotNull(azureStorageAccount, nameof(azureStorageAccount));
Guard.NotNullOrEmpty(connectionString, nameof(connectionString));
this.azureStorageAccount = azureStorageAccount;
this.connectionString = connectionString;
this.containerName = containerName;
}
public void Connect()
{
try
{
var storageAccount = CloudStorageAccount.Parse(connectionString);
var blobClient = storageAccount.CreateCloudBlobClient();
var blobReference = blobClient.GetContainerReference(containerName);
blobReference.CreateIfNotExistsAsync().Wait();
blobContainer = blobReference;
}
catch (Exception ex)
{
throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.", ex);
}
}
public async Task CopyTemporaryAsync(string name, string id, long version, string suffix)
{
var blobName = GetObjectName(id, version, suffix);
var blobSource = blobContainer.GetBlockBlobReference(name);
var targetTemporaryBlob = blobContainer.GetBlobReference(blobName);
var blobRef = blobContainer.GetBlobReference(blobName);
var tempBlob = blobContainer.GetBlockBlobReference(name);
try
{
await targetTemporaryBlob.StartCopyAsync(blobSource.Uri);
while (targetTemporaryBlob.CopyState.Status == CopyStatus.Pending)
await blobRef.StartCopyAsync(tempBlob.Uri);
while (blobRef.CopyState.Status == CopyStatus.Pending)
{
await Task.Delay(500);
await targetTemporaryBlob.FetchAttributesAsync();
await Task.Delay(50);
await blobRef.FetchAttributesAsync();
}
if (targetTemporaryBlob.CopyState.Status != CopyStatus.Success)
throw new Exception($"Copy of temporary file failed: {targetTemporaryBlob.CopyState.Status}");
if (blobRef.CopyState.Status != CopyStatus.Success)
{
throw new StorageException($"Copy of temporary file failed: {blobRef.CopyState.Status}");
}
}
catch (StorageException ex)
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
var requestInformation = ex.RequestInformation;
if (requestInformation.HttpStatusCode == 404)
throw new AssetNotFoundException($"Asset {name} not found.", ex);
throw;
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
}
public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{
var blobName = GetObjectName(id, version, suffix);
var blob = blobContainer.GetBlockBlobReference(blobName);
var blobRef = blobContainer.GetBlockBlobReference(blobName);
if (!await blob.ExistsAsync())
throw new AssetNotFoundException($"Asset {blobName} not found.");
await blob.DownloadToStreamAsync(stream);
}
public async Task UploadTemporaryAsync(string name, Stream stream)
{
var blob = blobContainer.GetBlockBlobReference(name);
await blob.UploadFromStreamAsync(stream);
try
{
await blobRef.DownloadToStreamAsync(stream);
}
catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
throw new AssetNotFoundException($"Asset {id}, {version} not found.", ex);
}
}
public async Task UploadAsync(string id, long version, string suffix, Stream stream)
{
var blobName = GetObjectName(id, version, suffix);
var blob = blobContainer.GetBlockBlobReference(blobName);
var blobRef = blobContainer.GetBlockBlobReference(blobName);
blob.Metadata[AssetVersion] = version.ToString();
blob.Metadata[AssetId] = id;
blobRef.Metadata[AssetVersion] = version.ToString();
blobRef.Metadata[AssetId] = id;
await blob.UploadFromStreamAsync(stream);
await blob.SetMetadataAsync();
await blobRef.UploadFromStreamAsync(stream);
await blobRef.SetMetadataAsync();
}
public async Task DeleteTemporaryAsync(string name)
public async Task UploadTemporaryAsync(string name, Stream stream)
{
var blob = blobContainer.GetBlockBlobReference(name);
await blob.DeleteIfExistsAsync();
var tempBlob = blobContainer.GetBlockBlobReference(name);
await tempBlob.UploadFromStreamAsync(stream);
}
public void Connect()
public async Task DeleteTemporaryAsync(string name)
{
try
{
var task = azureStorageAccount.GetContainerAsync(containerName);
task.Wait();
var tempBlob = blobContainer.GetBlockBlobReference(name);
blobContainer = task.Result;
}
catch (Exception)
{
throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.");
}
await tempBlob.DeleteIfExistsAsync();
}
private string GetObjectName(string id, long version, string suffix)

5
src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj

@ -1,15 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WindowsAzure.Storage" Version="8.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
</Project>

22
src/Squidex.Infrastructure.Azure/Storage/IStorageAccountManager.cs

@ -1,22 +0,0 @@
// ==========================================================================
// IStorageAccountManager.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage.Blob;
namespace Squidex.Infrastructure.Azure.Storage
{
public interface IStorageAccountManager
{
CloudBlobClient CreateCloudBlobClient();
string GetSharedAccessSignature();
Task<CloudBlobContainer> GetContainerAsync(string name);
}
}

57
src/Squidex.Infrastructure.Azure/Storage/StorageAccountManager.cs

@ -1,57 +0,0 @@
// ==========================================================================
// IStorageAccountManager.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
namespace Squidex.Infrastructure.Azure.Storage
{
public class StorageAccountManager : IStorageAccountManager
{
private readonly CloudStorageAccount storageAccount;
public StorageAccountManager(string storageAccountConnectionString)
{
try
{
storageAccount = CloudStorageAccount.Parse(storageAccountConnectionString);
}
catch (Exception ex)
when (ex is FormatException || ex is ArgumentException)
{
throw new ConfigurationException("Invalid storage account information provided. Please confirm the AccountName and AccountKey are valid in the app settings file.");
}
}
public CloudBlobClient CreateCloudBlobClient()
{
return storageAccount.CreateCloudBlobClient();
}
public string GetSharedAccessSignature()
{
return storageAccount.GetSharedAccessSignature(new SharedAccessAccountPolicy()
{
SharedAccessStartTime = DateTimeOffset.UtcNow,
SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddHours(1),
Permissions = SharedAccessAccountPermissions.Read | SharedAccessAccountPermissions.List
});
}
public async Task<CloudBlobContainer> GetContainerAsync(string name)
{
var blobClient = CreateCloudBlobClient();
var container = blobClient.GetContainerReference(name);
await container.CreateIfNotExistsAsync();
return container;
}
}
}

23
src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs

@ -62,13 +62,9 @@ namespace Squidex.Infrastructure.Assets
{
await storageClient.CopyObjectAsync(bucketName, name, bucketName, objectName);
}
catch (GoogleApiException ex)
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
if (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
throw;
throw new AssetNotFoundException($"Asset {name} not found.", ex);
}
}
@ -80,13 +76,9 @@ namespace Squidex.Infrastructure.Assets
{
await storageClient.DownloadObjectAsync(bucketName, objectName, stream);
}
catch (GoogleApiException ex)
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
if (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
throw new AssetNotFoundException($"Asset {id}, {version} not found.", ex);
}
throw;
throw new AssetNotFoundException($"Asset {id}, {version} not found.", ex);
}
}
@ -96,12 +88,9 @@ namespace Squidex.Infrastructure.Assets
{
await storageClient.DeleteObjectAsync(bucketName, name);
}
catch (GoogleApiException ex)
catch (GoogleApiException ex) when (ex.HttpStatusCode != HttpStatusCode.NotFound)
{
if (ex.HttpStatusCode != HttpStatusCode.NotFound)
{
throw;
}
throw;
}
}

27
src/Squidex/Config/Domain/AssetStoreModule.cs

@ -12,7 +12,6 @@ using Microsoft.Extensions.Configuration;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Azure.Storage;
// ReSharper disable InvertIf
@ -64,36 +63,30 @@ namespace Squidex.Config.Domain
.As<IExternalSystem>()
.SingleInstance();
}
else if (string.Equals(assetStoreType, "AzureBlobStorage", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(assetStoreType, "AzureBlob", StringComparison.OrdinalIgnoreCase))
{
var containerName = Configuration.GetValue<string>("assetStore:azureStorage:containerName");
// NOTE: here it can be improved - if we use keyvault the secret key won't be in the app settings, rather the app has to run
// in the same active directory as the keyvault.
var connectionString = Configuration.GetValue<string>("assetStore:azureStorage:connectionString");
var connectionString = Configuration.GetValue<string>("assetStore:azureBlob:connectionString");
if (string.IsNullOrWhiteSpace(containerName))
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ConfigurationException("Configure AssetStore AzureStorage container with 'assetStore:azureStorage:containerName'.");
throw new ConfigurationException("Configure AssetStore AzureBlob connection string with 'assetStore:azureBlob:connectionString'.");
}
if (string.IsNullOrWhiteSpace(connectionString))
var containerName = Configuration.GetValue<string>("assetStore:azureBlob:containerName");
if (string.IsNullOrWhiteSpace(containerName))
{
throw new ConfigurationException(
"Configure AssetStore AzureStorage connection string with 'assetStore:azureStorage:connectionString'.");
throw new ConfigurationException("Configure AssetStore AzureBlob container with 'assetStore:azureBlob:containerName'.");
}
builder.Register(c => new StorageAccountManager(connectionString))
.As<IStorageAccountManager>()
.SingleInstance();
builder.Register(c => new AzureBlobAssetStore(c.Resolve<IStorageAccountManager>(), containerName))
builder.Register(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>()
.As<IExternalSystem>()
.SingleInstance();
}
else
{
throw new ConfigurationException($"Unsupported value '{assetStoreType}' for 'assetStore:type', supported: Folder, GoogleCloud.");
throw new ConfigurationException($"Unsupported value '{assetStoreType}' for 'assetStore:type', supported: AzureBlob, Folder, GoogleCloud.");
}
}
}

8
src/Squidex/appsettings.json

@ -37,9 +37,9 @@
/*
* Define the type of the read store.
*
* Supported: Folder (local folder), GoogleCloud (hosted in Google Cloud only)
* Supported: Folder (local folder), GoogleCloud (hosted in Google Cloud only), AzureBlob.
*/
"type": "Folder",
"type": "AzureBlob",
"folder": {
/*
* The relative or absolute path to the folder to store the assets.
@ -52,7 +52,7 @@
*/
"bucket": "squidex-assets"
},
"azureStorage": {
"azureBlob": {
/*
* The name of the container in the Azure Blob Storage
*/
@ -60,7 +60,7 @@
/*
* The connection string to the azure storage service.
*/
"connectionString": ""
"connectionString": "UseDevelopmentStorage=true"
}
},

7
tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs

@ -6,18 +6,13 @@
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.Azure.Storage;
namespace Squidex.Infrastructure.Assets
{
public class AzureBlobAssetStoreTests : AssetStoreTests<AzureBlobAssetStore>
{
public override AzureBlobAssetStore CreateStore()
{
var azureStorageAccount =
new StorageAccountManager("UseDevelopmentStorage=true");
return new AzureBlobAssetStore(azureStorageAccount, "squidex-test-container"); ;
return new AzureBlobAssetStore("UseDevelopmentStorage=true", "squidex-test-container");
}
public override void Dispose()

Loading…
Cancel
Save