From ccef50d6811b8bdf4529dd2f92736aae1d2fa5d5 Mon Sep 17 00:00:00 2001 From: rmcrackan Date: Wed, 8 Apr 2026 17:43:21 -0400 Subject: [PATCH] move new check into datalayer --- .../ApplicationServices.csproj | 1 - Source/ApplicationServices/DbContexts.cs | 29 +-------- Source/DataLayer/LibationContextFactory.cs | 60 ++++++++++++++++++- Source/LibationCli/Options/CopyDbOptions.cs | 4 +- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/Source/ApplicationServices/ApplicationServices.csproj b/Source/ApplicationServices/ApplicationServices.csproj index e7365a20..7c5dcd6e 100644 --- a/Source/ApplicationServices/ApplicationServices.csproj +++ b/Source/ApplicationServices/ApplicationServices.csproj @@ -6,7 +6,6 @@ - compile;contentFiles;build;buildMultitargeting;buildTransitive;analyzers;native diff --git a/Source/ApplicationServices/DbContexts.cs b/Source/ApplicationServices/DbContexts.cs index da29b13e..03f3538b 100644 --- a/Source/ApplicationServices/DbContexts.cs +++ b/Source/ApplicationServices/DbContexts.cs @@ -1,8 +1,5 @@ using DataLayer; using LibationFileManager; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using System; using System.Collections.Generic; namespace ApplicationServices; @@ -17,29 +14,9 @@ public static class DbContexts var context = !string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) ? LibationContextFactory.CreatePostgres(Configuration.Instance.PostgresqlConnectionString) : LibationContextFactory.CreateSqlite(SqliteStorage.ConnectionString); - try - { - context.Database.Migrate(); - } - // SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html) - catch (SqliteException ex) when (ex.SqliteErrorCode == 8) - { - var dbPath = SqliteStorage.DatabasePath; - throw new InvalidOperationException( - $""" - Libation cannot write its SQLite database (migrations need write access). - - Database path: - {dbPath} - - This usually means the folder or the database file is not writable by your user (wrong owner or permissions), or the location is on a read-only or restricted filesystem. - - On Linux: check ownership and permissions on that folder (for example chmod/chown). Snap installs often store data under ~/snap/libation//.local/share/Libation — that entire tree must be writable. - - If the problem continues, try moving the Libation Files location (Settings) to a folder you know is writable, or use the non-Snap build if Snap confinement is blocking writes. - """, - ex); - } + LibationContextFactory.ApplyMigrations( + context, + string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString) ? SqliteStorage.DatabasePath : null); // Validate SQLite DB file was created and is accessible (once per process; OS may delay availability) if (!_sqliteDbValidated && string.IsNullOrEmpty(Configuration.Instance.PostgresqlConnectionString)) diff --git a/Source/DataLayer/LibationContextFactory.cs b/Source/DataLayer/LibationContextFactory.cs index fbec47a3..62106785 100644 --- a/Source/DataLayer/LibationContextFactory.cs +++ b/Source/DataLayer/LibationContextFactory.cs @@ -1,7 +1,10 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using System; +using System.Threading; +using System.Threading.Tasks; namespace DataLayer; @@ -37,4 +40,59 @@ public class LibationContextFactory return new LibationContext(options.Options); } + + /// + /// Runs EF migrations. For SQLite, wraps SQLITE_READONLY (8) with a clearer message including . + /// Pass as null when the context is not SQLite. + /// + public static void ApplyMigrations(LibationContext context, string? sqliteDatabaseFilePath) + { + try + { + context.Database.Migrate(); + } + // SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html) + catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null) + { + throw CreateSqliteReadonlyException(sqliteDatabaseFilePath, ex); + } + } + + /// + public static async Task ApplyMigrationsAsync(LibationContext context, string? sqliteDatabaseFilePath, CancellationToken cancellationToken = default) + { + try + { + await context.Database.MigrateAsync(cancellationToken); + } + // SQLITE_READONLY == 8 (https://www.sqlite.org/rescode.html) + catch (SqliteException ex) when (ex.SqliteErrorCode == 8 && sqliteDatabaseFilePath is not null) + { + throw CreateSqliteReadonlyException(sqliteDatabaseFilePath, ex); + } + } + + private static InvalidOperationException CreateSqliteReadonlyException(string sqliteDatabaseFilePath, SqliteException ex) + { + // Match LibationFileManager.Configuration.IsLinux (OperatingSystem.IsLinux); avoid referencing that project from DataLayer. + var linuxSection = OperatingSystem.IsLinux() + ? "\n\nOn Linux: check ownership and permissions on that folder (for example chmod/chown). Snap installs often store data under ~/snap/libation//.local/share/Libation — that entire tree must be writable." + : ""; + var snapHint = OperatingSystem.IsLinux() + ? ", or use the non-Snap build if Snap confinement is blocking writes" + : ""; + + return new InvalidOperationException( + $""" + Libation cannot write its SQLite database (migrations need write access). + + Database path: + {sqliteDatabaseFilePath} + + This usually means the folder or the database file is not writable by your user (wrong owner or permissions), or the location is on a read-only or restricted filesystem.{linuxSection} + + If the problem continues, try moving the Libation Files location (Settings) to a folder you know is writable{snapHint}. + """, + ex); + } } diff --git a/Source/LibationCli/Options/CopyDbOptions.cs b/Source/LibationCli/Options/CopyDbOptions.cs index 3be62563..5dae993d 100644 --- a/Source/LibationCli/Options/CopyDbOptions.cs +++ b/Source/LibationCli/Options/CopyDbOptions.cs @@ -32,12 +32,12 @@ public class CopyDbOptions : OptionsBase using var source = LibationContextFactory.CreateSqlite(srcConnectionString); using var destination = LibationContextFactory.CreatePostgres(destConnectionString); - await source.Database.MigrateAsync(); + await LibationContextFactory.ApplyMigrationsAsync(source, SqliteStorage.DatabasePath); try { Console.WriteLine("Creating destination database..."); - await destination.Database.MigrateAsync(); + await LibationContextFactory.ApplyMigrationsAsync(destination, null); Console.WriteLine("Destination database recreated."); Console.WriteLine(); }