mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-31 14:33:54 -04: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