< 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
16%
Covered lines: 8
Uncovered lines: 40
Coverable lines: 48
Total lines: 560
Line coverage: 16.6%
Branch coverage
0%
Covered branches: 0
Total branches: 2
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/25/2025 - 12:09:58 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 53210/28/2025 - 12:11:27 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5591/19/2026 - 12:13:54 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 560 10/25/2025 - 12:09:58 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 53210/28/2025 - 12:11:27 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 5591/19/2026 - 12:13:54 AM Line coverage: 16.6% (8/48) Branch coverage: 0% (0/2) Total lines: 560

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
ScheduleRestoreAndRestartServer(...)100%210%
TestBackupVersionCompatibility(...)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    {
 85        _logger.LogWarning("Begin restoring system to {BackupArchive}", archivePath); // Info isn't cutting it
 86        if (!File.Exists(archivePath))
 87        {
 88            throw new FileNotFoundException($"Requested backup file '{archivePath}' does not exist.");
 89        }
 90
 91        StorageHelper.TestCommonPathsForStorageCapacity(_applicationPaths, _logger);
 92
 93        var fileStream = File.OpenRead(archivePath);
 94        await using (fileStream.ConfigureAwait(false))
 95        {
 96            using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read, false);
 97            var zipArchiveEntry = zipArchive.GetEntry(ManifestEntryName);
 98
 99            if (zipArchiveEntry is null)
 100            {
 101                throw new NotSupportedException($"The loaded archive '{archivePath}' does not appear to be a Jellyfin ba
 102            }
 103
 104            BackupManifest? manifest;
 105            var manifestStream = await zipArchiveEntry.OpenAsync().ConfigureAwait(false);
 106            await using (manifestStream.ConfigureAwait(false))
 107            {
 108                manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).Co
 109            }
 110
 111            if (manifest!.ServerVersion > _applicationHost.ApplicationVersion) // newer versions of Jellyfin should be a
 112            {
 113                throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jell
 114            }
 115
 116            if (!TestBackupVersionCompatibility(manifest.BackupEngineVersion))
 117            {
 118                throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jell
 119            }
 120
 121            void CopyDirectory(string source, string target)
 122            {
 123                var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
 124                var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
 125                foreach (var item in zipArchive.Entries)
 126                {
 127                    var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
 128                    var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)))
 129
 130                    if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
 131                        || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
 132                        || Path.EndsInDirectorySeparator(item.FullName))
 133                    {
 134                        continue;
 135                    }
 136
 137                    _logger.LogInformation("Restore and override {File}", targetPath);
 138
 139                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
 140                    item.ExtractToFile(targetPath, overwrite: true);
 141                }
 142            }
 143
 144            CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
 145            CopyDirectory("Data", _applicationPaths.DataPath);
 146            CopyDirectory("Root", _applicationPaths.RootFolderPath);
 147
 148            if (manifest.Options.Database)
 149            {
 150                _logger.LogInformation("Begin restoring Database");
 151                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 152                await using (dbContext.ConfigureAwait(false))
 153                {
 154                    // restore migration history manually
 155                    var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(His
 156                    if (historyEntry is null)
 157                    {
 158                        _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin
 159                        throw new InvalidOperationException("Cannot restore backup that has no History data.");
 160                    }
 161
 162                    HistoryRow[] historyEntries;
 163                    var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false);
 164                    await using (historyArchive.ConfigureAwait(false))
 165                    {
 166                        historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAw
 167                            throw new InvalidOperationException("Cannot restore backup that has no History data.");
 168                    }
 169
 170                    var historyRepository = dbContext.GetService<IHistoryRepository>();
 171                    await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false);
 172
 173                    foreach (var item in await historyRepository.GetAppliedMigrationsAsync(CancellationToken.None).Confi
 174                    {
 175                        var insertScript = historyRepository.GetDeleteScript(item.MigrationId);
 176                        await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
 177                    }
 178
 179                    foreach (var item in historyEntries)
 180                    {
 181                        var insertScript = historyRepository.GetInsertScript(item);
 182                        await dbContext.Database.ExecuteSqlRawAsync(insertScript).ConfigureAwait(false);
 183                    }
 184
 185                    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 186                    var entityTypes = typeof(JellyfinDbContext).GetProperties(System.Reflection.BindingFlags.Public | Sy
 187                        .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 188                        .Select(e => (Type: e, Set: e.GetValue(dbContext) as IQueryable))
 189                        .ToArray();
 190
 191                    var tableNames = entityTypes.Select(f => dbContext.Model.FindEntityType(f.Type.PropertyType.GetGener
 192                    _logger.LogInformation("Begin purging database");
 193                    await _jellyfinDatabaseProvider.PurgeDatabase(dbContext, tableNames).ConfigureAwait(false);
 194                    _logger.LogInformation("Database Purged");
 195
 196                    foreach (var entityType in entityTypes)
 197                    {
 198                        _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
 199
 200                        var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType
 201                        if (zipEntry is null)
 202                        {
 203                            _logger.LogInformation("No backup of expected table {Table} is present in backup, continuing
 204                            continue;
 205                        }
 206
 207                        var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
 208                        await using (zipEntryStream.ConfigureAwait(false))
 209                        {
 210                            _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
 211                            var records = 0;
 212                            await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStr
 213                            {
 214                                var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
 215                                if (entity is null)
 216                                {
 217                                    throw new InvalidOperationException($"Cannot deserialize entity '{item}'");
 218                                }
 219
 220                                try
 221                                {
 222                                    records++;
 223                                    dbContext.Add(entity);
 224                                }
 225                                catch (Exception ex)
 226                                {
 227                                    _logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item);
 228                                }
 229                            }
 230
 231                            _logger.LogInformation("Prepared to restore {Number} entries for {Table}", records, entityTy
 232                        }
 233                    }
 234
 235                    _logger.LogInformation("Try restore Database");
 236                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
 237                    _logger.LogInformation("Restored database");
 238                }
 239            }
 240
 241            _logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated);
 242        }
 243    }
 244
 245    private bool TestBackupVersionCompatibility(Version backupEngineVersion)
 246    {
 0247        if (backupEngineVersion == _backupEngineVersion)
 248        {
 0249            return true;
 250        }
 251
 0252        return false;
 253    }
 254
 255    /// <inheritdoc/>
 256    public async Task<BackupManifestDto> CreateBackupAsync(BackupOptionsDto backupOptions)
 257    {
 258        var manifest = new BackupManifest()
 259        {
 260            DateCreated = DateTime.UtcNow,
 261            ServerVersion = _applicationHost.ApplicationVersion,
 262            DatabaseTables = null!,
 263            BackupEngineVersion = _backupEngineVersion,
 264            Options = Map(backupOptions)
 265        };
 266
 267        _logger.LogInformation("Running database optimization before backup");
 268
 269        await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false);
 270
 271        var backupFolder = Path.Combine(_applicationPaths.BackupPath);
 272
 273        if (!Directory.Exists(backupFolder))
 274        {
 275            Directory.CreateDirectory(backupFolder);
 276        }
 277
 278        var backupStorageSpace = StorageHelper.GetFreeSpaceOf(_applicationPaths.BackupPath);
 279
 280        const long FiveGigabyte = 5_368_709_115;
 281        if (backupStorageSpace.FreeSpace < FiveGigabyte)
 282        {
 283            throw new InvalidOperationException($"The backup directory '{backupStorageSpace.Path}' does not have at leas
 284        }
 285
 286        var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss
 287
 288        try
 289        {
 290            _logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath);
 291            var fileStream = File.OpenWrite(backupPath);
 292            await using (fileStream.ConfigureAwait(false))
 293            using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
 294            {
 295                _logger.LogInformation("Starting backup process");
 296                var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
 297                await using (dbContext.ConfigureAwait(false))
 298                {
 299                    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 300
 301                    static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
 302                    {
 303                        var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
 304                        var enumerable = method.Invoke(dbSet, null)!;
 305                        return (IAsyncEnumerable<object>)enumerable;
 306                    }
 307
 308                    // include the migration history as well
 309                    var historyRepository = dbContext.GetService<IHistoryRepository>();
 310                    var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
 311
 312                    ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes
 313                    [
 314                        .. typeof(JellyfinDbContext)
 315                            .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instan
 316                            .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 317                            .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGeneric
 318                        (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyn
 319                    ];
 320                    manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
 321                    var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
 322
 323                    await using (transaction.ConfigureAwait(false))
 324                    {
 325                        _logger.LogInformation("Begin Database backup");
 326
 327                        foreach (var entityType in entityTypes)
 328                        {
 329                            _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
 330                            var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{ent
 331                            var entities = 0;
 332                            var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false);
 333                            await using (zipEntryStream.ConfigureAwait(false))
 334                            {
 335                                var jsonSerializer = new Utf8JsonWriter(zipEntryStream);
 336                                await using (jsonSerializer.ConfigureAwait(false))
 337                                {
 338                                    jsonSerializer.WriteStartArray();
 339
 340                                    var set = entityType.ValueFactory().ConfigureAwait(false);
 341                                    await foreach (var item in set.ConfigureAwait(false))
 342                                    {
 343                                        entities++;
 344                                        try
 345                                        {
 346                                            using var document = JsonSerializer.SerializeToDocument(item, _serializerSet
 347                                            document.WriteTo(jsonSerializer);
 348                                        }
 349                                        catch (Exception ex)
 350                                        {
 351                                            _logger.LogError(ex, "Could not load entity {Entity}", item);
 352                                            throw;
 353                                        }
 354                                    }
 355
 356                                    jsonSerializer.WriteEndArray();
 357                                }
 358                            }
 359
 360                            _logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceNa
 361                        }
 362                    }
 363                }
 364
 365                _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
 366                foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", Sea
 367                             .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", Sea
 368                {
 369                    await zipArchive.CreateEntryFromFileAsync(item, NormalizePathSeparator(Path.Combine("Config", Path.G
 370                }
 371
 372                void CopyDirectory(string source, string target, string filter = "*")
 373                {
 374                    if (!Directory.Exists(source))
 375                    {
 376                        return;
 377                    }
 378
 379                    _logger.LogInformation("Backup of folder {Table}", source);
 380
 381                    foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
 382                    {
 383                        // TODO: @bond make async
 384                        zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativ
 385                    }
 386                }
 387
 388                CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config"
 389                CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine
 390                CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root");
 391                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections
 392                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists"));
 393                CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "Schedule
 394                if (backupOptions.Subtitles)
 395                {
 396                    CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles
 397                }
 398
 399                if (backupOptions.Trickplay)
 400                {
 401                    CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay
 402                }
 403
 404                if (backupOptions.Metadata)
 405                {
 406                    CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")
 407                }
 408
 409                var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);
 410                await using (manifestStream.ConfigureAwait(false))
 411                {
 412                    await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false);
 413                }
 414            }
 415
 416            _logger.LogInformation("Backup created");
 417            return Map(manifest, backupPath);
 418        }
 419        catch (Exception ex)
 420        {
 421            _logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath);
 422            try
 423            {
 424                if (File.Exists(backupPath))
 425                {
 426                    File.Delete(backupPath);
 427                }
 428            }
 429            catch (Exception innerEx)
 430            {
 431                _logger.LogWarning(innerEx, "Unable to remove failed backup");
 432            }
 433
 434            throw;
 435        }
 436    }
 437
 438    /// <inheritdoc/>
 439    public async Task<BackupManifestDto?> GetBackupManifest(string archivePath)
 440    {
 441        if (!File.Exists(archivePath))
 442        {
 443            return null;
 444        }
 445
 446        BackupManifest? manifest;
 447        try
 448        {
 449            manifest = await GetManifest(archivePath).ConfigureAwait(false);
 450        }
 451        catch (Exception ex)
 452        {
 453            _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath);
 454            return null;
 455        }
 456
 457        if (manifest is null)
 458        {
 459            return null;
 460        }
 461
 462        return Map(manifest, archivePath);
 463    }
 464
 465    /// <inheritdoc/>
 466    public async Task<BackupManifestDto[]> EnumerateBackups()
 467    {
 468        if (!Directory.Exists(_applicationPaths.BackupPath))
 469        {
 470            return [];
 471        }
 472
 473        var archives = Directory.EnumerateFiles(_applicationPaths.BackupPath, "*.zip");
 474        var manifests = new List<BackupManifestDto>();
 475        foreach (var item in archives)
 476        {
 477            try
 478            {
 479                var manifest = await GetManifest(item).ConfigureAwait(false);
 480
 481                if (manifest is null)
 482                {
 483                    continue;
 484                }
 485
 486                manifests.Add(Map(manifest, item));
 487            }
 488            catch (Exception ex)
 489            {
 490                _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item);
 491            }
 492        }
 493
 494        return manifests.ToArray();
 495    }
 496
 497    private static async ValueTask<BackupManifest?> GetManifest(string archivePath)
 498    {
 499        var archiveStream = File.OpenRead(archivePath);
 500        await using (archiveStream.ConfigureAwait(false))
 501        {
 502            using var zipStream = new ZipArchive(archiveStream, ZipArchiveMode.Read);
 503            var manifestEntry = zipStream.GetEntry(ManifestEntryName);
 504            if (manifestEntry is null)
 505            {
 506                return null;
 507            }
 508
 509            var manifestStream = await manifestEntry.OpenAsync().ConfigureAwait(false);
 510            await using (manifestStream.ConfigureAwait(false))
 511            {
 512                return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).Config
 513            }
 514        }
 515    }
 516
 517    private static BackupManifestDto Map(BackupManifest manifest, string path)
 518    {
 0519        return new BackupManifestDto()
 0520        {
 0521            BackupEngineVersion = manifest.BackupEngineVersion,
 0522            DateCreated = manifest.DateCreated,
 0523            ServerVersion = manifest.ServerVersion,
 0524            Path = path,
 0525            Options = Map(manifest.Options)
 0526        };
 527    }
 528
 529    private static BackupOptionsDto Map(BackupOptions options)
 530    {
 0531        return new BackupOptionsDto()
 0532        {
 0533            Metadata = options.Metadata,
 0534            Subtitles = options.Subtitles,
 0535            Trickplay = options.Trickplay,
 0536            Database = options.Database
 0537        };
 538    }
 539
 540    private static BackupOptions Map(BackupOptionsDto options)
 541    {
 0542        return new BackupOptions()
 0543        {
 0544            Metadata = options.Metadata,
 0545            Subtitles = options.Subtitles,
 0546            Trickplay = options.Trickplay,
 0547            Database = options.Database
 0548        };
 549    }
 550
 551    /// <summary>
 552    /// Windows is able to handle '/' as a path seperator in zip files
 553    /// but linux isn't able to handle '\' as a path seperator in zip files,
 554    /// So normalize to '/'.
 555    /// </summary>
 556    /// <param name="path">The path to normalize.</param>
 557    /// <returns>The normalized path. </returns>
 558    private static string NormalizePathSeparator(string path)
 0559        => path.Replace('\\', '/');
 560}