diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index 08138a045..c54ae17a5 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using Orleans; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Backup; @@ -17,7 +18,6 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps @@ -28,8 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IGrainFactory grainFactory; private readonly IUserResolver userResolver; private readonly HashSet activeUsers = new HashSet(); - private readonly Dictionary usersWithEmail = new Dictionary(); - private readonly Dictionary userMapping = new Dictionary(); + private Dictionary usersWithEmail = new Dictionary(); + private Dictionary userMapping = new Dictionary(); private bool isReserved; private bool isActorAssigned; private AppCreated appCreated; @@ -152,22 +152,16 @@ namespace Squidex.Domain.Apps.Entities.Apps private async Task ReadUsersAsync(BackupReader reader) { - await reader.ReadAttachmentAsync(UsersFile, stream => - { - stream.SerializeAsJson(usersWithEmail); + var json = await reader.ReadJsonAttachmentAsync(UsersFile); - return TaskHelper.Done; - }); + usersWithEmail = json.ToObject>(); } private Task WriterUsersAsync(BackupWriter writer) { - return writer.WriteAttachmentAsync(UsersFile, stream => - { - stream.SerializeAsJson(usersWithEmail); + var json = JObject.FromObject(usersWithEmail); - return TaskHelper.Done; - }); + return writer.WriteJsonAsync(UsersFile, json); } public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 39573c6e6..a63d894b6 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Backup; @@ -84,29 +85,23 @@ namespace Squidex.Domain.Apps.Entities.Assets await RebuildManyAsync(assetIds, id => RebuildAsync(id, (e, s) => s.Apply(e))); } - private Task RestoreTagsAsync(Guid appId, BackupReader reader) + private async Task RestoreTagsAsync(Guid appId, BackupReader reader) { - return reader.ReadAttachmentAsync(TagsFile, stream => - { - var tags = stream.DeserializeAsJson(); + var tags = await reader.ReadJsonAttachmentAsync(TagsFile); - return tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags); - }); + await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags.ToObject()); } - private Task BackupTagsAsync(Guid appId, BackupWriter writer) + private async Task BackupTagsAsync(Guid appId, BackupWriter writer) { - return writer.WriteAttachmentAsync(TagsFile, async stream => - { - var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); + var tags = await tagService.GetExportableTagsAsync(appId, TagGroups.Assets); - stream.SerializeAsJson(tags); - }); + await writer.WriteJsonAsync(TagsFile, JObject.FromObject(tags)); } private Task WriteAssetAsync(Guid assetId, long fileVersion, BackupWriter writer) { - return writer.WriteAttachmentAsync(GetName(assetId, fileVersion), stream => + return writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => { return assetStore.DownloadAsync(assetId.ToString(), fileVersion, null, stream); }); @@ -116,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { assetIds.Add(assetId); - return reader.ReadAttachmentAsync(GetName(assetId, fileVersion), async stream => + return reader.ReadBlobAsync(GetName(reader.OldGuid(assetId), fileVersion), async stream => { try { diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs index 67fd56e0f..d6a2eba0d 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Backup protected Task RemoveSnapshotAsync(Guid id) { - return store.RemoveSnapshotAsync(id); + return store.RemoveSnapshotAsync(id); } protected async Task RebuildManyAsync(IEnumerable ids, Func action) diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 8970cf26f..cc49fe3e5 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -9,14 +9,19 @@ using System; using System.IO; using System.IO.Compression; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Entities.Backup.Archive; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Backup { public sealed class BackupReader : DisposableObjectBase { + private static readonly JsonSerializer Serializer = new JsonSerializer(); + private readonly GuidMapper guidMapper = new GuidMapper(); private readonly ZipArchive archive; private int readEvents; private int readAttachments; @@ -44,7 +49,43 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public async Task ReadAttachmentAsync(string name, Func handler) + public Guid OldGuid(Guid newId) + { + return guidMapper.OldGuid(newId); + } + + public async Task ReadJsonAttachmentAsync(string name) + { + Guard.NotNullOrEmpty(name, nameof(name)); + + var attachmentEntry = archive.GetEntry(ArchiveHelper.GetAttachmentPath(name)); + + if (attachmentEntry == null) + { + throw new FileNotFoundException("Cannot find attachment.", name); + } + + JToken result; + + using (var stream = attachmentEntry.Open()) + { + using (var textReader = new StreamReader(stream)) + { + using (var jsonReader = new JsonTextReader(textReader)) + { + result = await JToken.ReadFromAsync(jsonReader); + + guidMapper.NewGuids(result); + } + } + } + + readAttachments++; + + return result; + } + + public async Task ReadBlobAsync(string name, Func handler) { Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNull(handler, nameof(handler)); @@ -64,9 +105,10 @@ namespace Squidex.Domain.Apps.Entities.Backup readAttachments++; } - public async Task ReadEventsAsync(Func handler) + public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, Func handler) { Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); while (true) { @@ -79,9 +121,25 @@ namespace Squidex.Domain.Apps.Entities.Backup using (var stream = eventEntry.Open()) { - var storedEvent = stream.DeserializeAsJson(); + using (var textReader = new StreamReader(stream)) + { + using (var jsonReader = new JsonTextReader(textReader)) + { + var storedEvent = Serializer.Deserialize(jsonReader); + + storedEvent.Data.Payload = guidMapper.NewGuids(storedEvent.Data.Payload); + storedEvent.Data.Metadata = guidMapper.NewGuids(storedEvent.Data.Metadata); + + var streamName = streamNameResolver.WithNewId(storedEvent.StreamName, guidMapper.NewGuidString); + + storedEvent = new StoredEvent(streamName, + storedEvent.EventPosition, + storedEvent.EventStreamNumber, + storedEvent.Data); - await handler(storedEvent); + await handler(storedEvent); + } + } } readEvents++; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupSerializer.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupSerializer.cs deleted file mode 100644 index b638f9da4..000000000 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupSerializer.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.IO; -using Newtonsoft.Json; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public static class BackupSerializer - { - private static readonly JsonSerializer JsonSerializer = JsonSerializer.CreateDefault(); - - public static void SerializeAsJson(this Stream stream, T value) - { - using (var writer = new StreamWriter(stream)) - { - JsonSerializer.Serialize(writer, value); - } - } - - public static T DeserializeAsJson(this Stream stream) - { - using (var reader = new StreamReader(stream)) - { - return (T)JsonSerializer.Deserialize(reader, typeof(T)); - } - } - - public static void DeserializeAsJson(this Stream stream, T result) - { - using (var reader = new StreamReader(stream)) - { - JsonSerializer.Populate(reader, result); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs index 49c45f94a..920ae4641 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -9,6 +9,8 @@ using System; using System.IO; using System.IO.Compression; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Entities.Backup.Archive; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -17,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { public sealed class BackupWriter : DisposableObjectBase { + private static readonly JsonSerializer Serializer = new JsonSerializer(); private readonly ZipArchive archive; private int writtenEvents; private int writtenAttachments; @@ -31,9 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Backup get { return writtenAttachments; } } - public BackupWriter(Stream stream) + public BackupWriter(Stream stream, bool keepOpen = false) { - archive = new ZipArchive(stream, ZipArchiveMode.Create, false); + archive = new ZipArchive(stream, ZipArchiveMode.Create, keepOpen); } protected override void DisposeObject(bool disposing) @@ -44,7 +47,27 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public async Task WriteAttachmentAsync(string name, Func handler) + public async Task WriteJsonAsync(string name, JToken value) + { + Guard.NotNullOrEmpty(name, nameof(name)); + + var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + + using (var stream = attachmentEntry.Open()) + { + using (var textWriter = new StreamWriter(stream)) + { + using (var jsonWriter = new JsonTextWriter(textWriter)) + { + await value.WriteToAsync(jsonWriter); + } + } + } + + writtenAttachments++; + } + + public async Task WriteBlobAsync(string name, Func handler) { Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNull(handler, nameof(handler)); @@ -67,7 +90,13 @@ namespace Squidex.Domain.Apps.Entities.Backup using (var stream = eventEntry.Open()) { - stream.SerializeAsJson(storedEvent); + using (var textWriter = new StreamWriter(stream)) + { + using (var jsonWriter = new JsonTextWriter(textWriter)) + { + Serializer.Serialize(jsonWriter, storedEvent); + } + } } writtenEvents++; diff --git a/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs b/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs new file mode 100644 index 000000000..c0f7ac827 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs @@ -0,0 +1,170 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class GuidMapper + { + private static readonly int GuidLength = Guid.Empty.ToString().Length; + private readonly List<(JObject Source, string NewKey, string OldKey)> mappings = new List<(JObject Source, string NewKey, string OldKey)>(); + private readonly Dictionary oldToNewGuid = new Dictionary(); + private readonly Dictionary newToOldGuid = new Dictionary(); + + public Guid NewGuid(Guid oldGuid) + { + return oldToNewGuid.GetOrDefault(oldGuid); + } + + public Guid OldGuid(Guid newGuid) + { + return newToOldGuid.GetOrDefault(newGuid); + } + + public string NewGuidString(string key) + { + if (Guid.TryParse(key, out var guid)) + { + return GenerateNewGuid(guid).ToString(); + } + + return null; + } + + public JToken NewGuids(JToken jToken) + { + var result = NewGuidsCore(jToken); + + if (mappings.Count > 0) + { + foreach (var mapping in mappings) + { + if (mapping.Source.TryGetValue(mapping.OldKey, out var value)) + { + mapping.Source.Remove(mapping.OldKey); + mapping.Source[mapping.NewKey] = value; + } + } + + mappings.Clear(); + } + + return result; + } + + private JToken NewGuidsCore(JToken jToken) + { + switch (jToken.Type) + { + case JTokenType.String: + if (TryConvertString(jToken.ToString(), out var result)) + { + return result; + } + + break; + case JTokenType.Guid: + return GenerateNewGuid((Guid)jToken); + case JTokenType.Object: + NewGuidsCore((JObject)jToken); + break; + case JTokenType.Array: + NewGuidsCore((JArray)jToken); + break; + } + + return jToken; + } + + private void NewGuidsCore(JArray jArray) + { + for (var i = 0; i < jArray.Count; i++) + { + jArray[i] = NewGuidsCore(jArray[i]); + } + } + + private void NewGuidsCore(JObject jObject) + { + foreach (var jProperty in jObject.Properties()) + { + var newValue = NewGuidsCore(jProperty.Value); + + if (!ReferenceEquals(newValue, jProperty.Value)) + { + jProperty.Value = newValue; + } + + if (TryConvertString(jProperty.Name, out var newKey)) + { + mappings.Add((jObject, newKey, jProperty.Name)); + } + } + } + + private bool TryConvertString(string value, out string result) + { + return TryGenerateNewGuidString(value, out result) || TryGenerateNewNamedId(value, out result); + } + + private bool TryGenerateNewGuidString(string value, out string result) + { + result = null; + + if (value.Length == GuidLength) + { + if (Guid.TryParse(value, out var guid)) + { + var newGuid = GenerateNewGuid(guid); + + result = newGuid.ToString(); + + return true; + } + } + + return false; + } + + private bool TryGenerateNewNamedId(string value, out string result) + { + result = null; + + if (value.Length > GuidLength && value[GuidLength] == ',') + { + if (Guid.TryParse(value.Substring(0, GuidLength), out var guid)) + { + var newGuid = GenerateNewGuid(guid); + + result = newGuid + value.Substring(GuidLength); + + return true; + } + } + + return false; + } + + private Guid GenerateNewGuid(Guid oldGuid) + { + return oldToNewGuid.GetOrAdd(oldGuid, GuidGenerator); + } + + private Guid GuidGenerator(Guid oldGuid) + { + var newGuid = Guid.NewGuid(); + + newToOldGuid[newGuid] = oldGuid; + + return newGuid; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index 70a7d5a50..134367e4b 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -34,6 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Backup private readonly IEventDataFormatter eventDataFormatter; private readonly IGrainFactory grainFactory; private readonly ISemanticLog log; + private readonly IStreamNameResolver streamNameResolver; private readonly IStore store; private RefToken actor; private RestoreState state = new RestoreState(); @@ -53,6 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Backup IGrainFactory grainFactory, IEnumerable handlers, ISemanticLog log, + IStreamNameResolver streamNameResolver, IStore store) { Guard.NotNull(assetStore, nameof(assetStore)); @@ -63,6 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Backup Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(handlers, nameof(handlers)); Guard.NotNull(store, nameof(store)); + Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); Guard.NotNull(log, nameof(log)); this.assetStore = assetStore; @@ -73,6 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Backup this.grainFactory = grainFactory; this.handlers = handlers; this.store = store; + this.streamNameResolver = streamNameResolver; this.log = log; } @@ -261,7 +265,7 @@ namespace Squidex.Domain.Apps.Entities.Backup private async Task ReadEventsAsync(BackupReader reader) { - await reader.ReadEventsAsync(async (storedEvent) => + await reader.ReadEventsAsync(streamNameResolver, async (storedEvent) => { var @event = eventDataFormatter.Parse(storedEvent.Data); diff --git a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs b/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs index e601232a8..f3fd17042 100644 --- a/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs @@ -14,6 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Backup.State { public sealed class RestoreStateJob : IRestoreJob { + [JsonProperty] + public string AppName { get; set; } + [JsonProperty] public Guid Id { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 40c2eb0d4..38b905248 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.contentRepository = contentRepository; } - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader) + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) { switch (@event.Payload) { diff --git a/src/Squidex.Domain.Apps.Entities/ICleanableAppGrain.cs b/src/Squidex.Domain.Apps.Entities/ICleanableAppGrain.cs deleted file mode 100644 index 4c9d97206..000000000 --- a/src/Squidex.Domain.Apps.Entities/ICleanableAppGrain.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Domain.Apps.Entities -{ - public interface ICleanableAppGrain : IGrainWithGuidKey - { - Task ClearAsync(); - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs index a6260d6c2..4700dc163 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Rules this.ruleEventRepository = ruleEventRepository; } - public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader) + public override Task RestoreEventAsync(Envelope @event, Guid appId, BackupReader reader, RefToken actor) { switch (@event.Payload) { diff --git a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs index b2c00d6b7..2351cd68d 100644 --- a/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs +++ b/src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs @@ -15,6 +15,9 @@ namespace Squidex.Infrastructure.States public string GetStreamName(Type aggregateType, string id) { + Guard.NotNullOrEmpty(id, nameof(id)); + Guard.NotNull(aggregateType, nameof(aggregateType)); + var typeName = char.ToLower(aggregateType.Name[0]) + aggregateType.Name.Substring(1); foreach (var suffix in Suffixes) @@ -29,5 +32,25 @@ namespace Squidex.Infrastructure.States return $"{typeName}-{id}"; } + + public string WithNewId(string streamName, Func idGenerator) + { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + Guard.NotNull(idGenerator, nameof(idGenerator)); + + var positionOfDash = streamName.LastIndexOf('-'); + + if (positionOfDash >= 0) + { + var newId = idGenerator(streamName.Substring(positionOfDash + 1)); + + if (!string.IsNullOrWhiteSpace(newId)) + { + streamName = $"{streamName.Substring(0, positionOfDash)}-{newId}"; + } + } + + return streamName; + } } } diff --git a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs b/src/Squidex.Infrastructure/States/IStreamNameResolver.cs index a8d13034c..02b15f2fb 100644 --- a/src/Squidex.Infrastructure/States/IStreamNameResolver.cs +++ b/src/Squidex.Infrastructure/States/IStreamNameResolver.cs @@ -12,5 +12,7 @@ namespace Squidex.Infrastructure.States public interface IStreamNameResolver { string GetStreamName(Type aggregateType, string id); + + string WithNewId(string streamName, Func idGenerator); } } diff --git a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs b/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs index 3b4154f0c..f1b4528cc 100644 --- a/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs +++ b/src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs @@ -13,12 +13,16 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models public sealed class RestoreRequest { /// - /// The url to the restore file. + /// The name of the app. /// [Required] - public Uri Url { get; set; } - [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] public string Name { get; set; } + + /// + /// The url to the restore file. + /// + [Required] + public Uri Url { get; set; } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Backup/EventStreamTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs similarity index 51% rename from tests/Squidex.Domain.Apps.Entities.Tests/Backup/EventStreamTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index 0085d1852..4db3961c5 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Backup/EventStreamTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -5,18 +5,30 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; +using FakeItEasy; using FluentAssertions; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Domain.Apps.Entities.Backup { - public class EventStreamTests + public class BackupReaderWriterTests { + private readonly IStreamNameResolver streamNameResolver = A.Fake(); + + public BackupReaderWriterTests() + { + A.CallTo(() => streamNameResolver.WithNewId(A.Ignored, A>.Ignored)) + .ReturnsLazily(new Func, string>((stream, idGenerator) => stream + "^2")); + } + [Fact] public async Task Should_write_and_read_events() { @@ -24,20 +36,26 @@ namespace Squidex.Domain.Apps.Entities.Backup var sourceEvents = new List(); - using (var writer = new BackupWriter(stream)) + using (var writer = new BackupWriter(stream, true)) { for (var i = 0; i < 1000; i++) { var eventData = new EventData { Type = i.ToString(), Metadata = i, Payload = i }; var eventStored = new StoredEvent("S", "1", 2, eventData); - if (i % 10 == 0) + if (i % 17 == 0) { - await writer.WriteAttachmentAsync(eventData.Type, innerStream => + await writer.WriteBlobAsync(eventData.Type, innerStream => { - return innerStream.WriteAsync(new byte[] { (byte)i }, 0, 1); + innerStream.WriteByte((byte)i); + + return TaskHelper.Done; }); } + else if (i % 37 == 0) + { + await writer.WriteJsonAsync(eventData.Type, $"JSON_{i}"); + } writer.WriteEvent(eventStored); @@ -51,13 +69,13 @@ namespace Squidex.Domain.Apps.Entities.Backup using (var reader = new BackupReader(stream)) { - await reader.ReadEventsAsync(async @event => + await reader.ReadEventsAsync(streamNameResolver, async @event => { var i = int.Parse(@event.Data.Type); - if (i % 10 == 0) + if (i % 17 == 0) { - await reader.ReadAttachmentAsync(@event.Data.Type, innerStream => + await reader.ReadBlobAsync(@event.Data.Type, innerStream => { var b = innerStream.ReadByte(); @@ -66,12 +84,25 @@ namespace Squidex.Domain.Apps.Entities.Backup return TaskHelper.Done; }); } + else if (i % 37 == 0) + { + var j = await reader.ReadJsonAttachmentAsync(@event.Data.Type); + + Assert.Equal($"JSON_{i}", j.ToString()); + } readEvents.Add(@event); }); } - readEvents.Should().BeEquivalentTo(sourceEvents); + var sourceEventsWithNewStreamName = + sourceEvents.Select(x => + new StoredEvent(streamNameResolver.WithNewId(x.StreamName, null), + x.EventPosition, + x.EventStreamNumber, + x.Data)).ToList(); + + readEvents.Should().BeEquivalentTo(sourceEventsWithNewStreamName); } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Backup/GuidMapperTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Backup/GuidMapperTests.cs new file mode 100644 index 000000000..fdab2731a --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Backup/GuidMapperTests.cs @@ -0,0 +1,161 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public class GuidMapperTests + { + private readonly Guid id1 = Guid.NewGuid(); + private readonly Guid id2 = Guid.NewGuid(); + private readonly GuidMapper map = new GuidMapper(); + + [Fact] + public void Should_map_guid_string_if_valid() + { + var result = map.NewGuidString(id1.ToString()); + + Assert.Equal(map.NewGuid(id1).ToString(), result); + } + + [Fact] + public void Should_return_null_if_mapping_invalid_guid_string() + { + var result = map.NewGuidString("invalid"); + + Assert.Null(result); + } + + [Fact] + public void Should_return_null_if_mapping_null_guid_string() + { + var result = map.NewGuidString(null); + + Assert.Null(result); + } + + [Fact] + public void Should_map_guid() + { + var result = map.NewGuids(id1); + + Assert.Equal(map.NewGuid(id1), result.Value()); + } + + [Fact] + public void Should_return_old_guid() + { + var newGuid = map.NewGuids(id1).Value(); + + Assert.Equal(id1, map.OldGuid(newGuid)); + } + + [Fact] + public void Should_map_guid_string() + { + var result = map.NewGuids(id1.ToString()); + + Assert.Equal(map.NewGuid(id1).ToString(), result.Value()); + } + + [Fact] + public void Should_map_named_id() + { + var result = map.NewGuids($"{id1},name"); + + Assert.Equal($"{map.NewGuid(id1)},name", result.Value()); + } + + [Fact] + public void Should_map_array_with_guid() + { + var obj = + new JObject( + new JProperty("k", + new JArray(id1, id1, id2))); + + map.NewGuids(obj); + + Assert.Equal(map.NewGuid(id1), obj["k"][0].Value()); + Assert.Equal(map.NewGuid(id1), obj["k"][1].Value()); + Assert.Equal(map.NewGuid(id2), obj["k"][2].Value()); + } + + [Fact] + public void Should_map_objects_with_guid_keys() + { + var obj = + new JObject( + new JProperty("k", + new JObject( + new JProperty(id1.ToString(), id1), + new JProperty(id2.ToString(), id2)))); + + map.NewGuids(obj); + + Assert.Equal(map.NewGuid(id1), obj["k"].Value(map.NewGuid(id1).ToString())); + Assert.Equal(map.NewGuid(id2), obj["k"].Value(map.NewGuid(id2).ToString())); + } + + [Fact] + public void Should_map_objects_with_guid() + { + var obj = + new JObject( + new JProperty("k", + new JObject( + new JProperty("v1", id1), + new JProperty("v2", id1), + new JProperty("v3", id2)))); + + map.NewGuids(obj); + + Assert.Equal(map.NewGuid(id1), obj["k"].Value("v1")); + Assert.Equal(map.NewGuid(id1), obj["k"].Value("v2")); + Assert.Equal(map.NewGuid(id2), obj["k"].Value("v3")); + } + + [Fact] + public void Should_map_objects_with_guid_string() + { + var obj = + new JObject( + new JProperty("k", + new JObject( + new JProperty("v1", id1.ToString()), + new JProperty("v2", id1.ToString()), + new JProperty("v3", id2.ToString())))); + + map.NewGuids(obj); + + Assert.Equal(map.NewGuid(id1).ToString(), obj["k"].Value("v1")); + Assert.Equal(map.NewGuid(id1).ToString(), obj["k"].Value("v2")); + Assert.Equal(map.NewGuid(id2).ToString(), obj["k"].Value("v3")); + } + + [Fact] + public void Should_map_objects_with_named_id() + { + var obj = + new JObject( + new JProperty("k", + new JObject( + new JProperty("v1", $"{id1},v1"), + new JProperty("v2", $"{id1},v2"), + new JProperty("v3", $"{id2},v3")))); + + map.NewGuids(obj); + + Assert.Equal($"{map.NewGuid(id1).ToString()},v1", obj["k"].Value("v1")); + Assert.Equal($"{map.NewGuid(id1).ToString()},v2", obj["k"].Value("v2")); + Assert.Equal($"{map.NewGuid(id2).ToString()},v3", obj["k"].Value("v3")); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs b/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs index 26f36ef21..d643ee9d1 100644 --- a/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs @@ -43,5 +43,35 @@ namespace Squidex.Infrastructure.States Assert.Equal($"myUser-{id}", name); } + + [Fact] + public void Should_calculate_new_stream_if_valid() + { + var oldStream = "myUser-123"; + + var newStream = sut.WithNewId(oldStream, x => "456"); + + Assert.Equal("myUser-456", newStream); + } + + [Fact] + public void Should_return_old_stream_if_format_not_valid() + { + var oldStream = "myUser|123"; + + var newStream = sut.WithNewId(oldStream, x => "456"); + + Assert.Equal(oldStream, newStream); + } + + [Fact] + public void Should_return_old_stream_if_new_id_not_valid() + { + var oldStream = "myUser-123"; + + var newStream = sut.WithNewId(oldStream, x => null); + + Assert.Equal(oldStream, newStream); + } } } diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index a5f760d35..be45577c0 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -50,28 +50,28 @@ namespace Migrate_01 public async Task RebuildAppsAsync() { - await store.ClearSnapshotsAsync(); + await store.GetSnapshotStore().ClearAsync(); await RebuildManyAsync("^app\\-", id => RebuildAsync(id, (e, s) => s.Apply(e))); } public async Task RebuildSchemasAsync() { - await store.ClearSnapshotsAsync(); + await store.GetSnapshotStore().ClearAsync(); await RebuildManyAsync("^schema\\-", id => RebuildAsync(id, (e, s) => s.Apply(e, fieldRegistry))); } public async Task RebuildRulesAsync() { - await store.ClearSnapshotsAsync(); + await store.GetSnapshotStore().ClearAsync(); await RebuildManyAsync("^rule\\-", id => RebuildAsync(id, (e, s) => s.Apply(e))); } public async Task RebuildAssetsAsync() { - await store.ClearSnapshotsAsync(); + await store.GetSnapshotStore().ClearAsync(); await RebuildManyAsync("^asset\\-", id => RebuildAsync(id, (e, s) => s.Apply(e))); } @@ -80,7 +80,7 @@ namespace Migrate_01 { using (localCache.StartContext()) { - await store.ClearSnapshotsAsync(); + await store.GetSnapshotStore().ClearAsync(); await RebuildManyAsync("^content\\-", async id => {