mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	Clean up and fix backup/restore (#14489)
This commit is contained in:
		
							parent
							
								
									48e93dcbce
								
							
						
					
					
						commit
						36c90ce2ce
					
				@ -39,7 +39,7 @@ public class BackupService : IBackupService
 | 
				
			|||||||
        ReferenceHandler = ReferenceHandler.IgnoreCycles,
 | 
					        ReferenceHandler = ReferenceHandler.IgnoreCycles,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly Version _backupEngineVersion = Version.Parse("0.1.0");
 | 
					    private readonly Version _backupEngineVersion = Version.Parse("0.2.0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Initializes a new instance of the <see cref="BackupService"/> class.
 | 
					    /// Initializes a new instance of the <see cref="BackupService"/> class.
 | 
				
			||||||
@ -120,26 +120,29 @@ public class BackupService : IBackupService
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            void CopyDirectory(string source, string target)
 | 
					            void CopyDirectory(string source, string target)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                source = Path.GetFullPath(source);
 | 
					                var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
 | 
				
			||||||
                Directory.CreateDirectory(source);
 | 
					                var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
 | 
				
			||||||
 | 
					 | 
				
			||||||
                foreach (var item in zipArchive.Entries)
 | 
					                foreach (var item in zipArchive.Entries)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    var sanitizedSourcePath = Path.GetFullPath(item.FullName.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
 | 
					                    var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
 | 
				
			||||||
                    if (!sanitizedSourcePath.StartsWith(target, StringComparison.Ordinal))
 | 
					                    var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
 | 
				
			||||||
 | 
					                        || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        continue;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    var targetPath = Path.Combine(source, sanitizedSourcePath[target.Length..].Trim('/'));
 | 
					 | 
				
			||||||
                    _logger.LogInformation("Restore and override {File}", targetPath);
 | 
					                    _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("Config", _applicationPaths.ConfigurationDirectoryPath);
 | 
				
			||||||
            CopyDirectory(_applicationPaths.DataPath, "Data/");
 | 
					            CopyDirectory("Data", _applicationPaths.DataPath);
 | 
				
			||||||
            CopyDirectory(_applicationPaths.RootFolderPath, "Root/");
 | 
					            CopyDirectory("Root", _applicationPaths.RootFolderPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (manifest.Options.Database)
 | 
					            if (manifest.Options.Database)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -148,7 +151,7 @@ public class BackupService : IBackupService
 | 
				
			|||||||
                await using (dbContext.ConfigureAwait(false))
 | 
					                await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    // restore migration history manually
 | 
					                    // 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)
 | 
					                    if (historyEntry is null)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        _logger.LogInformation("No backup of the history table in archive. This is required for Jellyfin operation");
 | 
					                        _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);
 | 
					                        _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)
 | 
					                        if (zipEntry is null)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name);
 | 
					                            _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);
 | 
					                            _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name);
 | 
				
			||||||
                            var records = 0;
 | 
					                            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]);
 | 
					                                var entity = item.Deserialize(entityType.Type.PropertyType.GetGenericArguments()[0]);
 | 
				
			||||||
                                if (entity is null)
 | 
					                                if (entity is null)
 | 
				
			||||||
@ -288,7 +291,7 @@ public class BackupService : IBackupService
 | 
				
			|||||||
            await using (dbContext.ConfigureAwait(false))
 | 
					            await using (dbContext.ConfigureAwait(false))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
 | 
					                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 method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!;
 | 
				
			||||||
                    var enumerable = method.Invoke(dbSet, null)!;
 | 
					                    var enumerable = method.Invoke(dbSet, null)!;
 | 
				
			||||||
@ -303,8 +306,8 @@ public class BackupService : IBackupService
 | 
				
			|||||||
                    .. typeof(JellyfinDbContext)
 | 
					                    .. typeof(JellyfinDbContext)
 | 
				
			||||||
                    .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
 | 
					                    .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
 | 
				
			||||||
                    .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
 | 
					                    .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)))),
 | 
					                    .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: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
 | 
					                    (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
 | 
				
			||||||
                ];
 | 
					                ];
 | 
				
			||||||
                manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
 | 
					                manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
 | 
				
			||||||
                var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
 | 
					                var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
 | 
				
			||||||
@ -316,7 +319,7 @@ public class BackupService : IBackupService
 | 
				
			|||||||
                    foreach (var entityType in entityTypes)
 | 
					                    foreach (var entityType in entityTypes)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
 | 
					                        _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 entities = 0;
 | 
				
			||||||
                        var zipEntryStream = zipEntry.Open();
 | 
					                        var zipEntryStream = zipEntry.Open();
 | 
				
			||||||
                        await using (zipEntryStream.ConfigureAwait(false))
 | 
					                        await using (zipEntryStream.ConfigureAwait(false))
 | 
				
			||||||
@ -354,7 +357,7 @@ public class BackupService : IBackupService
 | 
				
			|||||||
            foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
 | 
					            foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly)
 | 
				
			||||||
              .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", 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 = "*")
 | 
					            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))
 | 
					                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
 | 
					            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('\\', '/');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user