From e624c5b6289ee12ac209a90e673ebf3ee0046826 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 17 Feb 2023 18:36:22 +0100 Subject: [PATCH] Db: Migrate markers to db --- .../Configuration/ConfigurationManager.cs | 4 +- .../Configuration/Legacy/JsonFloorState.cs | 14 +- .../Configuration/Legacy/JsonMigration.cs | 141 ++++++++++++++++++ Pal.Client/Database/ClientLocation.cs | 39 +++++ Pal.Client/Database/ImportHistory.cs | 3 + ...30217160342_AddClientLocations.Designer.cs | 136 +++++++++++++++++ .../20230217160342_AddClientLocations.cs | 100 +++++++++++++ .../PalClientContextModelSnapshot.cs | 93 +++++++++++- Pal.Client/Database/PalClientContext.cs | 10 ++ Pal.Client/Database/RemoteEncounter.cs | 41 +++++ Pal.Client/DependencyInjectionContext.cs | 9 +- Pal.Client/Pal.Client.csproj | 2 +- Pal.Common/Pal.Common.csproj | 2 +- 13 files changed, 582 insertions(+), 12 deletions(-) create mode 100644 Pal.Client/Configuration/Legacy/JsonMigration.cs create mode 100644 Pal.Client/Database/ClientLocation.cs create mode 100644 Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs create mode 100644 Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs create mode 100644 Pal.Client/Database/RemoteEncounter.cs diff --git a/Pal.Client/Configuration/ConfigurationManager.cs b/Pal.Client/Configuration/ConfigurationManager.cs index 1c28c1d..f8c584f 100644 --- a/Pal.Client/Configuration/ConfigurationManager.cs +++ b/Pal.Client/Configuration/ConfigurationManager.cs @@ -28,8 +28,6 @@ namespace Pal.Client.Configuration _logger = logger; _pluginInterface = pluginInterface; _serviceProvider = serviceProvider; - - Migrate(); } private string ConfigPath => Path.Join(_pluginInterface.GetPluginConfigDirectory(), "palace-pal.config.json"); @@ -54,7 +52,7 @@ namespace Pal.Client.Configuration #pragma warning disable CS0612 #pragma warning disable CS0618 - private void Migrate() + public void Migrate() { if (_pluginInterface.ConfigFile.Exists) { diff --git a/Pal.Client/Configuration/Legacy/JsonFloorState.cs b/Pal.Client/Configuration/Legacy/JsonFloorState.cs index 1199b18..5742b2f 100644 --- a/Pal.Client/Configuration/Legacy/JsonFloorState.cs +++ b/Pal.Client/Configuration/Legacy/JsonFloorState.cs @@ -18,18 +18,18 @@ namespace Pal.Client.Configuration.Legacy private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private const int CurrentVersion = 4; - private static string _pluginConfigDirectory; - private static EMode _mode = EMode.Online; // might not be true, but this is 'less strict filtering' for migrations + private static string _pluginConfigDirectory = null!; + private static readonly EMode _mode = EMode.Online; // might not be true, but this is 'less strict filtering' for migrations internal static void SetContextProperties(string pluginConfigDirectory) { _pluginConfigDirectory = pluginConfigDirectory; } - public uint TerritoryType { get; set; } + public ushort TerritoryType { get; set; } public ConcurrentBag Markers { get; set; } = new(); - public JsonFloorState(uint territoryType) + public JsonFloorState(ushort territoryType) { TerritoryType = territoryType; } @@ -44,7 +44,7 @@ namespace Pal.Client.Configuration.Legacy Markers = new ConcurrentBag(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0)); } - public static JsonFloorState? Load(uint territoryType) + public static JsonFloorState? Load(ushort territoryType) { string path = GetSaveLocation(territoryType); if (!File.Exists(path)) @@ -136,6 +136,10 @@ namespace Pal.Client.Configuration.Legacy { foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues()) { + // we never had markers for eureka orthos, so don't bother + if (territory > ETerritoryType.HeavenOnHigh_91_100) + break; + JsonFloorState? localState = Load((ushort)territory); if (localState != null) action(localState); diff --git a/Pal.Client/Configuration/Legacy/JsonMigration.cs b/Pal.Client/Configuration/Legacy/JsonMigration.cs new file mode 100644 index 0000000..c73e7ea --- /dev/null +++ b/Pal.Client/Configuration/Legacy/JsonMigration.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Plugin; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Pal.Client.Database; +using Pal.Common; + +namespace Pal.Client.Configuration.Legacy +{ + /// + /// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist. + /// + internal sealed class JsonMigration + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly DalamudPluginInterface _pluginInterface; + + public JsonMigration(ILogger logger, IServiceScopeFactory serviceScopeFactory, + DalamudPluginInterface pluginInterface) + { + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; + _pluginInterface = pluginInterface; + } + +#pragma warning disable CS0612 + public async Task MigrateAsync(CancellationToken cancellationToken) + { + List floorsToMigrate = new(); + JsonFloorState.ForEach(floorsToMigrate.Add); + + if (floorsToMigrate.Count == 0) + { + _logger.LogInformation("Found no floors to migrate"); + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + await using var scope = _serviceScopeFactory.CreateAsyncScope(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + + var fileStream = new FileStream( + Path.Join(_pluginInterface.GetPluginConfigDirectory(), + $"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"), + FileMode.CreateNew); + using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) + { + IReadOnlyDictionary imports = + await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken); + + foreach (var floorToMigrate in floorsToMigrate) + { + backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(), + Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize); + await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken); + } + + await dbContext.SaveChangesAsync(cancellationToken); + } + + _logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count); + foreach (var floorToMigrate in floorsToMigrate) + File.Delete(floorToMigrate.GetSaveLocation()); + } + + /// Whether to archive this file once complete + private async Task MigrateFloor( + PalClientContext dbContext, + JsonFloorState floorToMigrate, + IReadOnlyDictionary imports, + CancellationToken cancellationToken) + { + using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}"); + if (floorToMigrate.Markers.Count == 0) + { + _logger.LogInformation("Skipping migration, floor has no markers"); + } + + if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType, + cancellationToken)) + { + _logger.LogInformation("Skipping migration, floor already has locations in the database"); + return; + } + + _logger.LogInformation("Starting migration of {Count} locations", floorToMigrate.Markers.Count); + List clientLocations = floorToMigrate.Markers + .Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard) + .Select(o => + { + var clientLocation = new ClientLocation + { + TerritoryType = floorToMigrate.TerritoryType, + Type = MapJsonType(o.Type), + X = o.Position.X, + Y = o.Position.Y, + Z = o.Position.Z, + Seen = o.Seen, + + // the SelectMany is misleading here, each import has either 0 or 1 associated db entry with that id + ImportedBy = o.Imports + .Select(importId => + imports.TryGetValue(importId, out ImportHistory? import) ? import : null) + .Where(import => import != null) + .Cast() + .Distinct() + .ToList(), + }; + + clientLocation.RemoteEncounters = o.RemoteSeenOn + .Select(accountId => new RemoteEncounter(clientLocation, accountId)) + .ToList(); + + return clientLocation; + }).ToList(); + await dbContext.Locations.AddRangeAsync(clientLocations, cancellationToken); + + _logger.LogInformation("Migrated {Count} locations", clientLocations.Count); + } + + private ClientLocation.EType MapJsonType(JsonMarker.EType type) + { + return type switch + { + JsonMarker.EType.Trap => ClientLocation.EType.Trap, + JsonMarker.EType.Hoard => ClientLocation.EType.Hoard, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } +#pragma warning restore CS0612 + } +} diff --git a/Pal.Client/Database/ClientLocation.cs b/Pal.Client/Database/ClientLocation.cs new file mode 100644 index 0000000..ac0714f --- /dev/null +++ b/Pal.Client/Database/ClientLocation.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Pal.Client.Database +{ + internal sealed class ClientLocation + { + [Key] public int LocalId { get; set; } + public ushort TerritoryType { get; set; } + public EType Type { get; set; } + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + /// + /// Whether we have encountered the trap/coffer at this location in-game. + /// + public bool Seen { get; set; } + + /// + /// Which account ids this marker was seen. This is a list merely to support different remote endpoints + /// (where each server would assign you a different id). + /// + public List RemoteEncounters { get; set; } = new(); + + /// + /// To keep track of which markers were imported through a downloaded file, we save the associated import-id. + /// + /// Importing another file for the same remote server will remove the old import-id, and add the new import-id here. + /// + public List ImportedBy { get; set; } = new(); + + public enum EType + { + Trap = 1, + Hoard = 2, + } + } +} diff --git a/Pal.Client/Database/ImportHistory.cs b/Pal.Client/Database/ImportHistory.cs index 33e74f4..535b502 100644 --- a/Pal.Client/Database/ImportHistory.cs +++ b/Pal.Client/Database/ImportHistory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Pal.Client.Database { @@ -8,5 +9,7 @@ namespace Pal.Client.Database public string? RemoteUrl { get; set; } public DateTime ExportedAt { get; set; } public DateTime ImportedAt { get; set; } + + public List ImportedLocations { get; set; } = new(); } } diff --git a/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs new file mode 100644 index 0000000..a7b24d6 --- /dev/null +++ b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs @@ -0,0 +1,136 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Pal.Client.Database; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + [DbContext(typeof(PalClientContext))] + [Migration("20230217160342_AddClientLocations")] + partial class AddClientLocations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ExportedAt") + .HasColumnType("TEXT"); + + b.Property("ImportedAt") + .HasColumnType("TEXT"); + + b.Property("RemoteUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Imports"); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters"); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany() + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs new file mode 100644 index 0000000..7f4580f --- /dev/null +++ b/Pal.Client/Database/Migrations/20230217160342_AddClientLocations.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Pal.Client.Database.Migrations +{ + /// + public partial class AddClientLocations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Locations", + columns: table => new + { + LocalId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TerritoryType = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + X = table.Column(type: "REAL", nullable: false), + Y = table.Column(type: "REAL", nullable: false), + Z = table.Column(type: "REAL", nullable: false), + Seen = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Locations", x => x.LocalId); + }); + + migrationBuilder.CreateTable( + name: "LocationImports", + columns: table => new + { + ImportedById = table.Column(type: "TEXT", nullable: false), + ImportedLocationsLocalId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LocationImports", x => new { x.ImportedById, x.ImportedLocationsLocalId }); + table.ForeignKey( + name: "FK_LocationImports_Imports_ImportedById", + column: x => x.ImportedById, + principalTable: "Imports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_LocationImports_Locations_ImportedLocationsLocalId", + column: x => x.ImportedLocationsLocalId, + principalTable: "Locations", + principalColumn: "LocalId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RemoteEncounters", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ClientLocationId = table.Column(type: "INTEGER", nullable: false), + AccountId = table.Column(type: "TEXT", maxLength: 13, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RemoteEncounters", x => x.Id); + table.ForeignKey( + name: "FK_RemoteEncounters_Locations_ClientLocationId", + column: x => x.ClientLocationId, + principalTable: "Locations", + principalColumn: "LocalId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_LocationImports_ImportedLocationsLocalId", + table: "LocationImports", + column: "ImportedLocationsLocalId"); + + migrationBuilder.CreateIndex( + name: "IX_RemoteEncounters_ClientLocationId", + table: "RemoteEncounters", + column: "ClientLocationId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LocationImports"); + + migrationBuilder.DropTable( + name: "RemoteEncounters"); + + migrationBuilder.DropTable( + name: "Locations"); + } + } +} diff --git a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs index d6be610..e0813c3 100644 --- a/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs +++ b/Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs @@ -17,6 +17,50 @@ namespace Pal.Client.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.Property("ImportedById") + .HasColumnType("TEXT"); + + b.Property("ImportedLocationsLocalId") + .HasColumnType("INTEGER"); + + b.HasKey("ImportedById", "ImportedLocationsLocalId"); + + b.HasIndex("ImportedLocationsLocalId"); + + b.ToTable("LocationImports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.ClientLocation", b => + { + b.Property("LocalId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Seen") + .HasColumnType("INTEGER"); + + b.Property("TerritoryType") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("LocalId"); + + b.ToTable("Locations", (string)null); + }); + modelBuilder.Entity("Pal.Client.Database.ImportHistory", b => { b.Property("Id") @@ -34,7 +78,54 @@ namespace Pal.Client.Database.Migrations b.HasKey("Id"); - b.ToTable("Imports"); + b.ToTable("Imports", (string)null); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("TEXT"); + + b.Property("ClientLocationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClientLocationId"); + + b.ToTable("RemoteEncounters", (string)null); + }); + + modelBuilder.Entity("ClientLocationImportHistory", b => + { + b.HasOne("Pal.Client.Database.ImportHistory", null) + .WithMany() + .HasForeignKey("ImportedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Pal.Client.Database.ClientLocation", null) + .WithMany() + .HasForeignKey("ImportedLocationsLocalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b => + { + b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation") + .WithMany() + .HasForeignKey("ClientLocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientLocation"); }); #pragma warning restore 612, 618 } diff --git a/Pal.Client/Database/PalClientContext.cs b/Pal.Client/Database/PalClientContext.cs index b1c65e3..8cbd6a6 100644 --- a/Pal.Client/Database/PalClientContext.cs +++ b/Pal.Client/Database/PalClientContext.cs @@ -4,11 +4,21 @@ namespace Pal.Client.Database { internal class PalClientContext : DbContext { + public DbSet Locations { get; set; } = null!; public DbSet Imports { get; set; } = null!; + public DbSet RemoteEncounters { get; set; } = null!; public PalClientContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(o => o.ImportedBy) + .WithMany(o => o.ImportedLocations) + .UsingEntity(o => o.ToTable("LocationImports")); + } } } diff --git a/Pal.Client/Database/RemoteEncounter.cs b/Pal.Client/Database/RemoteEncounter.cs new file mode 100644 index 0000000..0a0f0a1 --- /dev/null +++ b/Pal.Client/Database/RemoteEncounter.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using Pal.Client.Extensions; +using Pal.Client.Net; + +namespace Pal.Client.Database +{ + /// + /// To avoid sending too many requests to the server, we cache which locations have been seen + /// locally. These never expire, and locations which have been seen with a specific account + /// are never sent to the server again. + /// + /// To be marked as seen, it needs to be essentially processed by . + /// + internal sealed class RemoteEncounter + { + [Key] + public int Id { get; private set; } + + public int ClientLocationId { get; private set; } + public ClientLocation ClientLocation { get; private set; } = null!; + + /// + /// Partial account id. This is partially unique - however problems would (in theory) + /// only occur once you have two account-ids where the first 13 characters are equal. + /// + [MaxLength(13)] + public string AccountId { get; private set; } + + private RemoteEncounter(int clientLocationId, string accountId) + { + ClientLocationId = clientLocationId; + AccountId = accountId; + } + + public RemoteEncounter(ClientLocation clientLocation, string accountId) + { + ClientLocation = clientLocation; + AccountId = accountId.ToPartialId(); + } + } +} diff --git a/Pal.Client/DependencyInjectionContext.cs b/Pal.Client/DependencyInjectionContext.cs index c8e3f0c..eec832d 100644 --- a/Pal.Client/DependencyInjectionContext.cs +++ b/Pal.Client/DependencyInjectionContext.cs @@ -96,6 +96,7 @@ namespace Pal.Client _sqliteConnectionString = $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}"; services.AddDbContext(o => o.UseSqlite(_sqliteConnectionString)); + services.AddTransient(); // plugin-specific services.AddSingleton(); @@ -175,13 +176,19 @@ namespace Pal.Client { _logger.LogInformation("Loading database & running migrations"); await using var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.MigrateAsync(); + await dbContext.Database.MigrateAsync(token); _logger.LogInformation("Completed database migrations"); } token.ThrowIfCancellationRequested(); + // v1 migration: config migration for import history, json migration for markers + _serviceProvider.GetRequiredService().Migrate(); + await _serviceProvider.GetRequiredService().MigrateAsync(token); + + token.ThrowIfCancellationRequested(); + // windows that have logic to open on startup _serviceProvider.GetRequiredService(); diff --git a/Pal.Client/Pal.Client.csproj b/Pal.Client/Pal.Client.csproj index bb1d9a9..b423f52 100644 --- a/Pal.Client/Pal.Client.csproj +++ b/Pal.Client/Pal.Client.csproj @@ -17,7 +17,7 @@ false true portable - $(SolutionDir)=X:\ + $(SolutionDir)=X:\ diff --git a/Pal.Common/Pal.Common.csproj b/Pal.Common/Pal.Common.csproj index 5166815..720fa9b 100644 --- a/Pal.Common/Pal.Common.csproj +++ b/Pal.Common/Pal.Common.csproj @@ -6,6 +6,6 @@ enable enable portable - $(SolutionDir)=X:\ + $(SolutionDir)=X:\