Less intrusive legacy restore changes

Due to the legacy restore flow being volume-centric, restoring has to become two phases: 1 for the priority files, and 2 for the "rest" of the files.
This commit is contained in:
Carl Johnsen
2026-02-19 17:52:53 +01:00
parent 1cadb775b8
commit 4d1e388e33
+236 -402
View File
@@ -494,21 +494,47 @@ namespace Duplicati.Library.Main.Operation
m_result.EndTime = DateTime.UtcNow;
}
/// <summary>
/// Creates a filter that matches only the given priority files.
/// Priority file names are matched as suffix patterns (e.g., "geometry.json" becomes "*geometry.json").
/// </summary>
/// <param name="priorityFiles">The list of priority file names to include.</param>
/// <returns>A filter that includes only files matching the priority file patterns.</returns>
private static Library.Utility.IFilter BuildPriorityFilter(IList<string> priorityFiles)
{
var priorityPatterns = priorityFiles.Select(pf => "*" + pf);
return new FilterExpression(priorityPatterns, true);
}
/// <summary>
/// Creates a filter that excludes the given priority files from the original filter.
/// The exclude filter is placed first so it takes precedence in the joined expression.
/// </summary>
/// <param name="priorityFiles">The list of priority file names to exclude.</param>
/// <param name="originalFilter">The original filter to combine with.</param>
/// <returns>A filter that matches the original filter but excludes priority files.</returns>
private static Library.Utility.IFilter BuildExcludePriorityFilter(IList<string> priorityFiles, Library.Utility.IFilter originalFilter)
{
var excludePatterns = priorityFiles.Select(pf => "*" + pf);
var excludeFilter = new FilterExpression(excludePatterns, false);
// Put exclude first so it takes precedence in JoinedFilterExpression
return JoinedFilterExpression.Join(excludeFilter, originalFilter);
}
/// <summary>
/// Perform the restore operation.
/// This is the legacy implementation, which performs the restore in a single thread. Kept as in case the new implementation fails.
/// </summary>
/// <param name="backendManager">The backend manager for downloading volumes.</param>
/// <param name="database">The database containing information about the restore.</param>
/// <param name="filter">The filter of which files to restore.</param>
/// <param name="restoreDestination">The destination to restore to.</param>
/// <param name="cancellationToken">The cancellation token to monitor for cancellation requests.</param>
private async Task DoRunAsync(IBackendManager backendManager, LocalRestoreDatabase database, Library.Utility.IFilter filter, IRestoreDestinationProvider restoreDestination, CancellationToken cancellationToken)
{
//In this case, we check that the remote storage fits with the database.
//We can then query the database and find the blocks that we need to do the restore
//using (var database = new LocalRestoreDatabase(dbparent))
using (var metadatastorage = new RestoreHandlerMetadataStorage())
{
// One-time setup
await Utility.UpdateOptionsFromDb(database, m_options, cancellationToken)
.ConfigureAwait(false);
await Utility.VerifyOptionsAndUpdateDatabase(database, m_options, cancellationToken)
@@ -521,83 +547,205 @@ namespace Duplicati.Library.Main.Operation
await FilelistProcessor.VerifyRemoteList(backendManager, m_options, database, m_result.BackendWriter, latestVolumesOnly: false, verifyMode: FilelistProcessor.VerifyMode.VerifyOnly, cancellationToken).ConfigureAwait(false);
}
//Figure out what files are to be patched, and what blocks are needed
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_CreateFileList);
using (new Logging.Timer(LOGTAG, "PrepareBlockList", "PrepareBlockList"))
await PrepareBlockAndFileList(database, m_options, filter, restoreDestination, m_result)
.ConfigureAwait(false);
// Get priority files from restore destination
var priorityFiles = restoreDestination.GetPriorityFiles();
//Make the entire output setup
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_CreateTargetFolders);
using (new Logging.Timer(LOGTAG, "CreateDirectory", "CreateDirectory"))
await CreateDirectoryStructure(database, restoreDestination, string.IsNullOrEmpty(restoreDestination.TargetDestination), m_options, m_result)
.ConfigureAwait(false);
//If we are patching an existing target folder, do not touch stuff that is already updated
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForExistingFiles);
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm))
using (new Logging.Timer(LOGTAG, "ScanForExistingTargetBlocks", "ScanForExistingTargetBlocks"))
await ScanForExistingTargetBlocks(database, m_blockbuffer, blockhasher, filehasher, m_options, restoreDestination, m_result).ConfigureAwait(false);
//Look for existing blocks in the original source files only
if (m_options.UseLocalBlocks && !string.IsNullOrEmpty(restoreDestination.TargetDestination))
if (priorityFiles.Count > 0)
{
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (new Logging.Timer(LOGTAG, "ScanForExistingSourceBlocksFast", "ScanForExistingSourceBlocksFast"))
// Phase 1: Restore priority files only
Logging.Log.WriteInformationMessage(LOGTAG, "PriorityFilesRestore", "Restoring {0} priority file(s) first: {1}", priorityFiles.Count, string.Join(", ", priorityFiles));
var priorityFilter = BuildPriorityFilter(priorityFiles);
var restoredBefore = m_result.RestoredFiles;
await RestoreCoreAsync(backendManager, database, priorityFilter, restoreDestination, metadatastorage, cancellationToken)
.ConfigureAwait(false);
// Warn about any priority files not found in backup
var restoredInPhase1 = m_result.RestoredFiles - restoredBefore;
if (restoredInPhase1 == 0)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForLocalBlocks);
await ScanForExistingSourceBlocksFast(database, m_options, m_blockbuffer, blockhasher, restoreDestination, m_result).ConfigureAwait(false);
foreach (var pf in priorityFiles)
Logging.Log.WriteWarningMessage(LOGTAG, "PriorityFileNotFound", null,
"Priority file '{0}' was not found in the backup. This may cause subsequent restore steps to fail.", pf);
}
// Phase 2: Restore remaining files (excluding priority files)
Logging.Log.WriteInformationMessage(LOGTAG, "RemainingFilesRestore", "Restoring remaining files (excluding priority files)");
var phase2Filter = BuildExcludePriorityFilter(priorityFiles, filter);
await RestoreCoreAsync(backendManager, database, phase2Filter, restoreDestination, metadatastorage, cancellationToken)
.ConfigureAwait(false);
}
else
{
// No priority files — single phase
await RestoreCoreAsync(backendManager, database, filter, restoreDestination, metadatastorage, cancellationToken)
.ConfigureAwait(false);
}
}
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_Finalize);
m_result.EndTime = DateTime.UtcNow;
}
/// <summary>
/// Core restore logic extracted from the legacy restore flow.
/// Performs the full restore pipeline: prepare file/block lists, scan for existing blocks,
/// download and patch from remote volumes, restore empty files, apply metadata, and verify.
/// </summary>
/// <param name="backendManager">The backend manager for downloading volumes.</param>
/// <param name="database">The database containing information about the restore.</param>
/// <param name="filter">The filter of which files to restore in this phase.</param>
/// <param name="restoreDestination">The destination to restore to.</param>
/// <param name="metadatastorage">Shared metadata storage across phases.</param>
/// <param name="cancellationToken">The cancellation token to monitor for cancellation requests.</param>
private async Task RestoreCoreAsync(IBackendManager backendManager, LocalRestoreDatabase database, Library.Utility.IFilter filter, IRestoreDestinationProvider restoreDestination, RestoreHandlerMetadataStorage metadatastorage, CancellationToken cancellationToken)
{
//Figure out what files are to be patched, and what blocks are needed
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_CreateFileList);
using (new Logging.Timer(LOGTAG, "PrepareBlockList", "PrepareBlockList"))
await PrepareBlockAndFileList(database, m_options, filter, restoreDestination, m_result)
.ConfigureAwait(false);
//Make the entire output setup
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_CreateTargetFolders);
using (new Logging.Timer(LOGTAG, "CreateDirectory", "CreateDirectory"))
await CreateDirectoryStructure(database, restoreDestination, string.IsNullOrEmpty(restoreDestination.TargetDestination), m_options, m_result)
.ConfigureAwait(false);
//If we are patching an existing target folder, do not touch stuff that is already updated
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForExistingFiles);
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm))
using (new Logging.Timer(LOGTAG, "ScanForExistingTargetBlocks", "ScanForExistingTargetBlocks"))
await ScanForExistingTargetBlocks(database, m_blockbuffer, blockhasher, filehasher, m_options, restoreDestination, m_result).ConfigureAwait(false);
//Look for existing blocks in the original source files only
if (m_options.UseLocalBlocks && !string.IsNullOrEmpty(restoreDestination.TargetDestination))
{
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (new Logging.Timer(LOGTAG, "ScanForExistingSourceBlocksFast", "ScanForExistingSourceBlocksFast"))
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForLocalBlocks);
await ScanForExistingSourceBlocksFast(database, m_options, m_blockbuffer, blockhasher, restoreDestination, m_result).ConfigureAwait(false);
}
}
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
// If other local files already have the blocks we want, we use them instead of downloading
if (m_options.UseLocalBlocks)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PatchWithLocalBlocks);
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (new Logging.Timer(LOGTAG, "PatchWithLocalBlocks", "PatchWithLocalBlocks"))
await ScanForExistingSourceBlocks(database, m_options, m_blockbuffer, blockhasher, m_result, metadatastorage, restoreDestination).ConfigureAwait(false);
}
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
// Fill BLOCKS with remote sources
List<IRemoteVolume> volumes;
using (new Logging.Timer(LOGTAG, "GetMissingVolumes", "GetMissingVolumes"))
volumes = await database
.GetMissingVolumes(cancellationToken)
.ToListAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (volumes.Count > 0)
{
Logging.Log.WriteInformationMessage(LOGTAG, "RemoteFileCount", "{0} remote files are required to restore", volumes.Count);
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_DownloadingRemoteFiles);
}
var brokenFiles = new List<string>();
using (new Logging.Timer(LOGTAG, "PatchWithBlocklist", "PatchWithBlocklist"))
await foreach (var (tmpfile, _, _, name) in backendManager.GetFilesOverlappedAsync(volumes, cancellationToken).ConfigureAwait(false))
{
try
{
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
using (tmpfile)
using (var blocks = new BlockVolumeReader(GetCompressionModule(name), tmpfile, m_options))
await PatchWithBlocklist(database, blocks, m_options, m_result, m_blockbuffer, metadatastorage, restoreDestination, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
brokenFiles.Add(name);
Logging.Log.WriteErrorMessage(LOGTAG, "PatchingFailed", ex, "Failed to patch with remote file: \"{0}\", message: {1}", name, ex.Message);
if (ex.IsAbortException())
throw;
}
}
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
var fileErrors = 0L;
// Restore empty files. They might not have any blocks so don't appear in any volume.
await foreach (var file in database.GetFilesToRestore(true, cancellationToken).Where(item => item.Length == 0).ConfigureAwait(false))
{
Logging.Log.WriteVerboseMessage(LOGTAG, "RestoreEmptyFile", "Restoring empty file \"{0}\"", file.Path);
try
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
var folderpath = SystemIO.IO_OS.PathGetDirectoryName(file.Path);
if (!string.IsNullOrEmpty(folderpath))
{
await restoreDestination.CreateFolderIfNotExists(folderpath, cancellationToken).ConfigureAwait(false);
}
// Just create the file and close it right away, empty statement is intentional.
using (var stream = await restoreDestination.OpenWrite(file.Path, cancellationToken).ConfigureAwait(false))
{
try
{
stream.SetLength(0);
}
catch (NotSupportedException)
{
// Some streams do not support setting length, ignore
}
}
}
// If other local files already have the blocks we want, we use them instead of downloading
if (m_options.UseLocalBlocks)
catch (Exception ex)
{
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PatchWithLocalBlocks);
using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm))
using (new Logging.Timer(LOGTAG, "PatchWithLocalBlocks", "PatchWithLocalBlocks"))
await ScanForExistingSourceBlocks(database, m_options, m_blockbuffer, blockhasher, m_result, metadatastorage, restoreDestination).ConfigureAwait(false);
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore empty file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex.IsAbortException())
throw;
}
}
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
// Enforcing the length of files is now already done during ScanForExistingTargetBlocks
// and thus not necessary anymore.
// Fill BLOCKS with remote sources
List<IRemoteVolume> volumes;
using (new Logging.Timer(LOGTAG, "GetMissingVolumes", "GetMissingVolumes"))
volumes = await database
.GetMissingVolumes(cancellationToken)
.ToListAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
// Apply metadata
if (!m_options.SkipMetadata)
await ApplyStoredMetadata(m_options, metadatastorage, restoreDestination, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
if (volumes.Count > 0)
{
Logging.Log.WriteInformationMessage(LOGTAG, "RemoteFileCount", "{0} remote files are required to restore", volumes.Count);
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_DownloadingRemoteFiles);
}
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
return;
var brokenFiles = new List<string>();
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PostRestoreVerify);
// Get priority files from restore destination and process them first
var priorityFiles = restoreDestination.GetPriorityFiles();
if (priorityFiles.Count > 0)
{
Logging.Log.WriteInformationMessage(LOGTAG, "PriorityFilesToRestore", "Found {0} priority files to restore first", priorityFiles.Count);
await RestorePriorityFilesAsync(database, volumes, backendManager, priorityFiles, restoreDestination, metadatastorage, cancellationToken);
}
using (new Logging.Timer(LOGTAG, "PatchWithBlocklist", "PatchWithBlocklist"))
await foreach (var (tmpfile, _, _, name) in backendManager.GetFilesOverlappedAsync(volumes, cancellationToken).ConfigureAwait(false))
if (m_options.PerformRestoredFileVerification)
{
// After all blocks in the files are restored, verify the file hash
using (var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm))
using (new Logging.Timer(LOGTAG, "RestoreVerification", "RestoreVerification"))
await foreach (var file in database.GetFilesToRestore(true, cancellationToken).ConfigureAwait(false))
{
try
{
@@ -607,353 +755,39 @@ namespace Duplicati.Library.Main.Operation
return;
}
using (tmpfile)
using (var blocks = new BlockVolumeReader(GetCompressionModule(name), tmpfile, m_options))
await PatchWithBlocklist(database, blocks, m_options, m_result, m_blockbuffer, metadatastorage, restoreDestination, cancellationToken)
.ConfigureAwait(false);
Logging.Log.WriteVerboseMessage(LOGTAG, "TestFileIntegrity", "Testing restored file integrity: {0}", file.Path);
string key;
long size;
using (var fs = await restoreDestination.OpenRead(file.Path, cancellationToken).ConfigureAwait(false))
{
size = fs.Length;
key = Convert.ToBase64String(filehasher.ComputeHash(fs));
}
if (key != file.Hash)
throw new Exception(string.Format("Failed to restore file: \"{0}\". File hash is {1}, expected hash is {2}", file.Path, key, file.Hash));
m_result.RestoredFiles++;
m_result.SizeOfRestoredFiles += size;
}
catch (Exception ex)
{
brokenFiles.Add(name);
Logging.Log.WriteErrorMessage(LOGTAG, "PatchingFailed", ex, "Failed to patch with remote file: \"{0}\", message: {1}", name, ex.Message);
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex.IsAbortException())
throw;
}
}
var fileErrors = 0L;
// Restore empty files. They might not have any blocks so don't appear in any volume.
await foreach (var file in database.GetFilesToRestore(true, cancellationToken).Where(item => item.Length == 0).ConfigureAwait(false))
{
Logging.Log.WriteVerboseMessage(LOGTAG, "RestoreEmptyFile", "Restoring empty file \"{0}\"", file.Path);
try
{
var folderpath = SystemIO.IO_OS.PathGetDirectoryName(file.Path);
if (!string.IsNullOrEmpty(folderpath))
{
await restoreDestination.CreateFolderIfNotExists(folderpath, cancellationToken).ConfigureAwait(false);
}
// Just create the file and close it right away, empty statement is intentional.
using (var stream = await restoreDestination.OpenWrite(file.Path, cancellationToken).ConfigureAwait(false))
{
try
{
stream.SetLength(0);
}
catch (NotSupportedException)
{
// Some streams do not support setting length, ignore
}
}
}
catch (Exception ex)
{
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore empty file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex.IsAbortException())
throw;
}
}
// Enforcing the length of files is now already done during ScanForExistingTargetBlocks
// and thus not necessary anymore.
// Apply metadata
if (!m_options.SkipMetadata)
await ApplyStoredMetadata(m_options, metadatastorage, restoreDestination, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
return;
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PostRestoreVerify);
if (m_options.PerformRestoredFileVerification)
{
// After all blocks in the files are restored, verify the file hash
using (var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm))
using (new Logging.Timer(LOGTAG, "RestoreVerification", "RestoreVerification"))
await foreach (var file in database.GetFilesToRestore(true, cancellationToken).ConfigureAwait(false))
{
try
{
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
Logging.Log.WriteVerboseMessage(LOGTAG, "TestFileIntegrity", "Testing restored file integrity: {0}", file.Path);
string key;
long size;
using (var fs = await restoreDestination.OpenRead(file.Path, cancellationToken).ConfigureAwait(false))
{
size = fs.Length;
key = Convert.ToBase64String(filehasher.ComputeHash(fs));
}
if (key != file.Hash)
throw new Exception(string.Format("Failed to restore file: \"{0}\". File hash is {1}, expected hash is {2}", file.Path, key, file.Hash));
m_result.RestoredFiles++;
m_result.SizeOfRestoredFiles += size;
}
catch (Exception ex)
{
fileErrors++;
Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore file: \"{0}\". Error message was: {1}", file.Path, ex.Message);
if (ex.IsAbortException())
throw;
}
}
}
if (fileErrors > 0 && brokenFiles.Count > 0)
Logging.Log.WriteInformationMessage(LOGTAG, "RestoreFailures", "Failed to restore {0} files, additionally the following files failed to download, which may be the cause:{1}{2}", fileErrors, Environment.NewLine, string.Join(Environment.NewLine, brokenFiles));
else if (fileErrors > 0)
Logging.Log.WriteInformationMessage(LOGTAG, "RestoreFailures", "Failed to restore {0} files", fileErrors);
else if (m_result.RestoredFiles == 0)
Logging.Log.WriteWarningMessage(LOGTAG, "NoFilesRestored", null, "Restore completed without errors but no files were restored");
// Drop the temp tables
await database.DropRestoreTable(cancellationToken).ConfigureAwait(false);
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
}
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_Finalize);
m_result.EndTime = DateTime.UtcNow;
}
if (fileErrors > 0 && brokenFiles.Count > 0)
Logging.Log.WriteInformationMessage(LOGTAG, "RestoreFailures", "Failed to restore {0} files, additionally the following files failed to download, which may be the cause:{1}{2}", fileErrors, Environment.NewLine, string.Join(Environment.NewLine, brokenFiles));
else if (fileErrors > 0)
Logging.Log.WriteInformationMessage(LOGTAG, "RestoreFailures", "Failed to restore {0} files", fileErrors);
/// <summary>
/// Restores priority files first by downloading and patching them before other files.
/// This ensures that geometry/metadata files are available for reconstructing driver instances.
/// </summary>
private async Task RestorePriorityFilesAsync(LocalRestoreDatabase database, List<IRemoteVolume> volumes, IBackendManager backendManager, IList<string> priorityFiles, IRestoreDestinationProvider restoreDestination, RestoreHandlerMetadataStorage metadatastorage, CancellationToken cancellationToken)
{
if (priorityFiles.Count == 0)
return;
Logging.Log.WriteInformationMessage(LOGTAG, "RestoringPriorityFiles", "Restoring {0} priority files first", priorityFiles.Count);
// Get the file IDs for priority files from the database
var priorityFileIds = new HashSet<long>();
var fileIdToPath = new Dictionary<long, string>();
await foreach (var file in database.GetFilesToRestore(true, cancellationToken).ConfigureAwait(false))
{
// Check if this file matches any priority file path
foreach (var priorityPath in priorityFiles)
{
if (file.Path.Equals(priorityPath, StringComparison.OrdinalIgnoreCase) ||
file.Path.EndsWith(priorityPath, StringComparison.OrdinalIgnoreCase))
{
// We need to query the database to get the file ID
// For now, we'll use the path as the identifier and look up the ID later
Logging.Log.WriteVerboseMessage(LOGTAG, "PriorityFileFound", "Found priority file: {0}", file.Path);
break;
}
}
}
if (priorityFileIds.Count == 0)
{
Logging.Log.WriteWarningMessage(LOGTAG, "NoPriorityFilesFound", null, "No priority files found in the restore set");
return;
}
// Find which volumes contain the priority files
var priorityVolumes = new List<IRemoteVolume>();
foreach (var volume in volumes)
{
// Check if this volume contains any priority file blocks
// We need to query the database to see if the volume has blocks for priority files
if (await VolumeContainsPriorityFilesAsync(database, volume, priorityFileIds, cancellationToken).ConfigureAwait(false))
{
priorityVolumes.Add(volume);
}
}
if (priorityVolumes.Count == 0)
{
Logging.Log.WriteWarningMessage(LOGTAG, "NoVolumesForPriorityFiles", null, "No volumes found containing priority files");
return;
}
Logging.Log.WriteInformationMessage(LOGTAG, "PriorityVolumesFound", "Found {0} volumes containing priority files", priorityVolumes.Count);
// Download and process priority volumes first
var brokenFiles = new List<string>();
await foreach (var (tmpfile, _, _, name) in backendManager.GetFilesOverlappedAsync(priorityVolumes, cancellationToken).ConfigureAwait(false))
{
try
{
if (!await m_result.TaskControl.ProgressRendevouz().ConfigureAwait(false))
{
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
return;
}
using (tmpfile)
using (var blocks = new BlockVolumeReader(GetCompressionModule(name), tmpfile, m_options))
await PatchPriorityFilesWithBlocklist(database, blocks, priorityFileIds, m_options, m_result, m_blockbuffer, metadatastorage, restoreDestination, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
brokenFiles.Add(name);
Logging.Log.WriteErrorMessage(LOGTAG, "PriorityFilePatchingFailed", ex, "Failed to patch priority file with remote file: \"{0}\", message: {1}", name, ex.Message);
if (ex.IsAbortException())
throw;
}
}
// Remove priority volumes from the main list so they're not processed again
volumes.RemoveAll(v => priorityVolumes.Contains(v));
Logging.Log.WriteInformationMessage(LOGTAG, "PriorityFilesRestored", "Priority files have been restored");
}
/// <summary>
/// Checks if a volume contains blocks for priority files.
/// </summary>
private static async Task<bool> VolumeContainsPriorityFilesAsync(LocalRestoreDatabase database, IRemoteVolume volume, HashSet<long> priorityFileIds, CancellationToken cancellationToken)
{
// Query the database to check if this volume has blocks for any priority file
// This is a simplified check - in practice, we would need to query the Block table
// to see if any blocks for priority files are in this volume
// For now, we'll return true to process all volumes (safe but potentially inefficient)
await Task.CompletedTask.ConfigureAwait(false);
return true;
}
/// <summary>
/// Patches only priority files from a block volume.
/// Similar to PatchWithBlocklist but only processes files in the priorityFileIds set.
/// </summary>
private static async Task PatchPriorityFilesWithBlocklist(LocalRestoreDatabase database, BlockVolumeReader blocks, HashSet<long> priorityFileIds, Options options, RestoreResults result, byte[] blockbuffer, RestoreHandlerMetadataStorage metadatastorage, IRestoreDestinationProvider restoreDestination, CancellationToken cancellationToken)
{
var blocksize = options.Blocksize;
var updateCounter = 0L;
var fullblockverification = options.FullBlockVerification;
using var blockhasher = HashFactory.CreateHasher(options.BlockHashAlgorithm);
await using var blockmarker = await database.CreateBlockMarkerAsync(cancellationToken).ConfigureAwait(false);
await using var volumekeeper = await database.GetMissingBlockData(blocks, options.Blocksize, cancellationToken).ConfigureAwait(false);
await foreach (var restorelist in volumekeeper.FilesWithMissingBlocks(cancellationToken).ConfigureAwait(false))
{
// Only process priority files
if (!priorityFileIds.Contains(restorelist.FileID))
continue;
var targetpath = restorelist.Path;
if (options.Dryrun)
{
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldPatchPriorityFile", "Would patch priority file with remote data: {0}", targetpath);
}
else
{
Logging.Log.WriteVerboseMessage(LOGTAG, "PatchingPriorityFile", "Patching priority file with remote data: {0}", targetpath);
try
{
if (!options.Dryrun)
{
var folderpath = SystemIO.IO_OS.PathGetDirectoryName(targetpath);
if (await restoreDestination.CreateFolderIfNotExists(folderpath, cancellationToken).ConfigureAwait(false))
Logging.Log.WriteWarningMessage(LOGTAG, "CreateMissingFolder", null, "Creating missing folder {0} for priority file {1}", folderpath, targetpath);
}
using (var file = await restoreDestination.OpenWrite(targetpath, cancellationToken).ConfigureAwait(false))
await foreach (var targetblock in restorelist.Blocks(cancellationToken).ConfigureAwait(false))
{
file.Position = targetblock.Offset;
var size = blocks.ReadBlock(targetblock.Key, blockbuffer);
if (targetblock.Size == size)
{
var valid = !fullblockverification;
if (!valid)
{
var key = Convert.ToBase64String(blockhasher.ComputeHash(blockbuffer, 0, size));
if (targetblock.Key == key)
valid = true;
else
Logging.Log.WriteWarningMessage(LOGTAG, "InvalidBlock", null, "Invalid block detected for {0}, expected hash: {1}, actual hash: {2}", targetpath, targetblock.Key, key);
}
if (valid)
{
file.Write(blockbuffer, 0, size);
await blockmarker
.SetBlockRestored(restorelist.FileID, targetblock.Offset / blocksize, targetblock.Key, size, false, cancellationToken)
.ConfigureAwait(false);
}
}
else
{
Logging.Log.WriteWarningMessage(LOGTAG, "WrongBlockSize", null, "Block with hash {0} should have size {1} but has size {2}", targetblock.Key, targetblock.Size, size);
}
}
if ((++updateCounter) % 20 == 0)
await blockmarker
.UpdateProcessed(result.OperationProgressUpdater, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
Logging.Log.WriteWarningMessage(LOGTAG, "PriorityFilePatchFailed", ex, "Failed to patch priority file: \"{0}\", message: {1}", targetpath, ex.Message);
if (options.UnittestMode)
throw;
}
}
}
if (!options.SkipMetadata)
{
await foreach (var restoremetadata in volumekeeper.MetadataWithMissingBlocks(cancellationToken).ConfigureAwait(false))
{
// Only process metadata for priority files
if (!priorityFileIds.Contains(restoremetadata.FileID))
continue;
var targetpath = restoremetadata.Path;
Logging.Log.WriteVerboseMessage(LOGTAG, "RecordingPriorityFileMetadata", "Recording metadata from remote data: {0}", targetpath);
try
{
using (var ms = new System.IO.MemoryStream())
{
await foreach (var targetblock in restoremetadata.Blocks(cancellationToken).ConfigureAwait(false))
{
ms.Position = targetblock.Offset;
var size = blocks.ReadBlock(targetblock.Key, blockbuffer);
if (targetblock.Size == size)
{
ms.Write(blockbuffer, 0, size);
await blockmarker
.SetBlockRestored(restoremetadata.FileID, targetblock.Offset / blocksize, targetblock.Key, size, true, cancellationToken)
.ConfigureAwait(false);
}
}
ms.Position = 0;
metadatastorage.Add(targetpath, ms);
}
}
catch (Exception ex)
{
Logging.Log.WriteWarningMessage(LOGTAG, "PriorityFileMetatdataRecordFailed", ex, "Failed to record metadata for priority file: \"{0}\", message: {1}", targetpath, ex.Message);
if (options.UnittestMode)
throw;
}
}
}
await blockmarker
.UpdateProcessed(result.OperationProgressUpdater, cancellationToken)
.ConfigureAwait(false);
await blockmarker.CommitAsync(cancellationToken).ConfigureAwait(false);
// Drop the temp tables
await database.DropRestoreTable(cancellationToken).ConfigureAwait(false);
await backendManager.WaitForEmptyAsync(database, cancellationToken).ConfigureAwait(false);
}
public static async Task<bool> ApplyMetadata(string path, System.IO.Stream stream, Options options, IRestoreDestinationProvider restoreDestination, CancellationToken cancellationToken)