Add local JSON account import and export
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
@@ -600,6 +601,7 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
||||
[JsonSerializable(typeof(ApiEnvelope<UserMailboxSyncListDto>))]
|
||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||
[JsonSerializable(typeof(ReplaceUserMailboxesRequestDto))]
|
||||
[JsonSerializable(typeof(List<UserMailboxSyncItemDto>))]
|
||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||
|
||||
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -18,6 +21,7 @@ namespace Wino.Services;
|
||||
public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
{
|
||||
private const int DefaultMaxConcurrentClients = 5;
|
||||
private const int LocalExportVersion = 1;
|
||||
|
||||
private readonly IWinoAccountProfileService _profileService;
|
||||
private readonly IPreferencesService _preferencesService;
|
||||
@@ -35,37 +39,159 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
|
||||
public async Task<WinoAccountSyncExportResult> ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var exportedMailboxCount = 0;
|
||||
var preparedExport = await PrepareExportAsync(selection).ConfigureAwait(false);
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
if (selection.IncludePreferences && preparedExport.PreferencesJson != null)
|
||||
{
|
||||
await _profileService.SaveSettingsAsync(_preferencesService.ExportPreferences(), cancellationToken).ConfigureAwait(false);
|
||||
await _profileService.SaveSettingsAsync(preparedExport.PreferencesJson, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
var request = new ReplaceUserMailboxesRequestDto
|
||||
{
|
||||
Mailboxes = accounts
|
||||
.OrderBy(a => a.Order)
|
||||
.Select(MapMailbox)
|
||||
.ToList()
|
||||
Mailboxes = preparedExport.Mailboxes
|
||||
};
|
||||
|
||||
await _profileService.ReplaceMailboxesAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
exportedMailboxCount = request.Mailboxes.Count;
|
||||
}
|
||||
|
||||
return new WinoAccountSyncExportResult
|
||||
return preparedExport.ExportResult;
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncFileExportResult> ExportToJsonAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var preparedExport = await PrepareExportAsync(selection).ConfigureAwait(false);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
|
||||
{
|
||||
IncludedPreferences = selection.IncludePreferences,
|
||||
IncludedAccounts = selection.IncludeAccounts,
|
||||
ExportedMailboxCount = exportedMailboxCount
|
||||
writer.WriteStartObject();
|
||||
writer.WriteNumber("version", LocalExportVersion);
|
||||
writer.WriteString("exportedAtUtc", DateTime.UtcNow);
|
||||
writer.WriteBoolean("includesPreferences", preparedExport.ExportResult.IncludedPreferences);
|
||||
writer.WriteBoolean("includesAccounts", preparedExport.ExportResult.IncludedAccounts);
|
||||
|
||||
writer.WritePropertyName("preferences");
|
||||
if (!string.IsNullOrWhiteSpace(preparedExport.PreferencesJson))
|
||||
{
|
||||
using var preferencesDocument = JsonDocument.Parse(preparedExport.PreferencesJson);
|
||||
preferencesDocument.RootElement.WriteTo(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
|
||||
writer.WritePropertyName("mailboxes");
|
||||
JsonSerializer.Serialize(writer, preparedExport.Mailboxes, WinoAccountApiJsonContext.Default.ListUserMailboxSyncItemDto);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
return new WinoAccountSyncFileExportResult
|
||||
{
|
||||
JsonContent = Encoding.UTF8.GetString(stream.ToArray()),
|
||||
ExportResult = preparedExport.ExportResult
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncImportResult> ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? settingsJson = null;
|
||||
List<UserMailboxSyncItemDto> orderedMailboxes = [];
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
{
|
||||
settingsJson = await _profileService.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var mailboxes = await _profileService.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||
orderedMailboxes = mailboxes.Mailboxes
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.ThenBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return await ImportDataAsync(selection, settingsJson, orderedMailboxes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<WinoAccountSyncImportResult> ImportFromJsonAsync(string jsonContent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
jsonContent = TrimUtf8Bom(jsonContent);
|
||||
|
||||
using var document = JsonDocument.Parse(jsonContent);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new JsonException("Invalid root element.");
|
||||
}
|
||||
|
||||
string? settingsJson = null;
|
||||
if (document.RootElement.TryGetProperty("preferences", out var preferencesElement))
|
||||
{
|
||||
settingsJson = preferencesElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => preferencesElement.GetRawText(),
|
||||
JsonValueKind.String => preferencesElement.GetString(),
|
||||
JsonValueKind.Null or JsonValueKind.Undefined => null,
|
||||
_ => throw new JsonException("Invalid preferences payload.")
|
||||
};
|
||||
}
|
||||
|
||||
var mailboxes = new List<UserMailboxSyncItemDto>();
|
||||
if (document.RootElement.TryGetProperty("mailboxes", out var mailboxesElement))
|
||||
{
|
||||
if (mailboxesElement.ValueKind is not (JsonValueKind.Array or JsonValueKind.Null or JsonValueKind.Undefined))
|
||||
{
|
||||
throw new JsonException("Invalid mailboxes payload.");
|
||||
}
|
||||
|
||||
if (mailboxesElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
mailboxes = JsonSerializer.Deserialize(mailboxesElement.GetRawText(), WinoAccountApiJsonContext.Default.ListUserMailboxSyncItemDto) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
var selection = new WinoAccountSyncSelection(
|
||||
IncludePreferences: !string.IsNullOrWhiteSpace(settingsJson),
|
||||
IncludeAccounts: mailboxes.Count > 0);
|
||||
|
||||
return await ImportDataAsync(selection, settingsJson, mailboxes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<PreparedSyncExport> PrepareExportAsync(WinoAccountSyncSelection selection)
|
||||
{
|
||||
var preferencesJson = selection.IncludePreferences
|
||||
? _preferencesService.ExportPreferences()
|
||||
: null;
|
||||
|
||||
var mailboxes = selection.IncludeAccounts
|
||||
? (await _accountService.GetAccountsAsync().ConfigureAwait(false))
|
||||
.OrderBy(a => a.Order)
|
||||
.Select(MapMailbox)
|
||||
.ToList()
|
||||
: [];
|
||||
|
||||
return new PreparedSyncExport(
|
||||
preferencesJson,
|
||||
mailboxes,
|
||||
new WinoAccountSyncExportResult
|
||||
{
|
||||
IncludedPreferences = selection.IncludePreferences,
|
||||
IncludedAccounts = selection.IncludeAccounts,
|
||||
ExportedMailboxCount = mailboxes.Count
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<WinoAccountSyncImportResult> ImportDataAsync(
|
||||
WinoAccountSyncSelection selection,
|
||||
string? settingsJson,
|
||||
List<UserMailboxSyncItemDto> mailboxes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new WinoAccountSyncImportResult
|
||||
{
|
||||
@@ -73,30 +199,25 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
IncludedAccounts = selection.IncludeAccounts
|
||||
};
|
||||
|
||||
if (selection.IncludePreferences)
|
||||
if (selection.IncludePreferences && !string.IsNullOrWhiteSpace(settingsJson))
|
||||
{
|
||||
var settingsJson = await _profileService.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(settingsJson))
|
||||
var (appliedCount, failedCount) = _preferencesService.ImportPreferences(settingsJson);
|
||||
result = new WinoAccountSyncImportResult
|
||||
{
|
||||
var (appliedCount, failedCount) = _preferencesService.ImportPreferences(settingsJson);
|
||||
result = new WinoAccountSyncImportResult
|
||||
{
|
||||
IncludedPreferences = result.IncludedPreferences,
|
||||
IncludedAccounts = result.IncludedAccounts,
|
||||
HadRemotePreferences = true,
|
||||
AppliedPreferenceCount = appliedCount,
|
||||
FailedPreferenceCount = failedCount,
|
||||
ImportedMailboxCount = result.ImportedMailboxCount,
|
||||
SkippedDuplicateMailboxCount = result.SkippedDuplicateMailboxCount,
|
||||
RemoteMailboxCount = result.RemoteMailboxCount
|
||||
};
|
||||
}
|
||||
IncludedPreferences = result.IncludedPreferences,
|
||||
IncludedAccounts = result.IncludedAccounts,
|
||||
HadRemotePreferences = true,
|
||||
AppliedPreferenceCount = appliedCount,
|
||||
FailedPreferenceCount = failedCount,
|
||||
ImportedMailboxCount = result.ImportedMailboxCount,
|
||||
SkippedDuplicateMailboxCount = result.SkippedDuplicateMailboxCount,
|
||||
RemoteMailboxCount = result.RemoteMailboxCount
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.IncludeAccounts)
|
||||
{
|
||||
var mailboxes = await _profileService.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||
var orderedMailboxes = mailboxes.Mailboxes
|
||||
var orderedMailboxes = mailboxes
|
||||
.OrderBy(a => a.SortOrder)
|
||||
.ThenBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
@@ -288,4 +409,14 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
|
||||
private static string CreateMailboxKey(string? address, int providerType)
|
||||
=> $"{address?.Trim().ToLowerInvariant()}|{providerType}";
|
||||
|
||||
private static string TrimUtf8Bom(string jsonContent)
|
||||
=> !string.IsNullOrEmpty(jsonContent) && jsonContent[0] == '\uFEFF'
|
||||
? jsonContent[1..]
|
||||
: jsonContent;
|
||||
|
||||
private sealed record PreparedSyncExport(
|
||||
string? PreferencesJson,
|
||||
List<UserMailboxSyncItemDto> Mailboxes,
|
||||
WinoAccountSyncExportResult ExportResult);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user