| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 | 
							- using System;
 
- using System.Collections.Generic;
 
- using System.IO;
 
- using System.IO.Compression;
 
- using System.Linq;
 
- using System.Text.Json;
 
- using System.Text.Json.Nodes;
 
- using System.Text.Json.Serialization;
 
- using System.Threading;
 
- using System.Threading.Tasks;
 
- using Jellyfin.Database.Implementations;
 
- using Jellyfin.Server.Implementations.StorageHelpers;
 
- using Jellyfin.Server.Implementations.SystemBackupService;
 
- using MediaBrowser.Controller;
 
- using MediaBrowser.Controller.SystemBackupService;
 
- using Microsoft.EntityFrameworkCore;
 
- using Microsoft.Extensions.Logging;
 
- namespace Jellyfin.Server.Implementations.FullSystemBackup;
 
- /// <summary>
 
- /// Contains methods for creating and restoring backups.
 
- /// </summary>
 
- public class BackupService : IBackupService
 
- {
 
-     private const string ManifestEntryName = "manifest.json";
 
-     private readonly ILogger<BackupService> _logger;
 
-     private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 
-     private readonly IServerApplicationHost _applicationHost;
 
-     private readonly IServerApplicationPaths _applicationPaths;
 
-     private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
 
-     private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults.General)
 
-     {
 
-         AllowTrailingCommas = true,
 
-         ReferenceHandler = ReferenceHandler.IgnoreCycles,
 
-     };
 
-     private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
 
-     /// <summary>
 
-     /// Initializes a new instance of the <see cref="BackupService"/> class.
 
-     /// </summary>
 
-     /// <param name="logger">A logger.</param>
 
-     /// <param name="dbProvider">A Database Factory.</param>
 
-     /// <param name="applicationHost">The Application host.</param>
 
-     /// <param name="applicationPaths">The application paths.</param>
 
-     /// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</param>
 
-     public BackupService(
 
-         ILogger<BackupService> logger,
 
-         IDbContextFactory<JellyfinDbContext> dbProvider,
 
-         IServerApplicationHost applicationHost,
 
-         IServerApplicationPaths applicationPaths,
 
-         IJellyfinDatabaseProvider jellyfinDatabaseProvider)
 
-     {
 
-         _logger = logger;
 
-         _dbProvider = dbProvider;
 
-         _applicationHost = applicationHost;
 
-         _applicationPaths = applicationPaths;
 
-         _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 
-     }
 
-     /// <inheritdoc/>
 
-     public void ScheduleRestoreAndRestartServer(string archivePath)
 
-     {
 
-         _applicationHost.RestoreBackupPath = archivePath;
 
-         _applicationHost.ShouldRestart = true;
 
-         _applicationHost.NotifyPendingRestart();
 
-     }
 
-     /// <inheritdoc/>
 
-     public async Task RestoreBackupAsync(string archivePath)
 
-     {
 
-         _logger.LogWarning("Begin restoring system to {BackupArchive}", archivePath); // Info isn't cutting it
 
-         if (!File.Exists(archivePath))
 
-         {
 
-             throw new FileNotFoundException($"Requested backup file '{archivePath}' does not exist.");
 
-         }
 
-         StorageHelper.TestCommonPathsForStorageCapacity(_applicationPaths, _logger);
 
-         var fileStream = File.OpenRead(archivePath);
 
-         await using (fileStream.ConfigureAwait(false))
 
-         {
 
-             using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, false);
 
-             var zipArchiveEntry = zipArchive.GetEntry(ManifestEntryName);
 
-             if (zipArchiveEntry is null)
 
-             {
 
-                 throw new NotSupportedException($"The loaded archive '{archivePath}' does not appear to be a Jellyfin backup as its missing the '{ManifestEntryName}'.");
 
-             }
 
-             BackupManifest? manifest;
 
-             var manifestStream = zipArchiveEntry.Open();
 
-             await using (manifestStream.ConfigureAwait(false))
 
-             {
 
-                 manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
 
-             }
 
-             if (manifest!.ServerVersion > _applicationHost.ApplicationVersion) // newer versions of Jellyfin should be able to load older versions as we have migrations.
 
-             {
 
-                 throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
 
-             }
 
-             if (!TestBackupVersionCompatibility(manifest.BackupEngineVersion))
 
-             {
 
-                 throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
 
-             }
 
-             void CopyDirectory(string source, string target)
 
-             {
 
-                 source = Path.GetFullPath(source);
 
-                 Directory.CreateDirectory(source);
 
-                 foreach (var item in zipArchive.Entries)
 
-                 {
 
-                     var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
 
-                     if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
 
-                     {
 
-                         continue;
 
-                     }
 
-                     var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
 
-                     _logger.LogInformation("Restore and override {File}", targetPath);
 
-                     item.ExtractToFile(targetPath);
 
-                 }
 
-             }
 
-             CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
 
-             CopyDirectory(_applicationPaths.DataPath, "Data/");
 
-             CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
 
-             _logger.LogInformation("Begin restoring Database");
 
-             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 
-             await using (dbContext.ConfigureAwait(false))
 
-             {
 
-                 dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 
-                 var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
 
-                     .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 
-                     .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
 
-                     .ToArray();
 
-                 var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!);
 
-                 _logger.LogInformation("Begin purging database");
 
-                 await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false);
 
-                 _logger.LogInformation("Database Purged");
 
-                 foreach (var entityType in entityTypes)
 
-                 {
 
-                     _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
 
-                     var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
 
-                     if (zipEntry is null)
 
-                     {
 
-                         _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
 
-                         continue;
 
-                     }
 
-                     var zipEntryStream = zipEntry.Open();
 
-                     await using (zipEntryStream.ConfigureAwait(false))
 
-                     {
 
-                         _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
 
-                         var records = 0;
 
-                         await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
 
-                         {
 
-                             var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
 
-                             if (entity is null)
 
-                             {
 
-                                 throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
 
-                             }
 
-                             try
 
-                             {
 
-                                 records++;
 
-                                 dbContext.Add(entity);
 
-                             }
 
-                             catch (Exception ex)
 
-                             {
 
-                                 _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item);
 
-                             }
 
-                         }
 
-                         _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityType.Type.Name);
 
-                     }
 
-                 }
 
-                 _logger.LogInformation("Try restore Database");
 
-                 await dbContext.SaveChangesAsync().ConfigureAwait(false);
 
-                 _logger.LogInformation("Restored database.");
 
-             }
 
-             _logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated);
 
-         }
 
-     }
 
-     private bool TestBackupVersionCompatibility(Version backupEngineVersion)
 
-     {
 
-         if (backupEngineVersion == _backupEngineVersion)
 
-         {
 
-             return true;
 
-         }
 
-         return false;
 
-     }
 
-     /// <inheritdoc/>
 
-     public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
 
-     {
 
-         var manifest = new BackupManifest()
 
-         {
 
-             DateCreated = DateTime.UtcNow,
 
-             ServerVersion = _applicationHost.ApplicationVersion,
 
-             DatabaseTables = null!,
 
-             BackupEngineVersion = _backupEngineVersion,
 
-             Options = Map(backupOptions)
 
-         };
 
-         await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
 
-         var backupFolder = Path.Combine(_applicationPaths.BackupPath);
 
-         if (!Directory.Exists(backupFolder))
 
-         {
 
-             Directory.CreateDirectory(backupFolder);
 
-         }
 
-         var backupStorageSpace = StorageHelper.GetFreeSpaceOf(_applicationPaths.BackupPath);
 
-         const long FiveGigabyte = 5_368_709_115;
 
-         if (backupStorageSpace.FreeSpace < FiveGigabyte)
 
-         {
 
-             throw new InvalidOperationException($"The backup directory '{backupStorageSpace.Path}' does not have at least '{StorageHelper.HumanizeStorageSize(FiveGigabyte)}' free space. Cannot create backup.");
 
-         }
 
-         var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip");
 
-         _logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath);
 
-         var fileStream = File.OpenWrite(backupPath);
 
-         await using (fileStream.ConfigureAwait(false))
 
-         using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
 
-         {
 
-             _logger.LogInformation("Start backup process.");
 
-             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 
-             await using (dbContext.ConfigureAwait(false))
 
-             {
 
-                 dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 
-                 var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
 
-                     .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 
-                     .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
 
-                     .ToArray();
 
-                 manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
 
-                 var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
 
-                 await using (transaction.ConfigureAwait(false))
 
-                 {
 
-                     _logger.LogInformation("Begin Database backup");
 
-                     static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
 
-                     {
 
-                         var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
 
-                         var enumerable = method.Invoke(dbSet, null)!;
 
-                         return (IAsyncEnumerable<object>)enumerable;
 
-                     }
 
-                     foreach (var entityType in entityTypes)
 
-                     {
 
-                         _logger.LogInformation("Begin backup of entity {Table}", entityType.Type.Name);
 
-                         var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.Type.Name}.json");
 
-                         var entities = 0;
 
-                         var zipEntryStream = zipEntry.Open();
 
-                         await using (zipEntryStream.ConfigureAwait(false))
 
-                         {
 
-                             var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
 
-                             await using (jsonSerializer.ConfigureAwait(false))
 
-                             {
 
-                                 jsonSerializer.WriteStartArray();
 
-                                 var set = GetValues(entityType.Set!, entityType.Type.PropertyType).ConfigureAwait(false);
 
-                                 await foreach (var item in set.ConfigureAwait(false))
 
-                                 {
 
-                                     entities++;
 
-                                     try
 
-                                     {
 
-                                         JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer);
 
-                                     }
 
-                                     catch (Exception ex)
 
-                                     {
 
-                                         _logger.LogError(ex, "Could not load entity {Entity}", item);
 
-                                         throw;
 
-                                     }
 
-                                 }
 
-                                 jsonSerializer.WriteEndArray();
 
-                             }
 
-                         }
 
-                         _logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities);
 
-                     }
 
-                 }
 
-             }
 
-             _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
 
-             foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
 
-               .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly)))
 
-             {
 
-                 zipArchive.CreateEntryFromFile(item, Path.Combine("Config", Path.GetFileName(item)));
 
-             }
 
-             void CopyDirectory(string source, string target, string filter = "*")
 
-             {
 
-                 if (!Directory.Exists(source))
 
-                 {
 
-                     return;
 
-                 }
 
-                 _logger.LogInformation("Backup of folder {Table}", source);
 
-                 foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
 
-                 {
 
-                     zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
 
-                 }
 
-             }
 
-             CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users"));
 
-             CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks"));
 
-             CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
 
-             CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections"));
 
-             CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
 
-             CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks"));
 
-             if (backupOptions.Subtitles)
 
-             {
 
-                 CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles"));
 
-             }
 
-             if (backupOptions.Trickplay)
 
-             {
 
-                 CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay"));
 
-             }
 
-             if (backupOptions.Metadata)
 
-             {
 
-                 CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
 
-             }
 
-             var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open();
 
-             await using (manifestStream.ConfigureAwait(false))
 
-             {
 
-                 await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
 
-             }
 
-         }
 
-         _logger.LogInformation("Backup created");
 
-         return Map(manifest, backupPath);
 
-     }
 
-     /// <inheritdoc/>
 
-     public async Task<BackupManifestDto?> GetBackupManifest(string archivePath)
 
-     {
 
-         if (!File.Exists(archivePath))
 
-         {
 
-             return null;
 
-         }
 
-         BackupManifest? manifest;
 
-         try
 
-         {
 
-             manifest = await GetManifest(archivePath).ConfigureAwait(false);
 
-         }
 
-         catch (Exception ex)
 
-         {
 
-             _logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath);
 
-             return null;
 
-         }
 
-         if (manifest is null)
 
-         {
 
-             return null;
 
-         }
 
-         return Map(manifest, archivePath);
 
-     }
 
-     /// <inheritdoc/>
 
-     public async Task<BackupManifestDto[]> EnumerateBackups()
 
-     {
 
-         if (!Directory.Exists(_applicationPaths.BackupPath))
 
-         {
 
-             return [];
 
-         }
 
-         var archives = Directory.EnumerateFiles(_applicationPaths.BackupPath, "*.zip");
 
-         var manifests = new List<BackupManifestDto>();
 
-         foreach (var item in archives)
 
-         {
 
-             try
 
-             {
 
-                 var manifest = await GetManifest(item).ConfigureAwait(false);
 
-                 if (manifest is null)
 
-                 {
 
-                     continue;
 
-                 }
 
-                 manifests.Add(Map(manifest, item));
 
-             }
 
-             catch (Exception ex)
 
-             {
 
-                 _logger.LogError(ex, "Could not load {BackupArchive} path.", item);
 
-             }
 
-         }
 
-         return manifests.ToArray();
 
-     }
 
-     private static async ValueTask<BackupManifest?> GetManifest(string archivePath)
 
-     {
 
-         var archiveStream = File.OpenRead(archivePath);
 
-         await using (archiveStream.ConfigureAwait(false))
 
-         {
 
-             using var zipStream = new ZipArchive(archiveStream, ZipArchiveMode.Read);
 
-             var manifestEntry = zipStream.GetEntry(ManifestEntryName);
 
-             if (manifestEntry is null)
 
-             {
 
-                 return null;
 
-             }
 
-             var manifestStream = manifestEntry.Open();
 
-             await using (manifestStream.ConfigureAwait(false))
 
-             {
 
-                 return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false);
 
-             }
 
-         }
 
-     }
 
-     private static BackupManifestDto Map(BackupManifest manifest, string path)
 
-     {
 
-         return new BackupManifestDto()
 
-         {
 
-             BackupEngineVersion = manifest.BackupEngineVersion,
 
-             DateCreated = manifest.DateCreated,
 
-             ServerVersion = manifest.ServerVersion,
 
-             Path = path,
 
-             Options = Map(manifest.Options)
 
-         };
 
-     }
 
-     private static BackupOptionsDto Map(BackupOptions options)
 
-     {
 
-         return new BackupOptionsDto()
 
-         {
 
-             Metadata = options.Metadata,
 
-             Subtitles = options.Subtitles,
 
-             Trickplay = options.Trickplay
 
-         };
 
-     }
 
-     private static BackupOptions Map(BackupOptionsDto options)
 
-     {
 
-         return new BackupOptions()
 
-         {
 
-             Metadata = options.Metadata,
 
-             Subtitles = options.Subtitles,
 
-             Trickplay = options.Trickplay
 
-         };
 
-     }
 
- }
 
 
  |