feat: add support for applying migrations automatically
All checks were successful
Update changelog / changelog (push) Successful in 25s
All checks were successful
Update changelog / changelog (push) Successful in 25s
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Shadow.Entities;
|
using Shadow.Entities;
|
||||||
using System.Diagnostics.Metrics;
|
|
||||||
using System.Reflection.Emit;
|
|
||||||
|
|
||||||
namespace Shadow.Data;
|
namespace Shadow.Data;
|
||||||
public class ApplicationDbContext : DbContext
|
public class ApplicationDbContext : DbContext
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ if (args.FirstOrDefault() is not null)
|
|||||||
ApplicationDbContext db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
ApplicationDbContext db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
|
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
|
||||||
Cli cli = new(db, guhf, args);
|
Cli cli = new(db, guhf, args);
|
||||||
shutdown = await cli.Parse();
|
shutdown = cli.Parse();
|
||||||
}
|
}
|
||||||
if (shutdown) return;
|
if (shutdown) return;
|
||||||
}
|
}
|
||||||
|
|||||||
37
Tools/Cli.cs
37
Tools/Cli.cs
@@ -17,7 +17,7 @@ public class Cli
|
|||||||
args = _args;
|
args = _args;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Parse()
|
public bool Parse()
|
||||||
{
|
{
|
||||||
// Returns true if the program can finish execution.
|
// Returns true if the program can finish execution.
|
||||||
bool exit = true;
|
bool exit = true;
|
||||||
@@ -30,16 +30,19 @@ public class Cli
|
|||||||
case "adduser":
|
case "adduser":
|
||||||
case "--adduser":
|
case "--adduser":
|
||||||
case "--add-user":
|
case "--add-user":
|
||||||
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
AddUser();
|
AddUser();
|
||||||
break;
|
break;
|
||||||
case "resetpassword":
|
case "resetpassword":
|
||||||
case "--resetpassword":
|
case "--resetpassword":
|
||||||
case "--reset-password":
|
case "--reset-password":
|
||||||
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
ResetPassword();
|
ResetPassword();
|
||||||
break;
|
break;
|
||||||
case "removeuser":
|
case "removeuser":
|
||||||
case "--removeuser":
|
case "--removeuser":
|
||||||
case "--remove-user":
|
case "--remove-user":
|
||||||
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
RemoveUser();
|
RemoveUser();
|
||||||
break;
|
break;
|
||||||
case "help":
|
case "help":
|
||||||
@@ -49,6 +52,7 @@ public class Cli
|
|||||||
case "/h":
|
case "/h":
|
||||||
case "/?":
|
case "/?":
|
||||||
case "?":
|
case "?":
|
||||||
|
// Help should be shown without forcing migrations.
|
||||||
ShowHelp();
|
ShowHelp();
|
||||||
break;
|
break;
|
||||||
case "setupwizard":
|
case "setupwizard":
|
||||||
@@ -58,6 +62,7 @@ public class Cli
|
|||||||
case "--setup":
|
case "--setup":
|
||||||
case "configure":
|
case "configure":
|
||||||
case "--configure":
|
case "--configure":
|
||||||
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
SetupWizard();
|
SetupWizard();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -73,13 +78,21 @@ public class Cli
|
|||||||
// Shown when "Shadow --help"/"Shadow help"/... is ran.
|
// Shown when "Shadow --help"/"Shadow help"/... is ran.
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"--- Shadow commandline utility ---\n" +
|
$"--- Shadow commandline utility ---\n" +
|
||||||
$"Shadow version: #{ThisAssembly.Git.Commit} {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n\n" +
|
$"Shadow version: #{ThisAssembly.Git.Commit} {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n" +
|
||||||
|
$"\n" +
|
||||||
$"Available commands:\n" +
|
$"Available commands:\n" +
|
||||||
|
$"\n" +
|
||||||
|
$"=== User management ===\n" +
|
||||||
$"- addUser [username] - create a new user,\n" +
|
$"- addUser [username] - create a new user,\n" +
|
||||||
$"- resetPassword [username] - reset a user's password,\n" +
|
$"- resetPassword [username] - reset a user's password,\n" +
|
||||||
$"- removeUser [username] - remove the user COMPLETELY.\n\n" +
|
$"- removeUser [username] - remove the user COMPLETELY.\n" +
|
||||||
|
$"\n" +
|
||||||
|
$"=== Server maintenance ===\n" +
|
||||||
|
$"- setupWizard - configure library and thumbnail location on disk.\n" +
|
||||||
|
$"\n" +
|
||||||
$"Username is optional. If not provided, user will be prompted to enter it.\n" +
|
$"Username is optional. If not provided, user will be prompted to enter it.\n" +
|
||||||
$"Running without specifying a command launches a Kestrel web server.\n\n" +
|
$"Running without specifying a command launches a Kestrel web server.\n" +
|
||||||
|
$"\n" +
|
||||||
$"License: AGPLv3+, Source code: https://gitea.7o7.cx/sherl/Shadow"
|
$"License: AGPLv3+, Source code: https://gitea.7o7.cx/sherl/Shadow"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -271,7 +284,8 @@ public class Cli
|
|||||||
Global? musicLibraryPath = db.Globals.FirstOrDefault(g => g.Key == "musicLibraryPath");
|
Global? musicLibraryPath = db.Globals.FirstOrDefault(g => g.Key == "musicLibraryPath");
|
||||||
Global? musicThumbnailPath = db.Globals.FirstOrDefault(g => g.Key == "musicThumbnailPath");
|
Global? musicThumbnailPath = db.Globals.FirstOrDefault(g => g.Key == "musicThumbnailPath");
|
||||||
|
|
||||||
if (musicLibraryPath is not null || musicThumbnailPath is not null)
|
bool configurationExists = musicLibraryPath is not null || musicThumbnailPath is not null;
|
||||||
|
if (configurationExists)
|
||||||
{
|
{
|
||||||
Console.WriteLine(" Found existing configuration:");
|
Console.WriteLine(" Found existing configuration:");
|
||||||
if (musicLibraryPath is not null)
|
if (musicLibraryPath is not null)
|
||||||
@@ -314,12 +328,17 @@ public class Cli
|
|||||||
|
|
||||||
Console.WriteLine("success!");
|
Console.WriteLine("success!");
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
|
Console.WriteLine("\n" +
|
||||||
|
"Tip: On your next run of Shadow, a web server will start.\n" +
|
||||||
|
"If you ever need to enter setup wizard again, use `Shadow setupWizard`.\n" +
|
||||||
|
"For a complete list of available commands use `Shadow help`.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReadPassword(string prompt = " Enter password (will not be echoed back): ")
|
public static string ReadPassword(string prompt = " Enter password (will not be echoed back): ")
|
||||||
{
|
{
|
||||||
// https://www.silicloud.com/blog/how-to-hide-content-in-the-console-using-c/
|
// https://www.silicloud.com/blog/how-to-hide-content-in-the-console-using-c/
|
||||||
string password = String.Empty;
|
string password = String.Empty;
|
||||||
@@ -344,14 +363,14 @@ public class Cli
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? ReadName(string prompt = " Enter username: ")
|
public static string? ReadName(string prompt = " Enter username: ")
|
||||||
{
|
{
|
||||||
Console.Write(prompt);
|
Console.Write(prompt);
|
||||||
string? input = Console.ReadLine();
|
string? input = Console.ReadLine();
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool YesNoPrompt(string prompt, bool? default_value = null)
|
public static bool YesNoPrompt(string prompt, bool? default_value = null)
|
||||||
{
|
{
|
||||||
// Checks if user input starts with "y", and if it does, returns true.
|
// Checks if user input starts with "y", and if it does, returns true.
|
||||||
// Otherwise checks for "n". If both checks fail, and default_value is null,
|
// Otherwise checks for "n". If both checks fail, and default_value is null,
|
||||||
@@ -388,7 +407,7 @@ public class Cli
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DefaultPrompt(string prompt, string? default_value = null)
|
public static string DefaultPrompt(string prompt, string? default_value = null)
|
||||||
{
|
{
|
||||||
// Prompt the user repeatedly for answer. If default value
|
// Prompt the user repeatedly for answer. If default value
|
||||||
// is specified, an enter will be treated as if the user
|
// is specified, an enter will be treated as if the user
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ public class Seeder
|
|||||||
|
|
||||||
Console.WriteLine($"You're running Shadow, commit {ThisAssembly.Git.Commit} of branch {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n");
|
Console.WriteLine($"You're running Shadow, commit {ThisAssembly.Git.Commit} of branch {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n");
|
||||||
|
|
||||||
|
// Check migration status. If we can't proceed, exit now.
|
||||||
|
bool migrationSucceeded = EnsureMigrations(db);
|
||||||
|
if (!migrationSucceeded) return true;
|
||||||
|
|
||||||
// Check if this is a clean, first run. If so, run the setup wizard.
|
// Check if this is a clean, first run. If so, run the setup wizard.
|
||||||
Global? lastVersion = db.Globals.FirstOrDefault(c => c.Key == "lastVersion");
|
Global? lastVersion = db.Globals.FirstOrDefault(c => c.Key == "lastVersion");
|
||||||
Global? runs = db.Globals.FirstOrDefault(g => g.Key == "runs");
|
Global? runs = db.Globals.FirstOrDefault(g => g.Key == "runs");
|
||||||
@@ -104,4 +108,73 @@ public class Seeder
|
|||||||
return shutdown;
|
return shutdown;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool EnsureMigrations(ApplicationDbContext dbContext)
|
||||||
|
{
|
||||||
|
// Returns true if migrations have been successfully applied.
|
||||||
|
// Otherwise returns false.
|
||||||
|
bool migrationSuccess = false;
|
||||||
|
List<string> availableMigrations = dbContext.Database.GetMigrations().ToList();
|
||||||
|
List<string> appliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
||||||
|
|
||||||
|
bool hasMissingMigrations = availableMigrations.Count > appliedMigrations.Count;
|
||||||
|
bool doMigrationsMatch = availableMigrations.SequenceEqual(appliedMigrations);
|
||||||
|
if (hasMissingMigrations)
|
||||||
|
{
|
||||||
|
bool userMigrationConsent = false;
|
||||||
|
Console.WriteLine("\n" +
|
||||||
|
"========================================\n" +
|
||||||
|
"[Warn] Database migrations missing!\n" +
|
||||||
|
"========================================");
|
||||||
|
if (appliedMigrations.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine( "Empty database detected. Applying migrations is recommended, and required if this is the first run of Shadow.");
|
||||||
|
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [Y/n]: ", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Detected existing {appliedMigrations.Count} migrations. Backing up the database before applying migrations is recommended.");
|
||||||
|
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [y/N]: ", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we have user permission to perform migration?
|
||||||
|
if (userMigrationConsent)
|
||||||
|
{
|
||||||
|
dbContext.Database.Migrate();
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exiting...");
|
||||||
|
return migrationSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if migrations match afterwards
|
||||||
|
List<string> newlyAppliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
||||||
|
bool doNewlyAppliedMigrationsMatch = availableMigrations.SequenceEqual(newlyAppliedMigrations);
|
||||||
|
if (doNewlyAppliedMigrationsMatch)
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n" +
|
||||||
|
"==============================\n" +
|
||||||
|
"Migration success!\n" +
|
||||||
|
"==============================\n");
|
||||||
|
migrationSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// An error ocurred when applying migrations automatically
|
||||||
|
Console.WriteLine("\n" +
|
||||||
|
"==================================================\n" +
|
||||||
|
"[Error] Failed applying migrations automatically!\n" +
|
||||||
|
"==================================================\n");
|
||||||
|
Console.WriteLine("The list of newly applied migrations does not match the list of planned migrations.\n" +
|
||||||
|
"Shadow cannot operate with missing migrations. Please manually revise the database structure and create backups before altering it.\n");
|
||||||
|
migrationSuccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else migrationSuccess = true;
|
||||||
|
|
||||||
|
return migrationSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user