aboutsummaryrefslogtreecommitdiff
path: root/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2025-08-03 17:27:17 -0400
committerGitHub <noreply@github.com>2025-08-03 17:27:17 -0400
commit4b6fb6c4bb2478badad068ce18aabe0c2955db48 (patch)
tree15f986ee62327cceb8f5c8f009bcf08d10cfaa66 /src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
parente7bc86ebb8496615e0b3f73eb4f13ab4c0913dc8 (diff)
parentdb7465e83d9cc07134a0bffad7ed17b1c7b873da (diff)
Merge branch 'master' into master
Diffstat (limited to 'src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs')
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs174
1 files changed, 174 insertions, 0 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
new file mode 100644
index 000000000..e52ab69d7
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.DbConfiguration;
+using MediaBrowser.Common.Configuration;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Database.Providers.Sqlite;
+
+/// <summary>
+/// Configures jellyfin to use an SQLite database.
+/// </summary>
+[JellyfinDatabaseProviderKey("Jellyfin-SQLite")]
+public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
+{
+ private const string BackupFolderName = "SQLiteBackups";
+ private readonly IApplicationPaths _applicationPaths;
+ private readonly ILogger<SqliteDatabaseProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SqliteDatabaseProvider"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</param>
+ /// <param name="logger">A logger.</param>
+ public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger<SqliteDatabaseProvider> logger)
+ {
+ _applicationPaths = applicationPaths;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
+
+ /// <inheritdoc/>
+ public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
+ {
+ var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
+ sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
+ sqliteConnectionBuilder.Cache = Enum.Parse<SqliteCacheMode>(databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("cache", StringComparison.OrdinalIgnoreCase))?.Value ?? nameof(SqliteCacheMode.Default));
+ sqliteConnectionBuilder.Pooling = (databaseConfiguration.CustomProviderOptions?.Options.FirstOrDefault(e => e.Key.Equals("pooling", StringComparison.OrdinalIgnoreCase))?.Value ?? bool.FalseString).Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
+
+ options
+ .UseSqlite(
+ sqliteConnectionBuilder.ToString(),
+ sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
+ // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
+ .ConfigureWarnings(warnings =>
+ warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning));
+ }
+
+ /// <inheritdoc/>
+ public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
+ {
+ var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ if (context.Database.IsSqlite())
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+ await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ else
+ {
+ _logger.LogInformation("This database doesn't support optimization");
+ }
+ }
+ }
+
+ /// <inheritdoc/>
+ public void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
+ }
+
+ /// <inheritdoc/>
+ public async Task RunShutdownTask(CancellationToken cancellationToken)
+ {
+ if (DbContextFactory is null)
+ {
+ return;
+ }
+
+ // Run before disposing the application
+ var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+ }
+
+ SqliteConnection.ClearAllPools();
+ }
+
+ /// <inheritdoc/>
+ public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
+ {
+ configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
+ }
+
+ /// <inheritdoc />
+ public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
+ {
+ var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
+ var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
+ var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
+ Directory.CreateDirectory(backupFile);
+
+ backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
+ File.Copy(path, backupFile);
+ return Task.FromResult(key);
+ }
+
+ /// <inheritdoc />
+ public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
+ {
+ // ensure there are absolutly no dangling Sqlite connections.
+ SqliteConnection.ClearAllPools();
+ var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
+ var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
+
+ if (!File.Exists(backupFile))
+ {
+ _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key);
+ return Task.CompletedTask;
+ }
+
+ File.Copy(backupFile, path, true);
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc />
+ public Task DeleteBackup(string key)
+ {
+ var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
+
+ if (!File.Exists(backupFile))
+ {
+ _logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
+ return Task.CompletedTask;
+ }
+
+ File.Delete(backupFile);
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
+ {
+ ArgumentNullException.ThrowIfNull(tableNames);
+
+ var deleteQueries = new List<string>();
+ foreach (var tableName in tableNames)
+ {
+ deleteQueries.Add($"DELETE FROM \"{tableName}\";");
+ }
+
+ var deleteAllQuery =
+ $"""
+ PRAGMA foreign_keys = OFF;
+ {string.Join('\n', deleteQueries)}
+ PRAGMA foreign_keys = ON;
+ """;
+
+ await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
+ }
+}