fix app startup with mysql (#2205)
* don't run pragma command on mysql * add not required pathhash * make media file path hash required * update changelog
This commit is contained in:
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
- Fix app startup with MySql/MariaDB
|
||||
|
||||
## [25.3.1] - 2025-07-24
|
||||
### Fixed
|
||||
|
||||
@@ -136,7 +136,9 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
{
|
||||
if (key.StartsWith("ETV_", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("ASPNETCORE_", StringComparison.OrdinalIgnoreCase))
|
||||
|| key.StartsWith("ASPNETCORE_", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.Equals("PROVIDER", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("ELASTICSEARCH", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
environment[key] = value;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ public class MediaFile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Path { get; set; }
|
||||
public string PathHash { get; set; }
|
||||
|
||||
public int MediaVersionId { get; set; }
|
||||
public MediaVersion MediaVersion { get; set; }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ErsatzTV.Core;
|
||||
|
||||
public static class PathUtils
|
||||
{
|
||||
public static string GetPathHash(string path)
|
||||
{
|
||||
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(path));
|
||||
var builder = new StringBuilder();
|
||||
foreach (byte b in bytes)
|
||||
{
|
||||
builder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -12,8 +12,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
[DbContext(typeof(TvContext))]
|
||||
[Migration("20250723030616_Update_MediaFilePath")]
|
||||
partial class Update_MediaFilePath
|
||||
[Migration("20250726155754_Add_MediaFilePathHash")]
|
||||
partial class Add_MediaFilePathHash
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -1116,15 +1116,15 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("PathHash")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryFolderId");
|
||||
|
||||
b.HasIndex("MediaVersionId");
|
||||
|
||||
b.HasIndex("Path")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaFile", (string)null);
|
||||
|
||||
b.UseTptMappingStrategy();
|
||||
+22
-1
@@ -5,11 +5,15 @@
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Update_MediaFilePath : Migration
|
||||
public partial class Add_MediaFilePathHash : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MediaFile_Path",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Path",
|
||||
table: "MediaFile",
|
||||
@@ -20,11 +24,22 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PathHash",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Path",
|
||||
table: "MediaFile",
|
||||
@@ -35,6 +50,12 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaFile_Path",
|
||||
table: "MediaFile",
|
||||
column: "Path",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+6145
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Update_MediaFilePathHash : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
table: "MediaFile",
|
||||
keyColumn: "PathHash",
|
||||
keyValue: null,
|
||||
column: "PathHash",
|
||||
value: "");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "varchar(255)",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "longtext",
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaFile_PathHash",
|
||||
table: "MediaFile",
|
||||
column: "PathHash",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MediaFile_PathHash",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "longtext",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "varchar(255)")
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1113,13 +1113,17 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("PathHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryFolderId");
|
||||
|
||||
b.HasIndex("MediaVersionId");
|
||||
|
||||
b.HasIndex("Path")
|
||||
b.HasIndex("PathHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaFile", (string)null);
|
||||
|
||||
Generated
+5978
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_MediaFilePathHash : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MediaFile_Path",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PathHash",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaFile_Path",
|
||||
table: "MediaFile",
|
||||
column: "Path",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+5982
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Update_MediaFilePathHash : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MediaFile_PathHash",
|
||||
table: "MediaFile",
|
||||
column: "PathHash",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_MediaFile_PathHash",
|
||||
table: "MediaFile");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "PathHash",
|
||||
table: "MediaFile",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1056,13 +1056,17 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PathHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryFolderId");
|
||||
|
||||
b.HasIndex("MediaVersionId");
|
||||
|
||||
b.HasIndex("Path")
|
||||
b.HasIndex("PathHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MediaFile", (string)null);
|
||||
|
||||
@@ -10,9 +10,12 @@ public class MediaFileConfiguration : IEntityTypeConfiguration<MediaFile>
|
||||
{
|
||||
builder.ToTable("MediaFile");
|
||||
|
||||
builder.HasIndex(f => f.Path)
|
||||
builder.HasIndex(f => f.PathHash)
|
||||
.IsUnique();
|
||||
|
||||
builder.Property(f => f.PathHash)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(f => f.LibraryFolder)
|
||||
.WithMany(f => f.MediaFiles)
|
||||
.HasForeignKey(f => f.LibraryFolderId)
|
||||
|
||||
@@ -9,7 +9,10 @@ public static class DbInitializer
|
||||
{
|
||||
public static async Task<Unit> Initialize(TvContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.Connection.ExecuteAsync("PRAGMA journal_mode=WAL", cancellationToken);
|
||||
if (TvContext.IsSqlite)
|
||||
{
|
||||
await context.Connection.ExecuteAsync("PRAGMA journal_mode=WAL", cancellationToken);
|
||||
}
|
||||
|
||||
if (!context.LanguageCodes.Any())
|
||||
{
|
||||
|
||||
@@ -142,7 +142,7 @@ public class ImageRepository : IImageRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -252,7 +252,7 @@ public class MovieRepository : IMovieRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -233,7 +233,7 @@ public class MusicVideoRepository : IMusicVideoRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -207,7 +207,7 @@ public class OtherVideoRepository : IOtherVideoRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -149,7 +149,7 @@ public class RemoteStreamRepository(
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -134,7 +134,7 @@ public class SongRepository : ISongRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -773,7 +773,7 @@ public class TelevisionRepository : ITelevisionRepository
|
||||
[
|
||||
new MediaVersion
|
||||
{
|
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
|
||||
MediaFiles = [new MediaFile { Path = path, PathHash = PathUtils.GetPathHash(path), LibraryFolderId = libraryFolderId }],
|
||||
Streams = []
|
||||
}
|
||||
],
|
||||
|
||||
@@ -17,6 +17,7 @@ public class TvContext : DbContext
|
||||
|
||||
public static string LastInsertedRowId { get; set; } = "last_insert_rowid()";
|
||||
public static string CaseInsensitiveCollation { get; set; } = "NOCASE";
|
||||
public static bool IsSqlite { get; set; }
|
||||
|
||||
public IDbConnection Connection => Database.GetDbConnection();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Services.RunOnce;
|
||||
@@ -28,11 +30,39 @@ public class DatabaseMigratorService : BackgroundService
|
||||
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
|
||||
|
||||
await dbContext.Database.MigrateAsync("Add_MediaFilePathHash", stoppingToken);
|
||||
|
||||
// this can't be part of a migration, so we have to stop here and run some sql
|
||||
await PopulatePathHashes(dbContext);
|
||||
|
||||
// then continue migrating
|
||||
await dbContext.Database.MigrateAsync(stoppingToken);
|
||||
|
||||
await DbInitializer.Initialize(dbContext, stoppingToken);
|
||||
|
||||
_systemStartup.DatabaseIsReady();
|
||||
|
||||
_logger.LogInformation("Done applying database migrations");
|
||||
}
|
||||
|
||||
private static async Task PopulatePathHashes(TvContext dbContext)
|
||||
{
|
||||
if (await dbContext.Connection.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM `MediaFile` WHERE `PathHash` IS NULL OR `PathHash` = ''") == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbContext.Connection is SqliteConnection sqliteConnection)
|
||||
{
|
||||
sqliteConnection.CreateFunction("HASH_SHA256", (string text) => PathUtils.GetPathHash(text));
|
||||
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = HASH_SHA256(`Path`);");
|
||||
}
|
||||
else
|
||||
{
|
||||
// mysql
|
||||
await dbContext.Connection.ExecuteAsync("UPDATE `MediaFile` SET `PathHash` = sha2(`Path`, 256);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +362,8 @@ public class Startup
|
||||
{
|
||||
if (databaseProvider == Provider.Sqlite.Name)
|
||||
{
|
||||
TvContext.IsSqlite = true;
|
||||
|
||||
options.UseSqlite(
|
||||
sqliteConnectionString,
|
||||
o =>
|
||||
@@ -373,6 +375,8 @@ public class Startup
|
||||
|
||||
if (databaseProvider == Provider.MySql.Name)
|
||||
{
|
||||
TvContext.IsSqlite = false;
|
||||
|
||||
options.UseMySql(
|
||||
mySqlConnectionString,
|
||||
ServerVersion.AutoDetect(mySqlConnectionString),
|
||||
|
||||
Reference in New Issue
Block a user