344 lines
11 KiB
C#
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;
|
|
}
|
|
}
|