< 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
41%
Covered lines: 15
Uncovered lines: 21
Coverable lines: 36
Total lines: 167
Line coverage: 41.6%
Branch coverage
0%
Covered branches: 0
Total branches: 4
Branch coverage: 0%
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(...)100%11100%
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.Threading;
 6using System.Threading.Tasks;
 7using Jellyfin.Database.Implementations;
 8using MediaBrowser.Common.Configuration;
 9using Microsoft.Data.Sqlite;
 10using Microsoft.EntityFrameworkCore;
 11using Microsoft.EntityFrameworkCore.Diagnostics;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Jellyfin.Database.Providers.Sqlite;
 15
 16/// <summary>
 17/// Configures jellyfin to use an SQLite database.
 18/// </summary>
 19[JellyfinDatabaseProviderKey("Jellyfin-SQLite")]
 20public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
 21{
 22    private const string BackupFolderName = "SQLiteBackups";
 23    private readonly IApplicationPaths _applicationPaths;
 24    private readonly ILogger<SqliteDatabaseProvider> _logger;
 25
 26    /// <summary>
 27    /// Initializes a new instance of the <see cref="SqliteDatabaseProvider"/> class.
 28    /// </summary>
 29    /// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</
 30    /// <param name="logger">A logger.</param>
 31    public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger<SqliteDatabaseProvider> logger)
 32    {
 4333        _applicationPaths = applicationPaths;
 4334        _logger = logger;
 4335    }
 36
 37    /// <inheritdoc/>
 38    public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
 39
 40    /// <inheritdoc/>
 41    public void Initialise(DbContextOptionsBuilder options)
 42    {
 4243        options
 4244            .UseSqlite(
 4245                $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false",
 4246                sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
 4247            // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
 4248            .ConfigureWarnings(warnings =>
 4249                warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning));
 4250    }
 51
 52    /// <inheritdoc/>
 53    public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
 54    {
 55        var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 56        await using (context.ConfigureAwait(false))
 57        {
 58            if (context.Database.IsSqlite())
 59            {
 60                await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 61                await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
 62                _logger.LogInformation("jellyfin.db optimized successfully!");
 63            }
 64            else
 65            {
 66                _logger.LogInformation("This database doesn't support optimization");
 67            }
 68        }
 69    }
 70
 71    /// <inheritdoc/>
 72    public void OnModelCreating(ModelBuilder modelBuilder)
 73    {
 274        modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);
 275    }
 76
 77    /// <inheritdoc/>
 78    public async Task RunShutdownTask(CancellationToken cancellationToken)
 79    {
 80        if (DbContextFactory is null)
 81        {
 82            return;
 83        }
 84
 85        // Run before disposing the application
 86        var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 87        await using (context.ConfigureAwait(false))
 88        {
 89            await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
 90        }
 91
 92        SqliteConnection.ClearAllPools();
 93    }
 94
 95    /// <inheritdoc/>
 96    public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
 97    {
 298        configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention());
 299    }
 100
 101    /// <inheritdoc />
 102    public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
 103    {
 0104        var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
 0105        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0106        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
 0107        Directory.CreateDirectory(backupFile);
 108
 0109        backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
 0110        File.Copy(path, backupFile);
 0111        return Task.FromResult(key);
 112    }
 113
 114    /// <inheritdoc />
 115    public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
 116    {
 117        // ensure there are absolutly no dangling Sqlite connections.
 0118        SqliteConnection.ClearAllPools();
 0119        var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
 0120        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 121
 0122        if (!File.Exists(backupFile))
 123        {
 0124            _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key);
 0125            return Task.CompletedTask;
 126        }
 127
 0128        File.Copy(backupFile, path, true);
 0129        return Task.CompletedTask;
 130    }
 131
 132    /// <inheritdoc />
 133    public Task DeleteBackup(string key)
 134    {
 0135        var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");
 136
 0137        if (!File.Exists(backupFile))
 138        {
 0139            _logger.LogCritical("Tried to delete a backup that does not exist: {Key}", key);
 0140            return Task.CompletedTask;
 141        }
 142
 0143        File.Delete(backupFile);
 0144        return Task.CompletedTask;
 145    }
 146
 147    /// <inheritdoc/>
 148    public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
 149    {
 150        ArgumentNullException.ThrowIfNull(tableNames);
 151
 152        var deleteQueries = new List<string>();
 153        foreach (var tableName in tableNames)
 154        {
 155            deleteQueries.Add($"DELETE FROM \"{tableName}\";");
 156        }
 157
 158        var deleteAllQuery =
 159        $"""
 160        PRAGMA foreign_keys = OFF;
 161        {string.Join('\n', deleteQueries)}
 162        PRAGMA foreign_keys = ON;
 163        """;
 164
 165        await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
 166    }
 167}