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 System.Threading.Tasks;
using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.Blob;
using Squidex.Infrastructure.Assets;
namespace Squidex.Infrastructure.Azure.Storage namespace Squidex.Infrastructure.Assets
{ {
public class AzureBlobAssetStore : IAssetStore, IExternalSystem public class AzureBlobAssetStore : IAssetStore, IExternalSystem
{ {
private readonly IStorageAccountManager azureStorageAccount;
private readonly string containerName;
private CloudBlobContainer blobContainer;
private const string AssetVersion = "AssetVersion"; private const string AssetVersion = "AssetVersion";
private const string AssetId = "AssetId"; 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.NotNullOrEmpty(containerName, nameof(containerName));
Guard.NotNull(azureStorageAccount, nameof(azureStorageAccount)); Guard.NotNullOrEmpty(connectionString, nameof(connectionString));
this.azureStorageAccount = azureStorageAccount; this.connectionString = connectionString;
this.containerName = containerName; 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) public async Task CopyTemporaryAsync(string name, string id, long version, string suffix)
{ {
var blobName = GetObjectName(id, version, suffix); var blobName = GetObjectName(id, version, suffix);
var blobSource = blobContainer.GetBlockBlobReference(name); var blobRef = blobContainer.GetBlobReference(blobName);
var targetTemporaryBlob = blobContainer.GetBlobReference(blobName);
var tempBlob = blobContainer.GetBlockBlobReference(name);
try try
{ {
await targetTemporaryBlob.StartCopyAsync(blobSource.Uri); await blobRef.StartCopyAsync(tempBlob.Uri);
while (targetTemporaryBlob.CopyState.Status == CopyStatus.Pending)
while (blobRef.CopyState.Status == CopyStatus.Pending)
{ {
await Task.Delay(500); await Task.Delay(50);
await targetTemporaryBlob.FetchAttributesAsync(); await blobRef.FetchAttributesAsync();
} }
if (targetTemporaryBlob.CopyState.Status != CopyStatus.Success) if (blobRef.CopyState.Status != CopyStatus.Success)
throw new Exception($"Copy of temporary file failed: {targetTemporaryBlob.CopyState.Status}"); {
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; throw new AssetNotFoundException($"Asset {name} not found.", ex);
if (requestInformation.HttpStatusCode == 404)
throw new AssetNotFoundException($"Asset {name} not found.", ex);
throw;
} }
} }
public async Task DownloadAsync(string id, long version, string suffix, Stream stream) public async Task DownloadAsync(string id, long version, string suffix, Stream stream)
{ {
var blobName = GetObjectName(id, version, suffix); var blobName = GetObjectName(id, version, suffix);
var blob = blobContainer.GetBlockBlobReference(blobName); var blobRef = blobContainer.GetBlockBlobReference(blobName);
if (!await blob.ExistsAsync()) try
throw new AssetNotFoundException($"Asset {blobName} not found."); {
await blobRef.DownloadToStreamAsync(stream);
await blob.DownloadToStreamAsync(stream); }
} catch (StorageException ex) when (ex.RequestInformation.HttpStatusCode == 404)
{
public async Task UploadTemporaryAsync(string name, Stream stream) throw new AssetNotFoundException($"Asset {id}, {version} not found.", ex);
{ }
var blob = blobContainer.GetBlockBlobReference(name);
await blob.UploadFromStreamAsync(stream);
} }
public async Task UploadAsync(string id, long version, string suffix, Stream stream) public async Task UploadAsync(string id, long version, string suffix, Stream stream)
{ {
var blobName = GetObjectName(id, version, suffix); var blobName = GetObjectName(id, version, suffix);
var blob = blobContainer.GetBlockBlobReference(blobName); var blobRef = blobContainer.GetBlockBlobReference(blobName);
blob.Metadata[AssetVersion] = version.ToString(); blobRef.Metadata[AssetVersion] = version.ToString();
blob.Metadata[AssetId] = id; blobRef.Metadata[AssetId] = id;
await blob.UploadFromStreamAsync(stream); await blobRef.UploadFromStreamAsync(stream);
await blob.SetMetadataAsync(); await blobRef.SetMetadataAsync();
} }
public async Task DeleteTemporaryAsync(string name) public async Task UploadTemporaryAsync(string name, Stream stream)
{ {
var blob = blobContainer.GetBlockBlobReference(name); var tempBlob = blobContainer.GetBlockBlobReference(name);
await blob.DeleteIfExistsAsync();
await tempBlob.UploadFromStreamAsync(stream);
} }
public void Connect() public async Task DeleteTemporaryAsync(string name)
{ {
try var tempBlob = blobContainer.GetBlockBlobReference(name);
{
var task = azureStorageAccount.GetContainerAsync(containerName);
task.Wait();
blobContainer = task.Result; await tempBlob.DeleteIfExistsAsync();
}
catch (Exception)
{
throw new ConfigurationException($"Cannot connect to blob container '{containerName}'.");
}
} }
private string GetObjectName(string id, long version, string suffix) 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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework> <TargetFramework>netstandard1.6</TargetFramework>
<RootNamespace>Squidex.Infrastructure</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="WindowsAzure.Storage" Version="8.2.0" /> <PackageReference Include="WindowsAzure.Storage" Version="8.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
</Project> </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); 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 new AssetNotFoundException($"Asset {name} not found.", ex);
}
throw;
} }
} }
@ -80,13 +76,9 @@ namespace Squidex.Infrastructure.Assets
{ {
await storageClient.DownloadObjectAsync(bucketName, objectName, stream); 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 new AssetNotFoundException($"Asset {id}, {version} not found.", ex);
}
throw;
} }
} }
@ -96,12 +88,9 @@ namespace Squidex.Infrastructure.Assets
{ {
await storageClient.DeleteObjectAsync(bucketName, name); 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;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Azure.Storage;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -64,36 +63,30 @@ namespace Squidex.Config.Domain
.As<IExternalSystem>() .As<IExternalSystem>()
.SingleInstance(); .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"); var connectionString = Configuration.GetValue<string>("assetStore:azureBlob:connectionString");
// 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");
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( throw new ConfigurationException("Configure AssetStore AzureBlob container with 'assetStore:azureBlob:containerName'.");
"Configure AssetStore AzureStorage connection string with 'assetStore:azureStorage:connectionString'.");
} }
builder.Register(c => new StorageAccountManager(connectionString)) builder.Register(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IStorageAccountManager>()
.SingleInstance();
builder.Register(c => new AzureBlobAssetStore(c.Resolve<IStorageAccountManager>(), containerName))
.As<IAssetStore>() .As<IAssetStore>()
.As<IExternalSystem>() .As<IExternalSystem>()
.SingleInstance(); .SingleInstance();
} }
else 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. * 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": { "folder": {
/* /*
* The relative or absolute path to the folder to store the assets. * The relative or absolute path to the folder to store the assets.
@ -52,7 +52,7 @@
*/ */
"bucket": "squidex-assets" "bucket": "squidex-assets"
}, },
"azureStorage": { "azureBlob": {
/* /*
* The name of the container in the Azure Blob Storage * The name of the container in the Azure Blob Storage
*/ */
@ -60,7 +60,7 @@
/* /*
* The connection string to the azure storage service. * 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. // All rights reserved.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure.Azure.Storage;
namespace Squidex.Infrastructure.Assets namespace Squidex.Infrastructure.Assets
{ {
public class AzureBlobAssetStoreTests : AssetStoreTests<AzureBlobAssetStore> public class AzureBlobAssetStoreTests : AssetStoreTests<AzureBlobAssetStore>
{ {
public override AzureBlobAssetStore CreateStore() public override AzureBlobAssetStore CreateStore()
{ {
var azureStorageAccount = return new AzureBlobAssetStore("UseDevelopmentStorage=true", "squidex-test-container");
new StorageAccountManager("UseDevelopmentStorage=true");
return new AzureBlobAssetStore(azureStorageAccount, "squidex-test-container"); ;
} }
public override void Dispose() public override void Dispose()

Loading…
Cancel
Save