MigrateActivityLogDb.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using Emby.Server.Implementations.Data;
  5. using Jellyfin.Database.Implementations;
  6. using Jellyfin.Database.Implementations.Entities;
  7. using MediaBrowser.Controller;
  8. using Microsoft.Data.Sqlite;
  9. using Microsoft.EntityFrameworkCore;
  10. using Microsoft.Extensions.Logging;
  11. namespace Jellyfin.Server.Migrations.Routines
  12. {
  13. /// <summary>
  14. /// The migration routine for migrating the activity log database to EF Core.
  15. /// </summary>
  16. #pragma warning disable CS0618 // Type or member is obsolete
  17. [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")]
  18. public class MigrateActivityLogDb : IMigrationRoutine
  19. #pragma warning restore CS0618 // Type or member is obsolete
  20. {
  21. private const string DbFilename = "activitylog.db";
  22. private readonly ILogger<MigrateActivityLogDb> _logger;
  23. private readonly IDbContextFactory<JellyfinDbContext> _provider;
  24. private readonly IServerApplicationPaths _paths;
  25. /// <summary>
  26. /// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class.
  27. /// </summary>
  28. /// <param name="logger">The logger.</param>
  29. /// <param name="paths">The server application paths.</param>
  30. /// <param name="provider">The database provider.</param>
  31. public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, IDbContextFactory<JellyfinDbContext> provider)
  32. {
  33. _logger = logger;
  34. _provider = provider;
  35. _paths = paths;
  36. }
  37. /// <inheritdoc/>
  38. public void Perform()
  39. {
  40. var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase)
  41. {
  42. { "None", LogLevel.None },
  43. { "Trace", LogLevel.Trace },
  44. { "Debug", LogLevel.Debug },
  45. { "Information", LogLevel.Information },
  46. { "Info", LogLevel.Information },
  47. { "Warn", LogLevel.Warning },
  48. { "Warning", LogLevel.Warning },
  49. { "Error", LogLevel.Error },
  50. { "Critical", LogLevel.Critical }
  51. };
  52. var dataPath = _paths.DataPath;
  53. var activityLogPath = Path.Combine(dataPath, DbFilename);
  54. if (!File.Exists(activityLogPath))
  55. {
  56. _logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath);
  57. return;
  58. }
  59. using (var connection = new SqliteConnection($"Filename={activityLogPath}"))
  60. {
  61. connection.Open();
  62. var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';");
  63. foreach (var row in tableQuery)
  64. {
  65. if (row.GetInt32(0) == 0)
  66. {
  67. _logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath);
  68. break;
  69. }
  70. }
  71. using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
  72. userDbConnection.Open();
  73. _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
  74. using var dbContext = _provider.CreateDbContext();
  75. // Make sure that the database is empty in case of failed migration due to power outages, etc.
  76. dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
  77. dbContext.SaveChanges();
  78. // Reset the autoincrement counter
  79. dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';");
  80. dbContext.SaveChanges();
  81. var newEntries = new List<ActivityLog>();
  82. var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
  83. foreach (var entry in queryResult)
  84. {
  85. if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity))
  86. {
  87. severity = LogLevel.Trace;
  88. }
  89. var guid = Guid.Empty;
  90. if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid))
  91. {
  92. var id = entry.GetString(6);
  93. // This is not a valid Guid, see if it is an internal ID from an old Emby schema
  94. _logger.LogWarning("Invalid Guid in UserId column: {Guid}", id);
  95. using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
  96. statement.TryBind("@Id", id);
  97. using var reader = statement.ExecuteReader();
  98. if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid))
  99. {
  100. // Successfully parsed a Guid from the user table.
  101. break;
  102. }
  103. }
  104. var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid)
  105. {
  106. DateCreated = entry.GetDateTime(7),
  107. LogSeverity = severity
  108. };
  109. if (entry.TryGetString(2, out var result))
  110. {
  111. newEntry.Overview = result;
  112. }
  113. if (entry.TryGetString(3, out result))
  114. {
  115. newEntry.ShortOverview = result;
  116. }
  117. if (entry.TryGetString(5, out result))
  118. {
  119. newEntry.ItemId = result;
  120. }
  121. newEntries.Add(newEntry);
  122. }
  123. dbContext.ActivityLogs.AddRange(newEntries);
  124. dbContext.SaveChanges();
  125. }
  126. try
  127. {
  128. File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
  129. var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
  130. if (File.Exists(journalPath))
  131. {
  132. File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
  133. }
  134. }
  135. catch (IOException e)
  136. {
  137. _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'");
  138. }
  139. }
  140. }
  141. }