Browse Source

Merge branch 'feature-restore' of github.com:Squidex/squidex into feature-restore

# Conflicts:
#	src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs
#	src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
#	src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
pull/311/head
Sebastian Stehle 7 years ago
parent
commit
aad61d3bf6
  1. 20
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  2. 21
      src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs
  4. 64
      src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs
  5. 41
      src/Squidex.Domain.Apps.Entities/Backup/BackupSerializer.cs
  6. 37
      src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs
  7. 170
      src/Squidex.Domain.Apps.Entities/Backup/GuidMapper.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  9. 3
      src/Squidex.Domain.Apps.Entities/Backup/State/RestoreStateJob.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs
  11. 17
      src/Squidex.Domain.Apps.Entities/ICleanableAppGrain.cs
  12. 2
      src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs
  13. 23
      src/Squidex.Infrastructure/States/DefaultStreamNameResolver.cs
  14. 2
      src/Squidex.Infrastructure/States/IStreamNameResolver.cs
  15. 10
      src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs
  16. 49
      tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs
  17. 161
      tests/Squidex.Domain.Apps.Entities.Tests/Backup/GuidMapperTests.cs
  18. 30
      tests/Squidex.Infrastructure.Tests/States/DefaultStreamNameResolverTests.cs
  19. 10
      tools/Migrate_01/Rebuilder.cs

20
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<string> activeUsers = new HashSet<string>();
private readonly Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
private readonly Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
private Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
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<Dictionary<string, string>>();
}
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)

21
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<AssetState, AssetGrain>(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<TagSet>();
var tags = await reader.ReadJsonAttachmentAsync(TagsFile);
return tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags);
});
await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags.ToObject<TagSet>());
}
private Task BackupTagsAsync(Guid appId, BackupWriter writer)
{
return writer.WriteAttachmentAsync(TagsFile, async stream =>
private async Task BackupTagsAsync(Guid appId, BackupWriter writer)
{
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
{

2
src/Squidex.Domain.Apps.Entities/Backup/BackupHandlerWithStore.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
protected Task RemoveSnapshotAsync<TState>(Guid id)
{
return store.RemoveSnapshotAsync<TState>(id);
return store.RemoveSnapshotAsync<Guid, TState>(id);
}
protected async Task RebuildManyAsync(IEnumerable<Guid> ids, Func<Guid, Task> action)

64
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<Stream, Task> handler)
public Guid OldGuid(Guid newId)
{
return guidMapper.OldGuid(newId);
}
public async Task<JToken> 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<Stream, Task> 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<StoredEvent, Task> handler)
public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, Func<StoredEvent, Task> handler)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(streamNameResolver, nameof(streamNameResolver));
while (true)
{
@ -79,10 +121,26 @@ namespace Squidex.Domain.Apps.Entities.Backup
using (var stream = eventEntry.Open())
{
var storedEvent = stream.DeserializeAsJson<StoredEvent>();
using (var textReader = new StreamReader(stream))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var storedEvent = Serializer.Deserialize<StoredEvent>(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);
}
}
}
readEvents++;
}

41
src/Squidex.Domain.Apps.Entities/Backup/BackupSerializer.cs

@ -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<T>(this Stream stream, T value)
{
using (var writer = new StreamWriter(stream))
{
JsonSerializer.Serialize(writer, value);
}
}
public static T DeserializeAsJson<T>(this Stream stream)
{
using (var reader = new StreamReader(stream))
{
return (T)JsonSerializer.Deserialize(reader, typeof(T));
}
}
public static void DeserializeAsJson<T>(this Stream stream, T result)
{
using (var reader = new StreamReader(stream))
{
JsonSerializer.Populate(reader, result);
}
}
}
}

37
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<Stream, Task> 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<Stream, Task> 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++;

170
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<Guid, Guid> oldToNewGuid = new Dictionary<Guid, Guid>();
private readonly Dictionary<Guid, Guid> newToOldGuid = new Dictionary<Guid, Guid>();
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;
}
}
}

6
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<string> store;
private RefToken actor;
private RestoreState state = new RestoreState();
@ -53,6 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
IGrainFactory grainFactory,
IEnumerable<BackupHandler> handlers,
ISemanticLog log,
IStreamNameResolver streamNameResolver,
IStore<string> 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);

3
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; }

2
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<IEvent> @event, Guid appId, BackupReader reader)
public override Task RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor)
{
switch (@event.Payload)
{

17
src/Squidex.Domain.Apps.Entities/ICleanableAppGrain.cs

@ -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();
}
}

2
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<IEvent> @event, Guid appId, BackupReader reader)
public override Task RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor)
{
switch (@event.Payload)
{

23
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<string, string> 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;
}
}
}

2
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<string, string> idGenerator);
}
}

10
src/Squidex/Areas/Api/Controllers/Backups/Models/RestoreRequest.cs

@ -13,12 +13,16 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models
public sealed class RestoreRequest
{
/// <summary>
/// The url to the restore file.
/// The name of the app.
/// </summary>
[Required]
public Uri Url { get; set; }
[RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; }
/// <summary>
/// The url to the restore file.
/// </summary>
[Required]
public Uri Url { get; set; }
}
}

49
tests/Squidex.Domain.Apps.Entities.Tests/Backup/EventStreamTests.cs → 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<IStreamNameResolver>();
public BackupReaderWriterTests()
{
A.CallTo(() => streamNameResolver.WithNewId(A<string>.Ignored, A<Func<string, string>>.Ignored))
.ReturnsLazily(new Func<string, Func<string, string>, 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<StoredEvent>();
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);
}
}
}

161
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<Guid>());
}
[Fact]
public void Should_return_old_guid()
{
var newGuid = map.NewGuids(id1).Value<Guid>();
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<string>());
}
[Fact]
public void Should_map_named_id()
{
var result = map.NewGuids($"{id1},name");
Assert.Equal($"{map.NewGuid(id1)},name", result.Value<string>());
}
[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<Guid>());
Assert.Equal(map.NewGuid(id1), obj["k"][1].Value<Guid>());
Assert.Equal(map.NewGuid(id2), obj["k"][2].Value<Guid>());
}
[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<Guid>(map.NewGuid(id1).ToString()));
Assert.Equal(map.NewGuid(id2), obj["k"].Value<Guid>(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<Guid>("v1"));
Assert.Equal(map.NewGuid(id1), obj["k"].Value<Guid>("v2"));
Assert.Equal(map.NewGuid(id2), obj["k"].Value<Guid>("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<string>("v1"));
Assert.Equal(map.NewGuid(id1).ToString(), obj["k"].Value<string>("v2"));
Assert.Equal(map.NewGuid(id2).ToString(), obj["k"].Value<string>("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<string>("v1"));
Assert.Equal($"{map.NewGuid(id1).ToString()},v2", obj["k"].Value<string>("v2"));
Assert.Equal($"{map.NewGuid(id2).ToString()},v3", obj["k"].Value<string>("v3"));
}
}
}

30
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);
}
}
}

10
tools/Migrate_01/Rebuilder.cs

@ -50,28 +50,28 @@ namespace Migrate_01
public async Task RebuildAppsAsync()
{
await store.ClearSnapshotsAsync<AppState>();
await store.GetSnapshotStore<AppState>().ClearAsync();
await RebuildManyAsync("^app\\-", id => RebuildAsync<AppState, AppGrain>(id, (e, s) => s.Apply(e)));
}
public async Task RebuildSchemasAsync()
{
await store.ClearSnapshotsAsync<SchemaState>();
await store.GetSnapshotStore<SchemaState>().ClearAsync();
await RebuildManyAsync("^schema\\-", id => RebuildAsync<SchemaState, SchemaGrain>(id, (e, s) => s.Apply(e, fieldRegistry)));
}
public async Task RebuildRulesAsync()
{
await store.ClearSnapshotsAsync<RuleState>();
await store.GetSnapshotStore<RuleState>().ClearAsync();
await RebuildManyAsync("^rule\\-", id => RebuildAsync<RuleState, RuleGrain>(id, (e, s) => s.Apply(e)));
}
public async Task RebuildAssetsAsync()
{
await store.ClearSnapshotsAsync<AssetState>();
await store.GetSnapshotStore<AssetState>().ClearAsync();
await RebuildManyAsync("^asset\\-", id => RebuildAsync<AssetState, AssetGrain>(id, (e, s) => s.Apply(e)));
}
@ -80,7 +80,7 @@ namespace Migrate_01
{
using (localCache.StartContext())
{
await store.ClearSnapshotsAsync<ContentState>();
await store.GetSnapshotStore<ContentState>().ClearAsync();
await RebuildManyAsync("^content\\-", async id =>
{

Loading…
Cancel
Save