< Summary - Jellyfin

Information
Class: Jellyfin.Database.Providers.Sqlite.SqliteDatabaseProvider
Assembly: Jellyfin.Database.Providers.Sqlite
File(s): /srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
Line coverage
58%
Covered lines: 32
Uncovered lines: 23
Coverable lines: 55
Total lines: 207
Line coverage: 58.1%
Branch coverage
41%
Covered branches: 5
Total branches: 12
Branch coverage: 41.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Initialise(...)62.5%8892.59%
OnModelCreating(...)100%11100%
ConfigureConventions(...)100%11100%
MigrationBackupFast(...)100%210%
RestoreBackupFast(...)0%620%
DeleteBackup(...)0%620%

File(s)

/srv/git/jellyfin/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Database.Implementations;
 9using Jellyfin.Database.Implementations.DbConfiguration;
 10using MediaBrowser.Common.Configuration;
 11using Microsoft.Data.Sqlite;
 12using Microsoft.EntityFrameworkCore;
 13using Microsoft.EntityFrameworkCore.Diagnostics;
 14using Microsoft.Extensions.Logging;
 15
 16namespace Jellyfin.Database.Providers.Sqlite;
 17
 18/// <summary>
 19/// Configures jellyfin to use an SQLite database.
 20/// </summary>
 21[JellyfinDatabaseProviderKey("Jellyfin-SQLite")]
 22public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
 23{
 24    private const string BackupFolderName = "SQLiteBackups";
 25    private readonly IApplicationPaths _applicationPaths;
 26    private readonly ILogger<SqliteDatabaseProvider> _logger;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="SqliteDatabaseProvider"/> class.
 30    /// </summary>
 31    /// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</
 32    /// <param name="logger">A logger.</param>
 33    public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger<SqliteDatabaseProvider> logger)
 34    {
 4335        _applicationPaths = applicationPaths;
 4336        _logger = logger;
 4337    }
 38
 39    /// <inheritdoc/>
 40    public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
 41
 42    /// <inheritdoc/>
 43    public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
 44    {
 45        static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, Func<T
 46        {
 47            if (options is null)
 48            {
 49                return defaultValue is not null ? defaultValue() : default;
 50            }
 51
 52            var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
 53            if (value is null)
 54            {
 55                return defaultValue is not null ? defaultValue() : default;
 56            }
 57
 58            return converter(value.Value);
 59        }
 60
 4261        var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
 62
 4263        var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
 4264        sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 4265        sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCache
 4266        sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringCompa
 67
 4268        var connectionString = sqliteConnectionBuilder.ToString();
 69
 70        // Log SQLite connection parameters
 4271        _logger.LogInformation("SQLite connection string: {ConnectionString}", connectionString);
 72
 4273        options
 4274            .UseSqlite(
 4275                connectionString,
 4276                sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
 4277            // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
 4278            .ConfigureWarnings(warnings =>
 4279                warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning))
 4280            .AddInterceptors(new PragmaConnectionInterceptor(
 4281                _logger,
 4282                GetOption<int?>(customOptions, "cacheSize", e => int.Parse(e, CultureInfo.InvariantCulture)),
 4283                GetOption(customOptions, "lockingmode", e => e, () => "NORMAL")!,
 4284                GetOption(customOptions, "journalsizelimit", int.Parse, () => 134_217_728),
 4285                GetOption(customOptions, "tempstoremode", int.Parse, () => 2),
 4286                GetOption(customOptions, "syncmode", int.Parse, () => 1),
 4287                customOptions?.Where(e => e.Key.StartsWith("#PRAGMA:", StringComparison.OrdinalIgnoreCase)).ToDictionary
 88
 4289        var enableSensitiveDataLogging = GetOption(customOptions, "EnableSensitiveDataLogging", e => e.Equals(bool.TrueS
 4290        if (enableSensitiveDataLogging)
 91        {
 092            options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
 093            _logger.LogInformation("EnableSensitiveDataLogging is enabled on SQLite connection");
 94        }
 4295    }
 96
 97    /// <inheritdoc/>
 98    public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
 99    {
 100        var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 101        await using (context.ConfigureAwait(false))
 102        {
 103            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 104            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 105            await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
 106            await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwa
 107            _logger.LogInformation("jellyfin.db optimized successfully!");
 108        }
 109    }
 110
 111    /// <inheritdoc/>
 112    public void OnModelCreating(ModelBuilder modelBuilder)
 113    {
 2114        modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
 2115    }
 116
 117    /// <inheritdoc/>
 118    public async Task RunShutdownTask(CancellationToken cancellationToken)
 119    {
 120        if (DbContextFactory is null)
 121        {
 122            return;
 123        }
 124
 125        // Run before disposing the application
 126        var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 127        await using (context.ConfigureAwait(false))
 128        {
 129            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 130        }
 131
 132        SqliteConnection.ClearAllPools();
 133    }
 134
 135    /// <inheritdoc/>
 136    public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
 137    {
 2138        configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
 2139    }
 140
 141    /// <inheritdoc />
 142    public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
 143    {
 0144        var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
 0145        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0146        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
 0147        Directory.CreateDirectory(backupFile);
 148
 0149        backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
 0150        File.Copy(path, backupFile);
 0151        return Task.FromResult(key);
 152    }
 153
 154    /// <inheritdoc />
 155    public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
 156    {
 157        // ensure there are absolutly no dangling Sqlite connections.
 0158        SqliteConnection.ClearAllPools();
 0159        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0160        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 161
 0162        if (!File.Exists(backupFile))
 163        {
 0164            _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key);
 0165            return Task.CompletedTask;
 166        }
 167
 0168        File.Copy(backupFile, path, true);
 0169        return Task.CompletedTask;
 170    }
 171
 172    /// <inheritdoc />
 173    public Task DeleteBackup(string key)
 174    {
 0175        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 176
 0177        if (!File.Exists(backupFile))
 178        {
 0179            _logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
 0180            return Task.CompletedTask;
 181        }
 182
 0183        File.Delete(backupFile);
 0184        return Task.CompletedTask;
 185    }
 186
 187    /// <inheritdoc/>
 188    public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
 189    {
 190        ArgumentNullException.ThrowIfNull(tableNames);
 191
 192        var deleteQueries = new List<string>();
 193        foreach (var tableName in tableNames)
 194        {
 195            deleteQueries.Add($"DELETE FROM \"{tableName}\";");
 196        }
 197
 198        var deleteAllQuery =
 199        $"""
 200        PRAGMA foreign_keys = OFF;
 201        {string.Join('\n', deleteQueries)}
 202        PRAGMA foreign_keys = ON;
 203        """;
 204
 205        await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
 206    }
 207}