using System; using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using MediaBrowser.Common.Configuration; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Database.Providers.Sqlite; /// /// Configures jellyfin to use an SQLite database. /// [JellyfinDatabaseProviderKey("Jellyfin-SQLite")] public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider { private const string BackupFolderName = "SQLiteBackups"; private readonly IApplicationPaths _applicationPaths; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// Service to construct the fallback when the old data path configuration is used. /// A logger. public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger logger) { _applicationPaths = applicationPaths; _logger = logger; } /// public IDbContextFactory? DbContextFactory { get; set; } /// public void Initialise(DbContextOptionsBuilder options) { options.UseSqlite( $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false", sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly)); } /// 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"); } } } /// public void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); } /// public async Task RunShutdownTask(CancellationToken cancellationToken) { // 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(); } /// public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention()); } /// public Task 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); if (!Directory.Exists(backupFile)) { Directory.CreateDirectory(backupFile); } backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db"); File.Copy(path, backupFile); return Task.FromResult(key); } /// 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."); return Task.CompletedTask; } File.Copy(backupFile, path, true); return Task.CompletedTask; } }