< Summary - Jellyfin

Information
Class: Jellyfin.Server.Implementations.FullSystemBackup.BackupService
Assembly: Jellyfin.Server.Implementations
File(s): /srv/git/jellyfin/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
Line coverage
3%
Covered lines: 8
Uncovered lines: 258
Coverable lines: 266
Total lines: 577
Line coverage: 3%
Branch coverage
0%
Covered branches: 0
Total branches: 70
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5603/31/2026 - 12:14:24 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5774/19/2026 - 12:14:27 AM Line coverage: 3% (8/266) Branch coverage: 0% (0/70) Total lines: 577 1/23/2026 - 12:11:06 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5603/31/2026 - 12:14:24 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5774/19/2026 - 12:14:27 AM Line coverage: 3% (8/266) Branch coverage: 0% (0/70) Total lines: 577

Coverage delta

Coverage delta 14 -14

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
ScheduleRestoreAndRestartServer(...)100%210%
RestoreBackupAsync()0%702260%
TestBackupVersionCompatibility(...)0%620%
CreateBackupAsync()0%930300%
GetBackupManifest()0%2040%
EnumerateBackups()0%4260%
GetManifest()0%620%
Map(...)100%210%
Map(...)100%210%
Map(...)100%210%
NormalizePathSeparator(...)100%210%

File(s)

/srv/git/jellyfin/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.IO.Compression;
 5using System.Linq;
 6using System.Text.Json;
 7using System.Text.Json.Nodes;
 8using System.Text.Json.Serialization;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Jellyfin.Database.Implementations;
 12using Jellyfin.Server.Implementations.StorageHelpers;
 13using Jellyfin.Server.Implementations.SystemBackupService;
 14using MediaBrowser.Controller;
 15using MediaBrowser.Controller.SystemBackupService;
 16using Microsoft.EntityFrameworkCore;
 17using Microsoft.EntityFrameworkCore.Infrastructure;
 18using Microsoft.EntityFrameworkCore.Migrations;
 19using Microsoft.Extensions.Hosting;
 20using Microsoft.Extensions.Logging;
 21
 22namespace Jellyfin.Server.Implementations.FullSystemBackup;
 23
 24/// <summary>
 25/// Contains methods for creating and restoring backups.
 26/// </summary>
 27public class BackupService : IBackupService
 28{
 29    private const string ManifestEntryName = "manifest.json";
 30    private readonly ILogger<BackupService> _logger;
 31    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 32    private readonly IServerApplicationHost _applicationHost;
 33    private readonly IServerApplicationPaths _applicationPaths;
 34    private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
 35    private readonly IHostApplicationLifetime _hostApplicationLifetime;
 036    private static readonly JsonSerializerOptions _serializerSettings = new JsonSerializerOptions(JsonSerializerDefaults
 037    {
 038        AllowTrailingCommas = true,
 039        ReferenceHandler = ReferenceHandler.IgnoreCycles,
 040    };
 41
 2142    private readonly Version _backupEngineVersion = new Version(0, 2, 0);
 43
 44    /// <summary>
 45    /// Initializes a new instance of the <see cref="BackupService"/> class.
 46    /// </summary>
 47    /// <param name="logger">A logger.</param>
 48    /// <param name="dbProvider">A Database Factory.</param>
 49    /// <param name="applicationHost">The Application host.</param>
 50    /// <param name="applicationPaths">The application paths.</param>
 51    /// <param name="jellyfinDatabaseProvider">The Jellyfin database Provider in use.</param>
 52    /// <param name="applicationLifetime">The SystemManager.</param>
 53    public BackupService(
 54        ILogger<BackupService> logger,
 55        IDbContextFactory<JellyfinDbContext> dbProvider,
 56        IServerApplicationHost applicationHost,
 57        IServerApplicationPaths applicationPaths,
 58        IJellyfinDatabaseProvider jellyfinDatabaseProvider,
 59        IHostApplicationLifetime applicationLifetime)
 60    {
 2161        _logger = logger;
 2162        _dbProvider = dbProvider;
 2163        _applicationHost = applicationHost;
 2164        _applicationPaths = applicationPaths;
 2165        _jellyfinDatabaseProvider = jellyfinDatabaseProvider;
 2166        _hostApplicationLifetime = applicationLifetime;
 2167    }
 68
 69    /// <inheritdoc/>
 70    public void ScheduleRestoreAndRestartServer(string archivePath)
 71    {
 072        _applicationHost.RestoreBackupPath = archivePath;
 073        _applicationHost.ShouldRestart = true;
 074        _applicationHost.NotifyPendingRestart();
 075        _ = Task.Run(async () =>
 076        {
 077            await Task.Delay(500).ConfigureAwait(false);
 078            _hostApplicationLifetime.StopApplication();
 079        });
 080    }
 81
 82    /// <inheritdoc/>
 83    public async Task RestoreBackupAsync(string archivePath)
 84    {
 085        _logger.LogWarning("Begin restoring system to {BackupArchive}", archivePath); // Info isn't cutting it
 086        if (!File.Exists(archivePath))
 87        {
 088            throw new FileNotFoundException($"Requested backup file '{archivePath}' does not exist.");
 89        }
 90
 091        StorageHelper.TestCommonPathsForStorageCapacity(_applicationPaths, _logger);
 92
 093        var fileStream = File.OpenRead(archivePath);
 094        await using (fileStream.ConfigureAwait(false))
 95        {
 096            using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, false);
 097            var zipArchiveEntry = zipArchive.GetEntry(ManifestEntryName);
 98
 099            if (zipArchiveEntry is null)
 100            {
 0101                throw new NotSupportedException($"The loaded archive '{archivePath}' does not appear to be a Jellyfin ba
 102            }
 103
 104            BackupManifest? manifest;
 0105            var manifestStream = await zipArchiveEntry.OpenAsync().ConfigureAwait(false);
 0106            await using (manifestStream.ConfigureAwait(false))
 107            {
 0108                manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).Co
 109            }
 110
 0111            if (manifest!.ServerVersion > _applicationHost.ApplicationVersion) // newer versions of Jellyfin should be a
 112            {
 0113                throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jell
 114            }
 115
 0116            if (!TestBackupVersionCompatibility(manifest.BackupEngineVersion))
 117            {
 0118                throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jell
 119            }
 120
 121            void CopyDirectory(string source, string target, string[]? exclude = null)
 122            {
 123                var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
 124                var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
 125                var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
 126                foreach (var item in zipArchive.Entries)
 127                {
 128                    var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
 129                    var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)))
 130
 131                    if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.O
 132                    {
 133                        continue;
 134                    }
 135
 136                    if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
 137                        || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
 138                        || Path.EndsInDirectorySeparator(item.FullName))
 139                    {
 140                        continue;
 141                    }
 142
 143                    _logger.LogInformation("Restore and override {File}", targetPath);
 144
 145                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
 146                    item.ExtractToFile(targetPath, overwrite: true);
 147                }
 148            }
 149
 0150            CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
 0151            CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
 0152            CopyDirectory("Root", _applicationPaths.RootFolderPath);
 0153            CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
 0154            CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
 155
 0156            if (manifest.Options.Database)
 157            {
 0158                _logger.LogInformation("Begin restoring Database");
 0159                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0160                await using (dbContext.ConfigureAwait(false))
 161                {
 162                    // restore migration history manually
 0163                    var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(His
 0164                    if (historyEntry is null)
 165                    {
 0166                        _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin
 0167                        throw new InvalidOperationException("Cannot restore backup that has no History data.");
 168                    }
 169
 170                    HistoryRow[] historyEntries;
 0171                    var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
 0172                    await using (historyArchive.ConfigureAwait(false))
 173                    {
 0174                        historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAw
 0175                            throw new InvalidOperationException("Cannot restore backup that has no History data.");
 176                    }
 177
 0178                    var historyRepository = dbContext.GetService<IHistoryRepository>();
 0179                    await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
 180
 0181                    foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).Confi
 182                    {
 0183                        var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
 0184                        await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
 185                    }
 186
 0187                    foreach (var item in historyEntries)
 188                    {
 0189                        var insertScript = historyRepository.GetInsertScript(item);
 0190                        await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
 191                    }
 192
 0193                    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 0194                    var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | Sy
 0195                        .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 0196                        .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
 0197                        .ToArray();
 198
 0199                    var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGener
 0200                    _logger.LogInformation("Begin purging database");
 0201                    await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false);
 0202                    _logger.LogInformation("Database Purged");
 203
 0204                    foreach (var entityType in entityTypes)
 205                    {
 0206                        _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
 207
 0208                        var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType
 0209                        if (zipEntry is null)
 210                        {
 0211                            _logger.LogInformation("No backup of expected table {Table} is present in backup, continuing
 0212                            continue;
 213                        }
 214
 0215                        var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
 0216                        await using (zipEntryStream.ConfigureAwait(false))
 217                        {
 0218                            _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
 0219                            var records = 0;
 0220                            await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStr
 221                            {
 0222                                var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
 0223                                if (entity is null)
 224                                {
 0225                                    throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
 226                                }
 227
 228                                try
 229                                {
 0230                                    records++;
 0231                                    dbContext.Add(entity);
 0232                                }
 0233                                catch (Exception ex)
 234                                {
 0235                                    _logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
 0236                                }
 237                            }
 238
 0239                            _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityTy
 240                        }
 0241                    }
 242
 0243                    _logger.LogInformation("Try restore Database");
 0244                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 0245                    _logger.LogInformation("Restored database");
 0246                }
 0247            }
 248
 0249            _logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
 0250        }
 0251    }
 252
 253    private bool TestBackupVersionCompatibility(Version backupEngineVersion)
 254    {
 0255        if (backupEngineVersion == _backupEngineVersion)
 256        {
 0257            return true;
 258        }
 259
 0260        return false;
 261    }
 262
 263    /// <inheritdoc/>
 264    public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
 265    {
 0266        var manifest = new BackupManifest()
 0267        {
 0268            DateCreated = DateTime.UtcNow,
 0269            ServerVersion = _applicationHost.ApplicationVersion,
 0270            DatabaseTables = null!,
 0271            BackupEngineVersion = _backupEngineVersion,
 0272            Options = Map(backupOptions)
 0273        };
 274
 0275        _logger.LogInformation("Running database optimization before backup");
 276
 0277        await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
 278
 0279        var backupFolder = Path.Combine(_applicationPaths.BackupPath);
 280
 0281        if (!Directory.Exists(backupFolder))
 282        {
 0283            Directory.CreateDirectory(backupFolder);
 284        }
 285
 0286        var backupStorageSpace = StorageHelper.GetFreeSpaceOf(_applicationPaths.BackupPath);
 287
 288        const long FiveGigabyte = 5_368_709_115;
 0289        if (backupStorageSpace.FreeSpace < FiveGigabyte)
 290        {
 0291            throw new InvalidOperationException($"The backup directory '{backupStorageSpace.Path}' does not have at leas
 292        }
 293
 0294        var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss
 295
 296        try
 297        {
 0298            _logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
 0299            var fileStream = File.OpenWrite(backupPath);
 0300            await using (fileStream.ConfigureAwait(false))
 0301            using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
 302            {
 0303                _logger.LogInformation("Starting backup process");
 0304                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 0305                await using (dbContext.ConfigureAwait(false))
 306                {
 0307                    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 308
 309                    static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
 310                    {
 311                        var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
 312                        var enumerable = method.Invoke(dbSet, null)!;
 313                        return (IAsyncEnumerable<object>)enumerable;
 314                    }
 315
 316                    // include the migration history as well
 0317                    var historyRepository = dbContext.GetService<IHistoryRepository>();
 0318                    var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
 319
 0320                    ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes
 0321                    [
 0322                        .. typeof(JellyfinDbContext)
 0323                            .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instan
 0324                            .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 0325                            .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGeneric
 0326                        (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyn
 0327                    ];
 0328                    manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
 0329                    var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
 330
 0331                    await using (transaction.ConfigureAwait(false))
 332                    {
 0333                        _logger.LogInformation("Begin Database backup");
 334
 0335                        foreach (var entityType in entityTypes)
 336                        {
 0337                            _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
 0338                            var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{ent
 0339                            var entities = 0;
 0340                            var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
 0341                            await using (zipEntryStream.ConfigureAwait(false))
 342                            {
 0343                                var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
 0344                                await using (jsonSerializer.ConfigureAwait(false))
 345                                {
 0346                                    jsonSerializer.WriteStartArray();
 347
 0348                                    var set = entityType.ValueFactory().ConfigureAwait(false);
 0349                                    await foreach (var item in set.ConfigureAwait(false))
 350                                    {
 0351                                        entities++;
 352                                        try
 353                                        {
 0354                                            using var document = JsonSerializer.SerializeToDocument(item, _serializerSet
 0355                                            document.WriteTo(jsonSerializer);
 0356                                        }
 0357                                        catch (Exception ex)
 358                                        {
 0359                                            _logger.LogError(ex, "Could not load entity {Entity}", item);
 0360                                            throw;
 361                                        }
 362                                    }
 363
 0364                                    jsonSerializer.WriteEndArray();
 365                                }
 0366                            }
 367
 0368                            _logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceNa
 0369                        }
 370                    }
 0371                }
 372
 0373                _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
 0374                foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", Sea
 0375                             .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", Sea
 376                {
 0377                    await zipArchive.CreateEntryFromFileAsync(item, NormalizePathSeparator(Path.Combine("Config", Path.G
 378                }
 379
 380                void CopyDirectory(string source, string target, string filter = "*")
 381                {
 382                    if (!Directory.Exists(source))
 383                    {
 384                        return;
 385                    }
 386
 387                    _logger.LogInformation("Backup of folder {Table}", source);
 388
 389                    foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
 390                    {
 391                        // TODO: @bond make async
 392                        zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativ
 393                    }
 394                }
 395
 0396                CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config"
 0397                CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine
 0398                CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
 0399                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections
 0400                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
 0401                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "Schedule
 0402                if (backupOptions.Subtitles)
 403                {
 0404                    CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles
 405                }
 406
 0407                if (backupOptions.Trickplay)
 408                {
 0409                    CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay
 410                }
 411
 0412                if (backupOptions.Metadata)
 413                {
 0414                    CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")
 415
 416                    // If a custom metadata path is configured, the default location may still contain data.
 0417                    if (!string.Equals(
 0418                            Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
 0419                            Path.GetFullPath(_applicationPaths.InternalMetadataPath),
 0420                            StringComparison.OrdinalIgnoreCase))
 421                    {
 0422                        CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", 
 423                    }
 424                }
 425
 0426                var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);
 0427                await using (manifestStream.ConfigureAwait(false))
 428                {
 0429                    await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
 430                }
 0431            }
 432
 0433            _logger.LogInformation("Backup created");
 0434            return Map(manifest, backupPath);
 435        }
 0436        catch (Exception ex)
 437        {
 0438            _logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
 439            try
 440            {
 0441                if (File.Exists(backupPath))
 442                {
 0443                    File.Delete(backupPath);
 444                }
 0445            }
 0446            catch (Exception innerEx)
 447            {
 0448                _logger.LogWarning(innerEx, "Unable to remove failed backup");
 0449            }
 450
 0451            throw;
 452        }
 0453    }
 454
 455    /// <inheritdoc/>
 456    public async Task<BackupManifestDto?> GetBackupManifest(string archivePath)
 457    {
 0458        if (!File.Exists(archivePath))
 459        {
 0460            return null;
 461        }
 462
 463        BackupManifest? manifest;
 464        try
 465        {
 0466            manifest = await GetManifest(archivePath).ConfigureAwait(false);
 0467        }
 0468        catch (Exception ex)
 469        {
 0470            _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
 0471            return null;
 472        }
 473
 0474        if (manifest is null)
 475        {
 0476            return null;
 477        }
 478
 0479        return Map(manifest, archivePath);
 0480    }
 481
 482    /// <inheritdoc/>
 483    public async Task<BackupManifestDto[]> EnumerateBackups()
 484    {
 0485        if (!Directory.Exists(_applicationPaths.BackupPath))
 486        {
 0487            return [];
 488        }
 489
 0490        var archives = Directory.EnumerateFiles(_applicationPaths.BackupPath, "*.zip");
 0491        var manifests = new List<BackupManifestDto>();
 0492        foreach (var item in archives)
 493        {
 494            try
 495            {
 0496                var manifest = await GetManifest(item).ConfigureAwait(false);
 497
 0498                if (manifest is null)
 499                {
 0500                    continue;
 501                }
 502
 0503                manifests.Add(Map(manifest, item));
 0504            }
 0505            catch (Exception ex)
 506            {
 0507                _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
 0508            }
 0509        }
 510
 0511        return manifests.ToArray();
 0512    }
 513
 514    private static async ValueTask<BackupManifest?> GetManifest(string archivePath)
 515    {
 0516        var archiveStream = File.OpenRead(archivePath);
 0517        await using (archiveStream.ConfigureAwait(false))
 518        {
 0519            using var zipStream = new ZipArchive(archiveStream, ZipArchiveMode.Read);
 0520            var manifestEntry = zipStream.GetEntry(ManifestEntryName);
 0521            if (manifestEntry is null)
 522            {
 0523                return null;
 524            }
 525
 0526            var manifestStream = await manifestEntry.OpenAsync().ConfigureAwait(false);
 0527            await using (manifestStream.ConfigureAwait(false))
 528            {
 0529                return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).Config
 530            }
 0531        }
 0532    }
 533
 534    private static BackupManifestDto Map(BackupManifest manifest, string path)
 535    {
 0536        return new BackupManifestDto()
 0537        {
 0538            BackupEngineVersion = manifest.BackupEngineVersion,
 0539            DateCreated = manifest.DateCreated,
 0540            ServerVersion = manifest.ServerVersion,
 0541            Path = path,
 0542            Options = Map(manifest.Options)
 0543        };
 544    }
 545
 546    private static BackupOptionsDto Map(BackupOptions options)
 547    {
 0548        return new BackupOptionsDto()
 0549        {
 0550            Metadata = options.Metadata,
 0551            Subtitles = options.Subtitles,
 0552            Trickplay = options.Trickplay,
 0553            Database = options.Database
 0554        };
 555    }
 556
 557    private static BackupOptions Map(BackupOptionsDto options)
 558    {
 0559        return new BackupOptions()
 0560        {
 0561            Metadata = options.Metadata,
 0562            Subtitles = options.Subtitles,
 0563            Trickplay = options.Trickplay,
 0564            Database = options.Database
 0565        };
 566    }
 567
 568    /// <summary>
 569    /// Windows is able to handle '/' as a path seperator in zip files
 570    /// but linux isn't able to handle '\' as a path seperator in zip files,
 571    /// So normalize to '/'.
 572    /// </summary>
 573    /// <param name="path">The path to normalize.</param>
 574    /// <returns>The normalized path. </returns>
 575    private static string NormalizePathSeparator(string path)
 0576        => path.Replace('\\', '/');
 577}