aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
blob: 14089cac759ec0cf975b4ff304ce00ad8db41e68 (plain)
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
using System;
using System.Collections.Generic;
using System.IO;
using Emby.Server.Implementations.Data;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Server.Migrations.Routines
{
    /// <summary>
    /// The migration routine for migrating the activity log database to EF Core.
    /// </summary>
    [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
#pragma warning disable CS0618 // Type or member is obsolete
    public class MigrateActivityLogDb : IMigrationRoutine
#pragma warning restore CS0618 // Type or member is obsolete
    {
        private const string DbFilename = "activitylog.db";

        private readonly ILogger<MigrateActivityLogDb> _logger;
        private readonly IDbContextFactory<JellyfinDbContext> _provider;
        private readonly IServerApplicationPaths _paths;

        /// <summary>
        /// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class.
        /// </summary>
        /// <param name="logger">The logger.</param>
        /// <param name="paths">The server application paths.</param>
        /// <param name="provider">The database provider.</param>
        public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDbContext> provider)
        {
            _logger = logger;
            _provider = provider;
            _paths = paths;
        }

        /// <inheritdoc/>
        public void Perform()
        {
            var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
            {
                { "None", LogLevel.None },
                { "Trace", LogLevel.Trace },
                { "Debug", LogLevel.Debug },
                { "Information", LogLevel.Information },
                { "Info", LogLevel.Information },
                { "Warn", LogLevel.Warning },
                { "Warning", LogLevel.Warning },
                { "Error", LogLevel.Error },
                { "Critical", LogLevel.Critical }
            };

            var dataPath = _paths.DataPath;
            using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
            {
                connection.Open();

                using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
                userDbConnection.Open();
                _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
                using var dbContext = _provider.CreateDbContext();

                // Make sure that the database is empty in case of failed migration due to power outages, etc.
                dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
                dbContext.SaveChanges();
                // Reset the autoincrement counter
                dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
                dbContext.SaveChanges();

                var newEntries = new List<ActivityLog>();

                var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");

                foreach (var entry in queryResult)
                {
                    if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity))
                    {
                        severity = LogLevel.Trace;
                    }

                    var guid = Guid.Empty;
                    if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid))
                    {
                        var id = entry.GetString(6);
                        // This is not a valid Guid, see if it is an internal ID from an old Emby schema
                        _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id);

                        using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
                        statement.TryBind("@Id", id);

                        using var reader = statement.ExecuteReader();
                        if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid))
                        {
                            // Successfully parsed a Guid from the user table.
                            break;
                        }
                    }

                    var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid)
                    {
                        DateCreated = entry.GetDateTime(7),
                        LogSeverity = severity
                    };

                    if (entry.TryGetString(2, out var result))
                    {
                        newEntry.Overview = result;
                    }

                    if (entry.TryGetString(3, out result))
                    {
                        newEntry.ShortOverview = result;
                    }

                    if (entry.TryGetString(5, out result))
                    {
                        newEntry.ItemId = result;
                    }

                    newEntries.Add(newEntry);
                }

                dbContext.ActivityLogs.AddRange(newEntries);
                dbContext.SaveChanges();
            }

            try
            {
                File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));

                var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
                if (File.Exists(journalPath))
                {
                    File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
                }
            }
            catch (IOException e)
            {
                _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
            }
        }
    }
}