diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs new file mode 100644 index 000000000..c9197180e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Assets; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetPermanentDeleter : IEventConsumer + { + private readonly IAssetFileStore assetFileStore; + private readonly string? deletedType; + + public string Name + { + get => GetType().Name; + } + + public string EventsFilter + { + get => "^asset-"; + } + + public AssetPermanentDeleter(IAssetFileStore assetFileStore, TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(assetFileStore, nameof(assetFileStore)); + + this.assetFileStore = assetFileStore; + + deletedType = typeNameRegistry?.GetName(); + } + + public bool Handles(StoredEvent @event) + { + return @event.Data.Type == deletedType; + } + + public async Task On(Envelope @event) + { + if (@event.Payload is AssetDeleted assetDeleted) + { + for (var version = 0; version < @event.Headers.EventStreamNumber(); version++) + { + try + { + await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version); + } + catch (AssetNotFoundException) + { + continue; + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 30e0777bd..c65b4c334 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetRepository assetRepository; private readonly IAssetFolderRepository assetFolderRepository; private readonly ISemanticLog log; - private readonly string folderDeletedType; + private readonly string? folderDeletedType; public string Name { @@ -46,7 +46,6 @@ namespace Squidex.Domain.Apps.Entities.Assets Guard.NotNull(commandBus, nameof(commandBus)); Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository)); - Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); Guard.NotNull(log, nameof(log)); this.commandBus = commandBus; @@ -54,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Assets this.assetFolderRepository = assetFolderRepository; this.log = log; - folderDeletedType = typeNameRegistry.GetName(); + folderDeletedType = typeNameRegistry?.GetName(); } public bool Handles(StoredEvent @event) diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs index e35f29733..a92a5b0c4 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Setup/SetupController.cs @@ -103,7 +103,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Setup var result = new SetupVM { - BaseUrlConfigured = urlGenerator.BuildUrl(), + BaseUrlConfigured = urlGenerator.BuildUrl(string.Empty, false), BaseUrlCurrent = $"{request.Scheme}://{request.Host}", ErrorMessage = errorMessage, EverybodyCanCreateApps = !uiOptions.OnlyAdminsCanCreateApps, diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 6a1b32f7b..b864d1399 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -38,6 +38,12 @@ namespace Squidex.Config.Domain .As(); } + if (config.GetValue("assets:deletePermanent")) + { + services.AddTransientAs() + .As(); + } + services.AddTransientAs() .AsSelf(); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 219c9920f..4b4c11a73 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -302,7 +302,12 @@ /* * True to delete assets recursively. */ - "deleteRecursive": true + "deleteRecursive": true, + + /* + * True to delete assets files permanently. + */ + "deletePermanent": false }, "logging": { @@ -699,38 +704,38 @@ /* * Set to true to rebuild apps. */ - "apps": false, + "apps": false, - /* + /* * Set to true to rebuild assets. */ - "assets": false, + "assets": false, - /* + /* * Set to true to create dummy asset files if they do not exist. Useful when a backup fail. */ - "assetFiles": false, + "assetFiles": false, - /* + /* * Set to true to rebuild contents. */ - "contents": false, + "contents": false, - /* + /* * Set to true to rebuild rules. */ - "rules": false, + "rules": false, - /* + /* * Set to true to rebuild schemas. */ - "schemas": false, + "schemas": false, - /* + /* * Set to true to rebuild indexes. */ - "indexes": false - }, + "indexes": false + }, /*" * A list of configuration values that should be exposed from the info endpoint and in the UI. diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs new file mode 100644 index 000000000..358bef438 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Assets; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetPermanentDeleterTests + { + private readonly IAssetFileStore assetFiletore = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly AssetPermanentDeleter sut; + + public AssetPermanentDeleterTests() + { + var typeNameRegistry = new TypeNameRegistry().Map(typeof(AssetDeleted)); + + sut = new AssetPermanentDeleter(assetFiletore, typeNameRegistry); + } + + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; + + Assert.Equal("^asset-", consumer.EventsFilter); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; + + await consumer.ClearAsync(); + } + + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; + + Assert.Equal(nameof(AssetPermanentDeleter), consumer.Name); + } + + [Fact] + public async Task Should_delete_assets_for_all_versions() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + await sut.On(Envelope.Create(@event).SetEventStreamNumber(2)); + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + .MustHaveHappened(); + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_ignore_not_found_assets() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + .Throws(new AssetNotFoundException("fileName")); + + await sut.On(Envelope.Create(@event).SetEventStreamNumber(2));; + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_ignore_exceptions() + { + var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.On(Envelope.Create(@event).SetEventStreamNumber(2))); + + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1)) + .MustNotHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs index b93aabf16..a59af9d6d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs @@ -23,7 +23,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class RecursiveDeleterTests { - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); private readonly ISemanticLog log = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); private readonly IAssetFolderRepository assetFolderRepository = A.Fake(); @@ -33,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public RecursiveDeleterTests() { - typeNameRegistry.Map(typeof(AssetFolderDeleted)); + var typeNameRegistry = new TypeNameRegistry().Map(typeof(AssetFolderDeleted)); sut = new RecursiveDeleter(commandBus, assetRepository, assetFolderRepository, typeNameRegistry, log); }