From e037196e855eadfbf5f4f867c8e9f87f3f6ab04d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 22 Feb 2019 20:23:05 +0100 Subject: [PATCH] Example plugin with in-memory asset store. --- .../Samples/MemoryAssetStorePlugin.cs | 29 +++++ .../Assets/MemoryAssetStore.cs | 119 ++++++++++++++++++ .../Assets/NoopAssetStore.cs | 52 ++++++++ .../Configuration/ConfigurationExtensions.cs | 4 + src/Squidex/Config/Domain/AssetServices.cs | 5 + .../Assets/MemoryAssetStoreTests.cs | 31 +++++ .../Assets/MongoGridFsAssetStoreTests.cs | 4 +- 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs create mode 100644 src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs create mode 100644 src/Squidex.Infrastructure/Assets/NoopAssetStore.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs diff --git a/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs new file mode 100644 index 000000000..b4a018ed5 --- /dev/null +++ b/extensions/Squidex.Extensions/Samples/MemoryAssetStorePlugin.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.Samples +{ + public sealed class MemoryAssetStorePlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + var storeType = configuration.GetValue("assetStore:type"); + + if (string.Equals(storeType, "Memory", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingletonAs() + .As(); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs new file mode 100644 index 000000000..7c00f9a85 --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/MemoryAssetStore.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class MemoryAssetStore : IAssetStore + { + private readonly ConcurrentDictionary streams = new ConcurrentDictionary(); + private readonly AsyncLock readerLock = new AsyncLock(); + private readonly AsyncLock writerLock = new AsyncLock(); + + public string GeneratePublicUrl(string id, long version, string suffix) + { + return null; + } + + public async Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); + Guard.NotNullOrEmpty(id, nameof(id)); + + if (!streams.TryGetValue(sourceFileName, out var sourceStream)) + { + throw new AssetNotFoundException(sourceFileName); + } + + using (await readerLock.LockAsync()) + { + await UploadAsync(id, version, suffix, sourceStream, ct); + } + } + + public async Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + var fileName = GetFileName(id, version, suffix); + + if (!streams.TryGetValue(fileName, out var sourceStream)) + { + throw new AssetNotFoundException(fileName); + } + + using (await readerLock.LockAsync()) + { + try + { + await sourceStream.CopyToAsync(stream, 81920, ct); + } + finally + { + sourceStream.Position = 0; + } + } + } + + public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return UploadAsync(GetFileName(id, version, suffix), stream, ct); + } + + public async Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + var memoryStream = new MemoryStream(); + + if (streams.TryAdd(fileName, memoryStream)) + { + using (await writerLock.LockAsync()) + { + try + { + await stream.CopyToAsync(memoryStream, 81920, ct); + } + finally + { + memoryStream.Position = 0; + } + } + } + else + { + throw new AssetAlreadyExistsException(fileName); + } + } + + public Task DeleteAsync(string id, long version, string suffix) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return DeleteAsync(GetFileName(id, version, suffix)); + } + + public Task DeleteAsync(string fileName) + { + Guard.NotNullOrEmpty(fileName, nameof(fileName)); + + streams.TryRemove(fileName, out _); + + return TaskHelper.Done; + } + + private string GetFileName(string id, long version, string suffix) + { + return StringExtensions.JoinNonEmpty("_", id, version.ToString(), suffix); + } + } +} diff --git a/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs new file mode 100644 index 000000000..5c98cd77a --- /dev/null +++ b/src/Squidex.Infrastructure/Assets/NoopAssetStore.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Assets +{ + public sealed class NoopAssetStore : IAssetStore + { + public string GeneratePublicUrl(string id, long version, string suffix) + { + return null; + } + + public Task CopyAsync(string sourceFileName, string id, long version, string suffix, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DownloadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string fileName, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task UploadAsync(string id, long version, string suffix, Stream stream, CancellationToken ct = default) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string fileName) + { + throw new NotSupportedException(); + } + + public Task DeleteAsync(string id, long version, string suffix) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs index 62d0521a4..fafb0ee84 100644 --- a/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs +++ b/src/Squidex.Infrastructure/Configuration/ConfigurationExtensions.cs @@ -54,6 +54,10 @@ namespace Microsoft.Extensions.Configuration { action(); } + else if (options.TryGetValue("default", out action)) + { + action(); + } else { throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(" ", options.Keys)}."); diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index 617c8ff8c..5b8e36706 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -22,6 +22,11 @@ namespace Squidex.Config.Domain { config.ConfigureByOption("assetStore:type", new Options { + ["Default"] = () => + { + services.AddSingletonAs() + .AsOptional(); + }, ["Folder"] = () => { var path = config.GetRequiredValue("assetStore:folder:path"); diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs new file mode 100644 index 000000000..f11cb347d --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Assets/MemoryAssetStoreTests.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Infrastructure.Assets +{ + public class MemoryAssetStoreTests : AssetStoreTests + { + public override MemoryAssetStore CreateStore() + { + return new MemoryAssetStore(); + } + + public override void Dispose() + { + } + + [Fact] + public void Should_not_calculate_source_url() + { + var url = Sut.GeneratePublicUrl(AssetId, 1, null); + + Assert.Null(url); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs index 4fb5a6a66..e80d01ca9 100644 --- a/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs @@ -13,11 +13,11 @@ using Xunit; namespace Squidex.Infrastructure.Assets { - internal class MongoGridFSAssetStoreTests : AssetStoreTests + internal class MongoGridFsAssetStoreTests : AssetStoreTests { private static readonly IGridFSBucket GridFSBucket; - static MongoGridFSAssetStoreTests() + static MongoGridFsAssetStoreTests() { var mongoClient = new MongoClient("mongodb://localhost"); var mongoDatabase = mongoClient.GetDatabase("Test");