1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
using System;
using System.Collections.Generic;
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.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)
{
options
.UseSqlite(
$"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false",
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 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);
}
}
|