mirror of https://github.com/Squidex/squidex.git
5 changed files with 202 additions and 17 deletions
@ -0,0 +1,87 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.IO.Compression; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Backup |
|||
{ |
|||
public sealed class EventStreamReader : DisposableObjectBase |
|||
{ |
|||
private const int MaxItemsPerFolder = 1000; |
|||
private static readonly JsonSerializer JsonSerializer = JsonSerializer.CreateDefault(); |
|||
private readonly ZipArchive archive; |
|||
|
|||
public EventStreamReader(Stream stream) |
|||
{ |
|||
archive = new ZipArchive(stream, ZipArchiveMode.Read, true); |
|||
} |
|||
|
|||
protected override void DisposeObject(bool disposing) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
archive.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public async Task ReadEventsAsync(Func<EventData, Stream, Task> eventHandler) |
|||
{ |
|||
Guard.NotNull(eventHandler, nameof(eventHandler)); |
|||
|
|||
var readEvents = 0; |
|||
var readAttachments = 0; |
|||
|
|||
while (true) |
|||
{ |
|||
var eventFolder = readEvents / MaxItemsPerFolder; |
|||
var eventPath = $"events/{eventFolder}/{readEvents}.json"; |
|||
var eventEntry = archive.GetEntry(eventPath); |
|||
|
|||
if (eventEntry == null) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
EventData eventData; |
|||
|
|||
using (var stream = eventEntry.Open()) |
|||
{ |
|||
using (var textReader = new StreamReader(stream)) |
|||
{ |
|||
eventData = (EventData)JsonSerializer.Deserialize(textReader, typeof(EventData)); |
|||
} |
|||
} |
|||
|
|||
readEvents++; |
|||
|
|||
var attachmentFolder = readAttachments / MaxItemsPerFolder; |
|||
var attachmentPath = $"attachments/{attachmentFolder}/{readEvents}.blob"; |
|||
var attachmentEntry = archive.GetEntry(attachmentPath); |
|||
|
|||
if (attachmentEntry != null) |
|||
{ |
|||
using (var stream = attachmentEntry.Open()) |
|||
{ |
|||
await eventHandler(eventData, stream); |
|||
|
|||
readAttachments++; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
await eventHandler(eventData, null); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.Orleans; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Backup |
|||
{ |
|||
public sealed class RestoreGrain : GrainOfString |
|||
{ |
|||
public RestoreGrain() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using FluentAssertions; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Backup |
|||
{ |
|||
public class EventStreamTests |
|||
{ |
|||
public sealed class EventInfo |
|||
{ |
|||
public EventData Data { get; set; } |
|||
|
|||
public byte[] Attachment { get; set; } |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_write_and_read_events() |
|||
{ |
|||
var stream = new MemoryStream(); |
|||
|
|||
var sourceEvents = new List<EventInfo>(); |
|||
|
|||
for (var i = 0; i < 1000; i++) |
|||
{ |
|||
var eventData = new EventData { Type = i.ToString(), Metadata = i, Payload = i }; |
|||
var eventInfo = new EventInfo { Data = eventData }; |
|||
|
|||
if (i % 10 == 0) |
|||
{ |
|||
eventInfo.Attachment = new byte[] { (byte)i }; |
|||
} |
|||
|
|||
sourceEvents.Add(eventInfo); |
|||
} |
|||
|
|||
using (var reader = new EventStreamWriter(stream)) |
|||
{ |
|||
foreach (var @event in sourceEvents) |
|||
{ |
|||
if (@event.Attachment == null) |
|||
{ |
|||
await reader.WriteEventAsync(@event.Data); |
|||
} |
|||
else |
|||
{ |
|||
await reader.WriteEventAsync(@event.Data, s => s.WriteAsync(@event.Attachment, 0, 1)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
stream.Position = 0; |
|||
|
|||
var readEvents = new List<EventInfo>(); |
|||
|
|||
using (var reader = new EventStreamReader(stream)) |
|||
{ |
|||
await reader.ReadEventsAsync(async (eventData, attachment) => |
|||
{ |
|||
var eventInfo = new EventInfo { Data = eventData }; |
|||
|
|||
if (attachment != null) |
|||
{ |
|||
eventInfo.Attachment = new byte[1]; |
|||
|
|||
await attachment.ReadAsync(eventInfo.Attachment, 0, 1); |
|||
} |
|||
|
|||
readEvents.Add(eventInfo); |
|||
}); |
|||
} |
|||
|
|||
readEvents.Should().BeEquivalentTo(sourceEvents); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue