diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs index 380e90fd6..f0325e0dc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs @@ -68,7 +68,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { Guard.NotNull(app, nameof(app)); - return GetPlanUpgrade(app.Plan?.PlanId); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs index b31b7cf0e..6e839fa66 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetExtensions.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { using (var stream = DefaultPools.MemoryStream.GetStream()) { - await assetFileStore.DownloadAsync(appId, id, fileVersion, stream); + await assetFileStore.DownloadAsync(appId, id, fileVersion, null, stream); stream.Position = 0; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs index 2e9101c50..7d2b1ed7a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { try { - await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version); + await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version, null); } catch (AssetNotFoundException) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index bcd4df66c..1e5705af1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -127,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { await writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => { - return assetFileStore.DownloadAsync(appId, assetId, fileVersion, stream); + return assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream); }); } catch (AssetNotFoundException) @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { await reader.ReadBlobAsync(GetName(assetId, fileVersion), stream => { - return assetFileStore.UploadAsync(appId, assetId, fileVersion, stream); + return assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream); }); } catch (FileNotFoundException) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs index fbed03411..e34b8cbc1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs @@ -27,53 +27,53 @@ namespace Squidex.Domain.Apps.Entities.Assets this.options = options.Value; } - public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion) + public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix) { - var fileName = GetFileName(appId, id, fileVersion); + var fileName = GetFileName(appId, id, fileVersion, suffix); return assetStore.GeneratePublicUrl(fileName); } - public async Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, + public async Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default) { try { - var fileNameNew = GetFileName(appId, id, fileVersion); + var fileNameNew = GetFileName(appId, id, fileVersion, suffix); return await assetStore.GetSizeAsync(fileNameNew, ct); } - catch (AssetNotFoundException) + catch (AssetNotFoundException) when (!options.FolderPerApp) { - var fileNameOld = GetFileName(id, fileVersion); + var fileNameOld = GetFileName(id, fileVersion, suffix); return await assetStore.GetSizeAsync(fileNameOld, ct); } } - public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, BytesRange range = default, + public async Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, CancellationToken ct = default) { try { - var fileNameNew = GetFileName(appId, id, fileVersion); + var fileNameNew = GetFileName(appId, id, fileVersion, suffix); await assetStore.DownloadAsync(fileNameNew, stream, range, ct); } - catch (AssetNotFoundException) + catch (AssetNotFoundException) when (!options.FolderPerApp) { - var fileNameOld = GetFileName(id, fileVersion); + var fileNameOld = GetFileName(id, fileVersion, suffix); await assetStore.DownloadAsync(fileNameOld, stream, range, ct); } } - public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, + public Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default) { - var fileName = GetFileName(appId, id, fileVersion); + var fileName = GetFileName(appId, id, fileVersion, suffix); - return assetStore.UploadAsync(fileName, stream, true, ct); + return assetStore.UploadAsync(fileName, stream, overwrite, ct); } public Task UploadAsync(string tempFile, Stream stream, @@ -82,22 +82,29 @@ namespace Squidex.Domain.Apps.Entities.Assets return assetStore.UploadAsync(tempFile, stream, false, ct); } - public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, + public Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default) { - var fileName = GetFileName(appId, id, fileVersion); + var fileName = GetFileName(appId, id, fileVersion, suffix); return assetStore.CopyAsync(tempFile, fileName, ct); } - public async Task DeleteAsync(DomainId appId, DomainId id, long fileVersion) + public Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix) { - var fileNameOld = GetFileName(id, fileVersion); - var fileNameNew = GetFileName(appId, id, fileVersion); + var fileNameOld = GetFileName(id, fileVersion, suffix); + var fileNameNew = GetFileName(appId, id, fileVersion, suffix); - await Task.WhenAll( - assetStore.DeleteAsync(fileNameOld), - assetStore.DeleteAsync(fileNameNew)); + if (options.FolderPerApp) + { + return assetStore.DeleteAsync(fileNameNew); + } + else + { + return Task.WhenAll( + assetStore.DeleteAsync(fileNameOld), + assetStore.DeleteAsync(fileNameNew)); + } } public Task DeleteAsync(string tempFile) @@ -105,20 +112,41 @@ namespace Squidex.Domain.Apps.Entities.Assets return assetStore.DeleteAsync(tempFile); } - private static string GetFileName(DomainId id, long fileVersion) + private static string GetFileName(DomainId id, long fileVersion, string? suffix) { - return $"{id}_{fileVersion}"; + if (!string.IsNullOrWhiteSpace(suffix)) + { + return $"{id}_{fileVersion}_{suffix}"; + } + else + { + return $"{id}_{fileVersion}"; + } } - private string GetFileName(DomainId appId, DomainId id, long fileVersion) + private string GetFileName(DomainId appId, DomainId id, long fileVersion, string? suffix) { if (options.FolderPerApp) { - return $"{appId}/{id}_{fileVersion}"; + if (!string.IsNullOrWhiteSpace(suffix)) + { + return $"derived/{appId}/{id}_{fileVersion}_{suffix}"; + } + else + { + return $"{appId}/{id}_{fileVersion}"; + } } else { - return $"{appId}_{id}_{fileVersion}"; + if (!string.IsNullOrWhiteSpace(suffix)) + { + return $"{appId}_{id}_{fileVersion}_{suffix}"; + } + else + { + return $"{appId}_{id}_{fileVersion}"; + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs index 9f335bcd0..edc8985fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject try { - await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion); + await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null); } catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs index 75dcac29a..a3d1c69ae 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs @@ -15,20 +15,20 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetFileStore { - string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion); + string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix); - Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, CancellationToken ct = default); + Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default); - Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, CancellationToken ct = default); + Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default); Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default); - Task UploadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, CancellationToken ct = default); + Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default); - Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, Stream stream, BytesRange range = default, CancellationToken ct = default); + Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, CancellationToken ct = default); Task DeleteAsync(string tempFile); - Task DeleteAsync(DomainId appId, DomainId id, long fileVersion); + Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs index 263a6fb82..1d6aaf181 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs @@ -58,13 +58,13 @@ namespace Squidex.Domain.Apps.Entities.Assets { try { - await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, ct); + await assetFileStore.GetFileSizeAsync(appId.Id, id, fileVersion, null, ct); } catch (AssetNotFoundException) { DummyStream.Position = 0; - await assetFileStore.UploadAsync(appId.Id, id, fileVersion, DummyStream, ct); + await assetFileStore.UploadAsync(appId.Id, id, fileVersion, null, DummyStream, ct: ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 2fbd973b3..14f8445e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -17,7 +17,7 @@ - + diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj index fd84995c0..36ff6a906 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -10,7 +10,7 @@ True - + diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 021c05401..26b02c4bd 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -25,7 +25,7 @@ - + diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs index 8796fcc67..efb6e316e 100644 --- a/backend/src/Squidex.Web/Services/UrlGenerator.cs +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -61,7 +61,7 @@ namespace Squidex.Web.Services public string? AssetSource(NamedId appId, DomainId assetId, long fileVersion) { - return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion); + return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion, null); } public string AssetsUI(NamedId appId, string? query = null) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index cf46e4fe1..c0e1b143a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -36,7 +36,6 @@ namespace Squidex.Areas.Api.Controllers.Assets private readonly IAssetFileStore assetFileStore; private readonly IAssetQueryService assetQuery; private readonly IAssetLoader assetLoader; - private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetContentController( @@ -44,14 +43,12 @@ namespace Squidex.Areas.Api.Controllers.Assets IAssetFileStore assetFileStore, IAssetQueryService assetQuery, IAssetLoader assetLoader, - IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { this.assetFileStore = assetFileStore; this.assetQuery = assetQuery; this.assetLoader = assetLoader; - this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -162,21 +159,21 @@ namespace Squidex.Areas.Api.Controllers.Assets { callback = async (bodyStream, range, ct) => { - var resizedAsset = $"{asset.AppId.Id}_{asset.Id}_{asset.FileVersion}_{resizeOptions}"; - if (request.ForceResize) { - await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, true, ct); + await ResizeAsync(asset, bodyStream, resizeOptions, true, ct); } else { try { - await assetStore.DownloadAsync(resizedAsset, bodyStream, ct: ct); + var suffix = resizeOptions.ToString(); + + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, bodyStream, ct: ct); } catch (AssetNotFoundException) { - await ResizeAsync(asset, bodyStream, resizedAsset, resizeOptions, false, ct); + await ResizeAsync(asset, bodyStream, resizeOptions, false, ct); } } }; @@ -187,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Assets callback = async (bodyStream, range, ct) => { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, bodyStream, range, ct); + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, bodyStream, range, ct); }; } @@ -202,8 +199,10 @@ namespace Squidex.Areas.Api.Controllers.Assets }; } - private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, string fileName, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct) + private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct) { + var suffix = resizeOptions.ToString(); + using (Profiler.Trace("Resize")) { using (var sourceStream = GetTempStream()) @@ -212,7 +211,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { using (Profiler.Trace("ResizeDownload")) { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, sourceStream); + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, sourceStream); sourceStream.Position = 0; } @@ -234,7 +233,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { using (Profiler.Trace("ResizeUpload")) { - await assetStore.UploadAsync(fileName, destinationStream, overwrite); + await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, destinationStream, overwrite); destinationStream.Position = 0; } } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 1fe924541..702cb54f2 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -59,12 +59,12 @@ - - - - - - + + + + + + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs index 1e47ac5e5..b87aa5d4a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event).SetRestored()); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A._)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A._, null)) .MustNotHaveHappened(); } @@ -72,10 +72,10 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event).SetEventStreamNumber(2)); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) .MustHaveHappened(); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) .MustHaveHappened(); } @@ -84,12 +84,12 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) .Throws(new AssetNotFoundException("fileName")); await sut.On(Envelope.Create(@event).SetEventStreamNumber(2)); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) .MustHaveHappened(); } @@ -98,12 +98,12 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) .Throws(new InvalidOperationException()); await Assert.ThrowsAsync(() => sut.On(Envelope.Create(@event).SetEventStreamNumber(2))); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index 629b3ec87..e1d02c013 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); - A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); } @@ -280,16 +280,16 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); - A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); } private void SetupText(DomainId id, byte[] bytes) { - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A._, A._, A._)) .Invokes(x => { - var stream = x.GetArgument(3)!; + var stream = x.GetArgument(4)!; stream.Write(bytes); }); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index 8096e7380..fcf6db497 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -250,7 +250,7 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); - A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); } @@ -319,16 +319,16 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); - A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, null, A._, A._, A._)) .MustNotHaveHappened(); } private void SetupText(DomainId id, byte[] bytes) { - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, A._, A._, A._)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, id, 0, null, A._, A._, A._)) .Invokes(x => { - var stream = x.GetArgument(3)!; + var stream = x.GetArgument(4)!; stream.Write(bytes); }); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index 54a0a633f..cefecdf35 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.BackupEventAsync(AppEvent(@event), context); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, assetStream, default, default)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default)) .MustHaveHappened(); } @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => context.Writer.WriteBlobAsync($"{assetId}_{version}.asset", A>._)) .Invokes((string _, Func handler) => handler(assetStream)); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, assetStream, default, default)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default)) .Throws(new AssetNotFoundException(assetId.ToString())); await sut.BackupEventAsync(AppEvent(@event), context); @@ -183,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreEventAsync(AppEvent(@event), context); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, assetStream, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default)) .MustHaveHappened(); } @@ -199,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreEventAsync(AppEvent(@event), context); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, assetStream, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index c82a0b3d2..042c1aa5f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -22,59 +23,79 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId assetId = DomainId.NewGuid(); private readonly long assetFileVersion = 21; - private readonly string fileNameOld; - private readonly string fileNameNew; - private readonly string fileNameFolder; private readonly AssetOptions options = new AssetOptions(); private readonly DefaultAssetFileStore sut; public DefaultAssetFileStoreTests() { - fileNameOld = $"{assetId}_{assetFileVersion}"; - fileNameNew = $"{appId}_{assetId}_{assetFileVersion}"; - fileNameFolder = $"{appId}/{assetId}_{assetFileVersion}"; - sut = new DefaultAssetFileStore(assetStore, Options.Create(options)); } - [Fact] - public void Should_get_public_url_from_store() + public static IEnumerable PathCases() + { + yield return new object[] { true, "resize=100", "derived/{appId}/{assetId}_{assetFileVersion}_resize=100" }; + yield return new object[] { true, string.Empty, "{appId}/{assetId}_{assetFileVersion}" }; + yield return new object[] { false, "resize=100", "{appId}_{assetId}_{assetFileVersion}_resize=100" }; + yield return new object[] { false, string.Empty, "{appId}_{assetId}_{assetFileVersion}" }; + } + + public static IEnumerable PathCasesOld() { + yield return new object?[] { "resize=100", "{assetId}_{assetFileVersion}_resize=100" }; + yield return new object?[] { string.Empty, "{assetId}_{assetFileVersion}" }; + } + + [Theory] + [MemberData(nameof(PathCases))] + public void Should_get_public_url_from_store(bool folderPerApp, string? suffix, string fileName) + { + var fullName = GetFullName(fileName); + + options.FolderPerApp = folderPerApp; + var url = "http_//squidex.io/assets"; - A.CallTo(() => assetStore.GeneratePublicUrl(fileNameNew)) + A.CallTo(() => assetStore.GeneratePublicUrl(fullName)) .Returns(url); - var result = sut.GeneratePublicUrl(appId, assetId, assetFileVersion); + var result = sut.GeneratePublicUrl(appId, assetId, assetFileVersion, suffix); Assert.Equal(url, result); } - [Fact] - public async Task Should_get_file_size_from_store() + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_get_file_size_from_store(bool folderPerApp, string? suffix, string fileName) { + var fullName = GetFullName(fileName); + + options.FolderPerApp = folderPerApp; + var size = 1024L; - A.CallTo(() => assetStore.GetSizeAsync(fileNameNew, default)) + A.CallTo(() => assetStore.GetSizeAsync(fullName, default)) .Returns(size); - var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion); + var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix); Assert.Equal(size, result); } - [Fact] - public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found() + [Theory] + [MemberData(nameof(PathCasesOld))] + public async Task Should_get_file_size_from_store_with_old_file_name_if_new_name_not_found(string? suffix, string fileName) { + var fullName = GetFullName(fileName); + var size = 1024L; - A.CallTo(() => assetStore.GetSizeAsync(fileNameOld, default)) - .Throws(new AssetNotFoundException(fileNameOld)); + A.CallTo(() => assetStore.GetSizeAsync(A._, default)) + .Throws(new AssetNotFoundException(assetId.ToString())); - A.CallTo(() => assetStore.GetSizeAsync(fileNameNew, default)) + A.CallTo(() => assetStore.GetSizeAsync(fullName, default)) .Returns(size); - var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion); + var result = await sut.GetFileSizeAsync(appId, assetId, assetFileVersion, suffix); Assert.Equal(size, result); } @@ -90,66 +111,82 @@ namespace Squidex.Domain.Apps.Entities.Assets .MustHaveHappened(); } - [Fact] - public async Task Should_upload_file_to_store() + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_upload_file_to_store(bool folderPerApp, string? suffix, string fileName) { - options.FolderPerApp = false; + var fullName = GetFullName(fileName); + + options.FolderPerApp = folderPerApp; var stream = new MemoryStream(); - await sut.UploadAsync(appId, assetId, assetFileVersion, stream); + await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream); - A.CallTo(() => assetStore.UploadAsync(fileNameNew, stream, true, CancellationToken.None)) + A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, CancellationToken.None)) .MustHaveHappened(); } - [Fact] - public async Task Should_upload_file_to_store_with_folder() + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_download_file_from_store(bool folderPerApp, string? suffix, string fileName) { - options.FolderPerApp = true; + var fullName = GetFullName(fileName); + + options.FolderPerApp = folderPerApp; var stream = new MemoryStream(); - await sut.UploadAsync(appId, assetId, assetFileVersion, stream); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream); - A.CallTo(() => assetStore.UploadAsync(fileNameFolder, stream, true, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None)) .MustHaveHappened(); } [Fact] - public async Task Should_download_file_from_store() + public async Task Should_download_file_from_store_with_folder_only_if_configured() { + options.FolderPerApp = true; + var stream = new MemoryStream(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, stream); + A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, CancellationToken.None)) + .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None)) - .MustHaveHappened(); + await Assert.ThrowsAsync(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream)); + + A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, CancellationToken.None)) + .MustHaveHappenedOnceExactly(); } - [Fact] - public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found() + [Theory] + [MemberData(nameof(PathCasesOld))] + public async Task Should_download_file_from_store_with_old_file_name_if_new_name_not_found(string suffix, string fileName) { + var fullName = GetFullName(fileName); + var stream = new MemoryStream(); - A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None)) - .Throws(new AssetNotFoundException(fileNameNew)); + A.CallTo(() => assetStore.DownloadAsync(A.That.Matches(x => x != fileName), stream, default, CancellationToken.None)) + .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, stream); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream); - A.CallTo(() => assetStore.DownloadAsync(fileNameOld, stream, default, CancellationToken.None)) - .MustHaveHappened(); - - A.CallTo(() => assetStore.DownloadAsync(fileNameNew, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None)) .MustHaveHappened(); } - [Fact] - public async Task Should_copy_file_to_store() + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_copy_file_to_store(bool folderPerApp, string? suffix, string fileName) { - await sut.CopyAsync("Temp", appId, assetId, assetFileVersion); + var fullName = GetFullName(fileName); - A.CallTo(() => assetStore.CopyAsync("Temp", fileNameNew, CancellationToken.None)) + options.FolderPerApp = folderPerApp; + + await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix); + + A.CallTo(() => assetStore.CopyAsync("Temp", fullName, CancellationToken.None)) .MustHaveHappened(); } @@ -162,16 +199,29 @@ namespace Squidex.Domain.Apps.Entities.Assets .MustHaveHappened(); } - [Fact] - public async Task Should_delete_file_from_store() + [Theory] + [MemberData(nameof(PathCases))] + public async Task Should_delete_file_from_store(bool folderPerApp, string? suffix, string fileName) { - await sut.DeleteAsync(appId, assetId, assetFileVersion); + var fullName = GetFullName(fileName); - A.CallTo(() => assetStore.DeleteAsync(fileNameNew)) - .MustHaveHappened(); + options.FolderPerApp = folderPerApp; + + await sut.DeleteAsync(appId, assetId, assetFileVersion, suffix); - A.CallTo(() => assetStore.DeleteAsync(fileNameOld)) + A.CallTo(() => assetStore.DeleteAsync(fullName)) .MustHaveHappened(); + + A.CallTo(() => assetStore.DeleteAsync(A._)) + .MustHaveHappenedANumberOfTimesMatching(x => x == (folderPerApp ? 1 : 2)); + } + + private string GetFullName(string fileName) + { + return fileName + .Replace("{appId}", appId.ToString()) + .Replace("{assetId}", assetId.ToString()) + .Replace("{assetFileVersion}", assetFileVersion.ToString()); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index e57d07ace..cae17e0fb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { A.CallTo(() => assetFileStore.UploadAsync(A._, A._, CancellationToken.None)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, CancellationToken.None)) + A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, null, CancellationToken.None)) .MustHaveHappened(); A.CallTo(() => assetFileStore.DeleteAsync(A._)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs index a2e0c30ad..8469c176c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs @@ -38,12 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupEvent(@event); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) .Throws(new AssetNotFoundException("file")); await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, A._, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A._, true, default)) .MustHaveHappened(); } @@ -54,12 +54,12 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupEvent(@event); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 0, null, default)) .Returns(100); await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, A._, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A._, true, default)) .MustNotHaveHappened(); } @@ -70,12 +70,12 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupEvent(@event); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default)) .Throws(new AssetNotFoundException("file")); await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, A._, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A._, true, default)) .MustHaveHappened(); } @@ -86,12 +86,12 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupEvent(@event); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default)) .Returns(100); await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, A._, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A._, true, default)) .MustNotHaveHappened(); } @@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RepairAsync(); - A.CallTo(() => assetFileStore.GetFileSizeAsync(A._, A._, A._, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(A._, A._, A._, null, default)) .MustNotHaveHappened(); }