diff --git a/src/Squidex.Infrastructure.Azure/Storage/AzureBlobAssetStore.cs b/src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs similarity index 50% rename from src/Squidex.Infrastructure.Azure/Storage/AzureBlobAssetStore.cs rename to src/Squidex.Infrastructure.Azure/Assets/AzureBlobAssetStore.cs index 506b38df2..6026f10b0 100644 --- a/src/Squidex.Infrastructure.Azure/Storage/AzureBlobAssetStore.cs +++ b/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) diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj index a1aa5eb3a..a2d331b7d 100644 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -1,15 +1,12 @@  - netstandard1.6 + Squidex.Infrastructure - - - \ No newline at end of file diff --git a/src/Squidex.Infrastructure.Azure/Storage/IStorageAccountManager.cs b/src/Squidex.Infrastructure.Azure/Storage/IStorageAccountManager.cs deleted file mode 100644 index 43240316d..000000000 --- a/src/Squidex.Infrastructure.Azure/Storage/IStorageAccountManager.cs +++ /dev/null @@ -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 GetContainerAsync(string name); - } -} diff --git a/src/Squidex.Infrastructure.Azure/Storage/StorageAccountManager.cs b/src/Squidex.Infrastructure.Azure/Storage/StorageAccountManager.cs deleted file mode 100644 index f09f105b8..000000000 --- a/src/Squidex.Infrastructure.Azure/Storage/StorageAccountManager.cs +++ /dev/null @@ -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 GetContainerAsync(string name) - { - var blobClient = CreateCloudBlobClient(); - var container = blobClient.GetContainerReference(name); - await container.CreateIfNotExistsAsync(); - - return container; - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs b/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs index b6a080044..da0edad51 100644 --- a/src/Squidex.Infrastructure.GoogleCloud/Assets/GoogleCloudAssetStore.cs +++ b/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; } } diff --git a/src/Squidex/Config/Domain/AssetStoreModule.cs b/src/Squidex/Config/Domain/AssetStoreModule.cs index 789bd8c6d..ca6dad138 100644 --- a/src/Squidex/Config/Domain/AssetStoreModule.cs +++ b/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() .SingleInstance(); } - else if (string.Equals(assetStoreType, "AzureBlobStorage", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(assetStoreType, "AzureBlob", StringComparison.OrdinalIgnoreCase)) { - var containerName = Configuration.GetValue("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("assetStore:azureStorage:connectionString"); + var connectionString = Configuration.GetValue("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("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() - .SingleInstance(); - - builder.Register(c => new AzureBlobAssetStore(c.Resolve(), containerName)) + builder.Register(c => new AzureBlobAssetStore(connectionString, containerName)) .As() .As() .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."); } } } diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index bf7b6da81..674986e38 100644 --- a/src/Squidex/appsettings.json +++ b/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" } }, diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs index 517f80bf0..36d23af38 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/AzureBlobAssetStoreTests.cs +++ b/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 { 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()