Просмотр исходного кода

Clean up and fix backup/restore (#14489)

Cody Robibero 1 неделя назад
Родитель
Сommit
36c90ce2ce
1 измененных файлов с 33 добавлено и 20 удалено
  1. 33 20
      Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs

+ 33 - 20
Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs

@@ -39,7 +39,7 @@ public class BackupService : IBackupService
         ReferenceHandler = ReferenceHandler.IgnoreCycles,
     };
 
-    private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
+    private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
 
     /// <summary>
     /// Initializes a new instance of the <see cref="BackupService"/> class.
@@ -120,26 +120,29 @@ public class BackupService : IBackupService
 
             void CopyDirectory(string source, string target)
             {
-                source = Path.GetFullPath(source);
-                Directory.CreateDirectory(source);
-
+                var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
+                var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
                 foreach (var item in zipArchive.Entries)
                 {
-                    var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
-                    if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
+                    var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
+                    var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
+
+                    if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
+                        || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
                     {
                         continue;
                     }
 
-                    var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
                     _logger.LogInformation("Restore and override {File}", targetPath);
-                    item.ExtractToFile(targetPath);
+
+                    Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
+                    item.ExtractToFile(targetPath, overwrite: true);
                 }
             }
 
-            CopyDirectory(_applicationPaths.ConfigurationDirectoryPath, "Config/");
-            CopyDirectory(_applicationPaths.DataPath, "Data/");
-            CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
+            CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
+            CopyDirectory("Data", _applicationPaths.DataPath);
+            CopyDirectory("Root", _applicationPaths.RootFolderPath);
 
             if (manifest.Options.Database)
             {
@@ -148,7 +151,7 @@ public class BackupService : IBackupService
                 await using (dbContext.ConfigureAwait(false))
                 {
                     // restore migration history manually
-                    var historyEntry = zipArchive.GetEntry($"Database\\{nameof(HistoryRow)}.json");
+                    var historyEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{nameof(HistoryRow)}.json")));
                     if (historyEntry is null)
                     {
                         _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
@@ -193,7 +196,7 @@ public class BackupService : IBackupService
                     {
                         _logger.LogInformation("Read backup of {Table}", entityType.Type.Name);
 
-                        var zipEntry = zipArchive.GetEntry($"Database\\{entityType.Type.Name}.json");
+                        var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("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);
@@ -205,7 +208,7 @@ public class BackupService : IBackupService
                         {
                             _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
                             var records = 0;
-                            await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<JsonObject>(zipEntryStream, _serializerSettings).ConfigureAwait(false)!)
+                            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)
@@ -288,7 +291,7 @@ public class BackupService : IBackupService
             await using (dbContext.ConfigureAwait(false))
             {
                 dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
-                static IAsyncEnumerable<object> GetValues(IQueryable dbSet, Type type)
+                static IAsyncEnumerable<object> GetValues(IQueryable dbSet)
                 {
                     var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
                     var enumerable = method.Invoke(dbSet, null)!;
@@ -303,8 +306,8 @@ public class BackupService : IBackupService
                     .. typeof(JellyfinDbContext)
                     .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
                     .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
-                    .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
-                    (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
+                    .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))),
+                    (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
                 ];
                 manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
                 var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
@@ -316,7 +319,7 @@ public class BackupService : IBackupService
                     foreach (var entityType in entityTypes)
                     {
                         _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
-                        var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.SourceName}.json");
+                        var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json")));
                         var entities = 0;
                         var zipEntryStream = zipEntry.Open();
                         await using (zipEntryStream.ConfigureAwait(false))
@@ -354,7 +357,7 @@ public class BackupService : IBackupService
             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)));
+                zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item))));
             }
 
             void CopyDirectory(string source, string target, string filter = "*")
@@ -368,7 +371,7 @@ public class BackupService : IBackupService
 
                 foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories))
                 {
-                    zipArchive.CreateEntryFromFile(item, Path.Combine(target, item[..source.Length].Trim('\\')));
+                    zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item))));
                 }
             }
 
@@ -516,4 +519,14 @@ public class BackupService : IBackupService
             Database = options.Database
         };
     }
+
+    /// <summary>
+    /// Windows is able to handle '/' as a path seperator in zip files
+    /// but linux isn't able to handle '\' as a path seperator in zip files,
+    /// So normalize to '/'.
+    /// </summary>
+    /// <param name="path">The path to normalize.</param>
+    /// <returns>The normalized path. </returns>
+    private static string NormalizePathSeparator(string path)
+        => path.Replace('\\', '/');
 }