Files
LightlessClient/LightlessSync/LightlessConfiguration/ConfigurationMigrator.cs

344 lines
11 KiB
C#

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightlessSync.LightlessConfiguration;
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
LightlessConfigService lightlessConfigService) : IHostedService
{
private readonly ILogger<ConfigurationMigrator> _logger = logger;
public void Migrate()
{
if (transientConfigService.Current.Version == 0)
{
_logger.LogInformation("Migrating Transient Config V0 => V1");
transientConfigService.Current.TransientConfigs.Clear();
transientConfigService.Current.Version = 1;
transientConfigService.Save();
}
if (transientConfigService.Current.Version == 1)
{
_logger.LogInformation("Migrating Transient Config V1 => V2");
var totalRemoved = 0;
var configCount = 0;
var changedCount = 0;
foreach (var config in transientConfigService.Current.TransientConfigs.Values)
{
if (config.NormalizePaths(out var removed))
changedCount++;
totalRemoved += removed;
configCount++;
}
_logger.LogInformation("Transient config normalization: processed {count} entries, updated {updated}, removed {removed} paths", configCount, changedCount, totalRemoved);
transientConfigService.Current.Version = 2;
transientConfigService.Save();
}
if (serverConfigService.Current.Version == 1)
{
_logger.LogInformation("Migrating Server Config V1 => V2");
var centralServer = serverConfigService.Current.ServerStorage.Find(f => f.ServerName.Equals("Follow the light (Central Server EU)", StringComparison.Ordinal));
if (centralServer != null)
{
centralServer.ServerName = ApiController.MainServer;
}
serverConfigService.Current.Version = 2;
serverConfigService.Save();
}
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
}
public Task StartAsync(CancellationToken cancellationToken)
{
Migrate();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
{
var now = DateTime.UtcNow;
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
var tempChanged = false;
var tempNeedsSave = false;
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
{
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
var removed = root.Remove("OrphanableTempCollections");
removed |= root.Remove("OrphanableTempCollectionEntries");
if (removed)
{
try
{
string updatedJson = root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
lightlessConfigService.UpdateLastWriteTime();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
return;
}
}
if (ids.Count > 0 || entries.Count > 0)
{
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
}
}
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
{
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
if (tempRoot.Remove("OrphanableTempCollections"))
{
tempNeedsSave = true;
}
}
if (tempChanged || tempNeedsSave)
{
tempCollectionConfigService.Save();
}
}
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
{
root = new JsonObject();
ids = [];
entries = [];
if (!File.Exists(configPath))
{
return false;
}
try
{
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
return false;
}
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
if (idsNode == null && entriesNode == null)
{
return false;
}
ids = ParseGuidSet(idsNode);
entries = ParseEntries(entriesNode);
return true;
}
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
{
HashSet<Guid> ids = [];
if (node is not JsonArray array)
{
return ids;
}
foreach (JsonNode? item in array)
{
Guid id = ParseGuid(item);
if (id != Guid.Empty)
{
ids.Add(id);
}
}
return ids;
}
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
{
List<OrphanableTempCollectionEntry> entries = [];
if (node is not JsonArray array)
{
return entries;
}
foreach (JsonNode? item in array)
{
if (item is not JsonObject obj)
{
continue;
}
Guid id = ParseGuid(obj["Id"]);
if (id == Guid.Empty)
{
continue;
}
DateTime registeredAtUtc = DateTime.MinValue;
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
{
registeredAtUtc = parsed;
}
entries.Add(new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = registeredAtUtc
});
}
return entries;
}
private static Guid ParseGuid(JsonNode? node)
{
if (node is JsonValue value)
{
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
{
return parsed;
}
}
return Guid.Empty;
}
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
{
value = DateTime.MinValue;
if (node is not JsonValue val)
{
return false;
}
if (val.TryGetValue<DateTime>(out DateTime direct))
{
value = direct;
return true;
}
if (val.TryGetValue<string>(out string? stringValue)
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
{
value = parsed;
return true;
}
return false;
}
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
{
bool changed = false;
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
{
var entry = config.OrphanableTempCollectionEntries[i];
if (entry.Id == Guid.Empty)
{
config.OrphanableTempCollectionEntries.RemoveAt(i);
changed = true;
continue;
}
if (entryLookup.TryGetValue(entry.Id, out var existing))
{
if (entry.RegisteredAtUtc != DateTime.MinValue
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
{
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
changed = true;
}
config.OrphanableTempCollectionEntries.RemoveAt(i);
changed = true;
continue;
}
entryLookup[entry.Id] = entry;
}
foreach (OrphanableTempCollectionEntry entry in entries)
{
if (entry.Id == Guid.Empty)
{
continue;
}
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
{
var added = new OrphanableTempCollectionEntry
{
Id = entry.Id,
RegisteredAtUtc = entry.RegisteredAtUtc
};
config.OrphanableTempCollectionEntries.Add(added);
entryLookup[entry.Id] = added;
changed = true;
continue;
}
if (entry.RegisteredAtUtc != DateTime.MinValue
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
{
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
changed = true;
}
}
foreach (Guid id in ids)
{
if (id == Guid.Empty)
{
continue;
}
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
{
var added = new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = now
};
config.OrphanableTempCollectionEntries.Add(added);
entryLookup[id] = added;
changed = true;
continue;
}
if (existing.RegisteredAtUtc == DateTime.MinValue)
{
existing.RegisteredAtUtc = now;
changed = true;
}
}
return changed;
}
}