diff --git a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs index 7c1e76f7e..6d86aaa2e 100644 --- a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs +++ b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs @@ -67,16 +67,25 @@ namespace Migrations.Migrations.MongoDb var domainType = eventStream.Substring(0, indexOfType); var domainId = eventStream.Substring(indexOfId); - var newDomainId = DomainId.Combine(DomainId.Create(appId), DomainId.Create(domainId)).ToString(); - var newStreamName = $"{domainType}-{newDomainId}"; + if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase)) + { + var newDomainId = DomainId.Combine(DomainId.Create(appId), DomainId.Create(domainId)).ToString(); + var newStreamName = $"{domainType}-{newDomainId}"; + + document["EventStream"] = newStreamName; - document["EventStream"] = newStreamName; + foreach (var @event in document["Events"].AsBsonArray) + { + var metadata = @event["Metadata"].AsBsonDocument; + + metadata["AggregateId"] = newDomainId; + } + } foreach (var @event in document["Events"].AsBsonArray) { var metadata = @event["Metadata"].AsBsonDocument; - metadata["AggregateId"] = newDomainId; metadata.Remove("AppId"); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 5204d2844..0de765b4c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -6,12 +6,14 @@ // ========================================================================== using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; @@ -122,20 +124,34 @@ namespace Squidex.Domain.Apps.Entities.Assets await context.Writer.WriteJsonAsync(TagsFile, tags); } - private Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer) + private async Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer) { - return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => + try { - return assetFileStore.DownloadAsync(appId, assetId, fileVersion, stream); - }); + await writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => + { + return assetFileStore.DownloadAsync(appId, assetId, fileVersion, stream); + }); + } + catch (AssetNotFoundException) + { + return; + } } - private Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader) + private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader) { - return reader.ReadBlobAsync(GetName(assetId, fileVersion), stream => + try { - return assetFileStore.UploadAsync(appId, assetId, fileVersion, stream); - }); + await reader.ReadBlobAsync(GetName(assetId, fileVersion), stream => + { + return assetFileStore.UploadAsync(appId, assetId, fileVersion, stream); + }); + } + catch (FileNotFoundException) + { + return; + } } private static string GetName(DomainId assetId, long fileVersion) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 8b71f5b6e..627807bb7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); - if (attachmentEntry == null) + if (attachmentEntry == null || attachmentEntry.Length == 0) { throw new FileNotFoundException("Cannot find attachment.", name); } 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 c9cdeca1e..890f930c4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Xunit; @@ -81,7 +82,15 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; - await TestBackupEventAsync(@event, 0); + await TestBackupAsync(@event, 0); + } + + [Fact] + public async Task Should_backup_created_asset_with_missing_file() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + + await TestBackupFailedAsync(@event, 0); } [Fact] @@ -89,10 +98,18 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; - await TestBackupEventAsync(@event, @event.FileVersion); + await TestBackupAsync(@event, @event.FileVersion); } - private async Task TestBackupEventAsync(AssetEvent @event, long version) + [Fact] + public async Task Should_backup_updated_asset_with_missing_file() + { + var @event = new AssetUpdated { AssetId = DomainId.NewGuid(), FileVersion = 3 }; + + await TestBackupFailedAsync(@event, @event.FileVersion); + } + + private async Task TestBackupAsync(AssetEvent @event, long version) { var assetStream = new MemoryStream(); var assetId = @event.AssetId; @@ -108,6 +125,22 @@ namespace Squidex.Domain.Apps.Entities.Assets .MustHaveHappened(); } + private async Task TestBackupFailedAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; + + var context = CreateBackupContext(); + + 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)) + .Throws(new AssetNotFoundException(assetId.ToString())); + + await sut.BackupEventAsync(AppEvent(@event), context); + } + [Fact] public async Task Should_restore_created_asset() { @@ -116,6 +149,14 @@ namespace Squidex.Domain.Apps.Entities.Assets await TestRestoreAsync(@event, 0); } + [Fact] + public async Task Should_restore_created_asset_with_missing_file() + { + var @event = new AssetCreated { AssetId = DomainId.NewGuid() }; + + await TestRestoreFailedAsync(@event, 0); + } + [Fact] public async Task Should_restore_updated_asset() { @@ -124,6 +165,14 @@ namespace Squidex.Domain.Apps.Entities.Assets await TestRestoreAsync(@event, @event.FileVersion); } + [Fact] + public async Task Should_restore_updated_asset_with_missing_file() + { + var @event = new AssetUpdated { AppId = appId, AssetId = DomainId.NewGuid(), FileVersion = 3 }; + + await TestRestoreFailedAsync(@event, @event.FileVersion); + } + private async Task TestRestoreAsync(AssetEvent @event, long version) { var assetStream = new MemoryStream(); @@ -140,6 +189,22 @@ namespace Squidex.Domain.Apps.Entities.Assets .MustHaveHappened(); } + private async Task TestRestoreFailedAsync(AssetEvent @event, long version) + { + var assetStream = new MemoryStream(); + var assetId = @event.AssetId; + + var context = CreateRestoreContext(); + + A.CallTo(() => context.Reader.ReadBlobAsync($"{assetId}_{version}.asset", A>._)) + .Throws(new FileNotFoundException()); + + await sut.RestoreEventAsync(AppEvent(@event), context); + + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, assetStream, default)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_restore_states_for_all_assets() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index f44acd2c7..9ec9f5ccf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -40,6 +40,30 @@ namespace Squidex.Domain.Apps.Entities.Backup formatter = new DefaultEventDataFormatter(typeNameRegistry, serializer); } + [Fact] + public async Task Should_not_write_blob_if_handler_failed() + { + var file = "File.json"; + + await TestReaderWriterAsync(BackupVersion.V1, async writer => + { + try + { + await writer.WriteBlobAsync(file, _ => + { + throw new InvalidOperationException(); + }); + } + catch + { + return; + } + }, async reader => + { + await Assert.ThrowsAsync(() => ReadGuidAsync(reader, file)); + }); + } + [Fact] public async Task Should_read_and_write_json_async() {