Added functionality for priority files for restore providers

This commit is contained in:
Carl Johnsen
2026-02-09 11:36:15 +01:00
parent fbaf2968c6
commit 3fe57b308d
11 changed files with 622 additions and 5 deletions
@@ -148,5 +148,12 @@ public interface IRestoreDestinationProvider : IDisposable
/// <returns>A task that completes when the deletion is done</returns>
Task DeleteFile(string path, CancellationToken cancel);
/// <summary>
/// Gets a list of priority files that should be restored first.
/// These files are typically metadata files needed to reconstruct
/// the target structure (e.g., disk geometry, partition tables) before
/// the actual data files are restored.
/// </summary>
/// <returns>A list of file paths that should be restored first, in order of priority.</returns>
IList<string> GetPriorityFiles();
}
@@ -20,10 +20,12 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CoCoL;
using Duplicati.Library.Interface;
using Duplicati.Library.Main.Database;
#nullable enable
@@ -50,6 +52,18 @@ namespace Duplicati.Library.Main.Operation.Restore
/// <param name="options">The restore options</param>
/// <param name="result">The restore results</param>
public static Task Run(Channels channels, LocalRestoreDatabase db, Options options, RestoreResults result)
=> Run(channels, db, options, result, null);
/// <summary>
/// Runs the file lister process that lists the files that need to be restored
/// and sends them to the <see cref="FileProcessor"/>.
/// </summary>
/// <param name="channels">The named channels for the restore operation.</param>
/// <param name="db">The restore database, which is queried for the file list.</param>
/// <param name="options">The restore options</param>
/// <param name="result">The restore results</param>
/// <param name="restoreDestination">The restore destination provider, used to get priority files.</param>
public static Task Run(Channels channels, LocalRestoreDatabase db, Options options, RestoreResults result, IRestoreDestinationProvider? restoreDestination)
{
return AutomationExtensions.RunTask(
new
@@ -77,9 +91,33 @@ namespace Duplicati.Library.Main.Operation.Restore
result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_DownloadingRemoteFiles);
sw_get_files?.Stop();
// Get priority files from the restore destination provider
var priorityFiles = restoreDestination?.GetPriorityFiles() ?? Array.Empty<string>();
sw_write_file?.Start();
foreach (var file in files)
await self.Output.WriteAsync(file).ConfigureAwait(false);
// First, send priority files (if any) to be restored first
if (priorityFiles.Count > 0)
{
var priorityFileSet = new HashSet<string>(priorityFiles, StringComparer.OrdinalIgnoreCase);
var priorityFileList = files.Where(f => priorityFileSet.Any(pf => f.TargetPath.EndsWith(pf, StringComparison.OrdinalIgnoreCase))).ToList();
var remainingFiles = files.Where(f => !priorityFileSet.Any(pf => f.TargetPath.EndsWith(pf, StringComparison.OrdinalIgnoreCase))).ToList();
// Send priority files first
foreach (var file in priorityFileList)
await self.Output.WriteAsync(file).ConfigureAwait(false);
// Then send remaining files
foreach (var file in remainingFiles)
await self.Output.WriteAsync(file).ConfigureAwait(false);
}
else
{
// No priority files, send all files in order
foreach (var file in files)
await self.Output.WriteAsync(file).ConfigureAwait(false);
}
sw_write_file?.Stop();
if (!options.SkipMetadata)
@@ -364,7 +364,7 @@ namespace Duplicati.Library.Main.Operation
Restore.FileProcessor.file_processors_restoring_files = m_options.RestoreFileProcessors;
// Create the process network
var filelister = Restore.FileLister.Run(channels, database, m_options, m_result);
var filelister = Restore.FileLister.Run(channels, database, m_options, m_result, restoreDestination);
var fileprocessors = Enumerable.Range(0, m_options.RestoreFileProcessors).Select(i => Restore.FileProcessor.Run(channels, database, fileprocessor_requests[i], fileprocessor_responses[i], restoreDestination, m_options, m_result)).ToArray();
var blockmanager = Restore.BlockManager.Run(channels, database, fileprocessor_requests, fileprocessor_responses, m_options, m_result);
var volumecache = Restore.VolumeManager.Run(channels, m_options, m_result);
@@ -166,4 +166,11 @@ public class FileRestoreDestinationProvider(string mountedPath) : IRestoreDestin
SystemIO.IO_OS.SetMetadata(path, metadata, restorePermissions);
return Task.FromResult(wrote_something);
}
/// <inheritdoc />
public IList<string> GetPriorityFiles()
{
// File-based restore doesn't have special priority files
return Array.Empty<string>();
}
}
+210
View File
@@ -0,0 +1,210 @@
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Duplicati.Proprietary.DiskImage;
/// <summary>
/// Represents the consolidated geometry metadata for a disk image.
/// This metadata is stored as a single file during backup and used during restore
/// to reconstruct the disk, partition table, partition, and filesystem structures.
/// </summary>
public class GeometryMetadata
{
/// <summary>
/// The version of the metadata format.
/// </summary>
public int Version { get; set; } = 1;
/// <summary>
/// Disk-level geometry information.
/// </summary>
public DiskGeometry? Disk { get; set; }
/// <summary>
/// Partition table-level geometry information.
/// </summary>
public PartitionTableGeometry? PartitionTable { get; set; }
/// <summary>
/// Array of partition geometries.
/// </summary>
public List<PartitionGeometry>? Partitions { get; set; }
/// <summary>
/// Array of filesystem geometries, keyed by partition number.
/// </summary>
public List<FilesystemGeometry>? Filesystems { get; set; }
/// <summary>
/// Serializes this metadata to a JSON string.
/// </summary>
public string ToJson()
{
return JsonSerializer.Serialize(this, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
});
}
/// <summary>
/// Deserializes a JSON string to a GeometryMetadata instance.
/// </summary>
public static GeometryMetadata? FromJson(string json)
{
return JsonSerializer.Deserialize<GeometryMetadata>(json, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
}
}
/// <summary>
/// Disk-level geometry information.
/// </summary>
public class DiskGeometry
{
/// <summary>
/// The device path of the source disk.
/// </summary>
public string? DevicePath { get; set; }
/// <summary>
/// The total size of the disk in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// The sector size in bytes.
/// </summary>
public int SectorSize { get; set; }
/// <summary>
/// The number of sectors on the disk.
/// </summary>
public int Sectors { get; set; }
/// <summary>
/// The type of partition table on the disk.
/// </summary>
public PartitionTableType TableType { get; set; }
}
/// <summary>
/// Partition table-level geometry information.
/// </summary>
public class PartitionTableGeometry
{
/// <summary>
/// The type of partition table (MBR, GPT, etc.).
/// </summary>
public PartitionTableType Type { get; set; }
/// <summary>
/// The size of the partition table data in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// The sector size of the disk.
/// </summary>
public int SectorSize { get; set; }
/// <summary>
/// For GPT: whether the disk has a protective MBR.
/// </summary>
public bool HasProtectiveMbr { get; set; }
/// <summary>
/// For GPT: the size of the GPT header in bytes.
/// </summary>
public int HeaderSize { get; set; }
/// <summary>
/// For MBR: the size of the MBR in bytes (typically 512).
/// </summary>
public int MbrSize { get; set; }
}
/// <summary>
/// Partition-level geometry information.
/// </summary>
public class PartitionGeometry
{
/// <summary>
/// The partition number (1-based).
/// </summary>
public int Number { get; set; }
/// <summary>
/// The partition type.
/// </summary>
public PartitionType Type { get; set; }
/// <summary>
/// The starting offset of the partition in bytes.
/// </summary>
public long StartOffset { get; set; }
/// <summary>
/// The size of the partition in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// The partition name (if available).
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The filesystem type detected for this partition.
/// </summary>
public FileSystemType FilesystemType { get; set; }
/// <summary>
/// The volume GUID for GPT partitions.
/// </summary>
public Guid? VolumeGuid { get; set; }
/// <summary>
/// The partition table type this partition belongs to.
/// </summary>
public PartitionTableType TableType { get; set; }
}
/// <summary>
/// Filesystem-level geometry information.
/// </summary>
public class FilesystemGeometry
{
/// <summary>
/// The partition number this filesystem resides on.
/// </summary>
public int PartitionNumber { get; set; }
/// <summary>
/// The type of filesystem.
/// </summary>
public FileSystemType Type { get; set; }
/// <summary>
/// The starting offset of the partition in bytes.
/// </summary>
public long PartitionStartOffset { get; set; }
/// <summary>
/// The block size used by the filesystem.
/// </summary>
public int BlockSize { get; set; }
/// <summary>
/// Filesystem-specific metadata as a JSON string.
/// </summary>
public string? Metadata { get; set; }
}
+137
View File
@@ -54,6 +54,12 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
/// </summary>
private readonly ConcurrentDictionary<int, IFilesystem> _filesystemCache = new();
/// <summary>
/// Stores geometry metadata parsed from restored geometry files.
/// Used to reconstruct disk, partition, and filesystem structures.
/// </summary>
private readonly Dictionary<string, GeometryMetadata> _geometryMetadata = new();
/// <summary>
/// Represents a pending write operation.
/// </summary>
@@ -240,6 +246,7 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
"partition_table" => OpenWritePartitionTable(path, metadata, cancel),
"disk" => OpenWriteDisk(path, metadata, cancel),
"partition" => OpenWritePartition(path, metadata, cancel),
"geometry" => OpenWriteGeometry(path, metadata, cancel),
"block" or "file" => OpenWriteThroughFilesystem(path, metadata, cancel),
_ => throw new NotSupportedException($"Unsupported item type: {typeStr}")
};
@@ -322,6 +329,57 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
return Task.FromResult<Stream>(wrapper);
}
/// <summary>
/// Opens a stream for writing geometry metadata (stored in memory until Finalize).
/// </summary>
private Task<Stream> OpenWriteGeometry(string path, Dictionary<string, string?> metadata, CancellationToken cancel)
{
var stream = new MemoryStream();
var wrapper = new CaptureStream(stream, data =>
{
try
{
// Parse the geometry metadata from the JSON data
var json = System.Text.Encoding.UTF8.GetString(data);
var geometry = GeometryMetadata.FromJson(json);
if (geometry != null)
{
_geometryMetadata[path] = geometry;
Log.WriteInformationMessage(LOGTAG, "GeometryMetadataReceived",
$"Received consolidated geometry metadata for {path} (Version: {geometry.Version})");
// Store the geometry metadata in the regular metadata dictionary as well
metadata["geometry:Parsed"] = "true";
metadata["geometry:Version"] = geometry.Version.ToString();
if (geometry.Disk != null)
{
metadata["geometry:DiskTableType"] = geometry.Disk.TableType.ToString();
}
if (geometry.Partitions != null)
{
metadata["geometry:PartitionCount"] = geometry.Partitions.Count.ToString();
}
if (geometry.Filesystems != null)
{
metadata["geometry:FilesystemCount"] = geometry.Filesystems.Count.ToString();
}
}
}
catch (Exception ex)
{
Log.WriteWarningMessage(LOGTAG, "GeometryMetadataParseFailed", ex,
$"Failed to parse geometry metadata for {path}");
}
});
return Task.FromResult<Stream>(wrapper);
}
/// <summary>
/// Parses a path to extract partition and filesystem information.
/// Expected path format: root/part_{PartitionTableType}_{PartitionNumber}/fs_{FileSystemType}/...
@@ -472,6 +530,7 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
/// <summary>
/// Gets or creates a partition instance for the specified partition number.
/// Uses geometry metadata if available to reconstruct the partition structure.
/// </summary>
private Task<IPartition> GetOrCreatePartitionAsync(int partitionNumber, long startOffset, PartitionTableType tableType, CancellationToken cancel)
{
@@ -480,6 +539,17 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
if (_targetDisk == null)
throw new InvalidOperationException("Target disk not initialized.");
// Try to find geometry metadata for this partition
var partitionGeometry = FindPartitionGeometry(partitionNumber);
if (partitionGeometry != null)
{
// Use geometry metadata to reconstruct the partition
Log.WriteInformationMessage(LOGTAG, "UsingPartitionGeometry",
$"Using geometry metadata for partition #{partitionNumber}");
startOffset = partitionGeometry.StartOffset;
tableType = partitionGeometry.TableType;
}
// Create a partition table wrapper with the correct type
var partitionTable = new TargetPartitionTable(_targetDisk, tableType);
@@ -495,11 +565,23 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
/// <summary>
/// Gets or creates a filesystem instance for the specified partition.
/// Uses geometry metadata if available to reconstruct the filesystem structure.
/// </summary>
private Task<IFilesystem> GetOrCreateFilesystemAsync(int partitionNumber, IPartition partition, FileSystemType filesystemType, int blockSize, CancellationToken cancel)
{
return Task.FromResult(_filesystemCache.GetOrAdd(partitionNumber, _ =>
{
// Try to find filesystem geometry metadata for this partition
var fsGeometry = FindFilesystemGeometry(partitionNumber);
if (fsGeometry != null)
{
// Use geometry metadata to reconstruct the filesystem
Log.WriteInformationMessage(LOGTAG, "UsingFilesystemGeometry",
$"Using geometry metadata for filesystem on partition #{partitionNumber}");
filesystemType = fsGeometry.Type;
blockSize = fsGeometry.BlockSize;
}
// For unknown filesystems (or any filesystem we don't have specific support for),
// use the UnknownFilesystem which handles raw block access
IFilesystem filesystem = filesystemType switch
@@ -514,6 +596,52 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
}));
}
/// <summary>
/// Finds partition geometry metadata for the specified partition number.
/// Searches through all consolidated geometry metadata entries.
/// </summary>
private PartitionGeometry? FindPartitionGeometry(int partitionNumber)
{
foreach (var kvp in _geometryMetadata)
{
var geometry = kvp.Value;
if (geometry.Partitions != null)
{
foreach (var partition in geometry.Partitions)
{
if (partition.Number == partitionNumber)
{
return partition;
}
}
}
}
return null;
}
/// <summary>
/// Finds filesystem geometry metadata for the specified partition number.
/// Searches through all consolidated geometry metadata entries.
/// </summary>
private FilesystemGeometry? FindFilesystemGeometry(int partitionNumber)
{
foreach (var kvp in _geometryMetadata)
{
var geometry = kvp.Value;
if (geometry.Filesystems != null)
{
foreach (var filesystem in geometry.Filesystems)
{
if (filesystem.PartitionNumber == partitionNumber)
{
return filesystem;
}
}
}
}
return null;
}
/// <summary>
/// A simple partition table implementation for the target disk during restore.
/// </summary>
@@ -952,6 +1080,15 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos
public Task DeleteFile(string path, CancellationToken cancel)
=> Task.CompletedTask;
/// <inheritdoc />
public IList<string> GetPriorityFiles()
{
// Return the single consolidated geometry metadata file
// This file contains all geometry information needed to reconstruct
// the disk, partition table, partitions, and filesystems
return new List<string> { "geometry.json" };
}
/// <inheritdoc />
public async Task Finalize(Action<double>? progressCallback, CancellationToken cancel)
{
@@ -21,14 +21,102 @@ internal class DiskSourceEntry(SourceProvider provider, IRawDisk disk)
public override async IAsyncEnumerable<ISourceProviderEntry> Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
// Create a consolidated geometry metadata object
var geometryMetadata = new GeometryMetadata
{
Version = 1,
Disk = new DiskGeometry
{
DevicePath = disk.DevicePath,
Size = disk.Size,
SectorSize = disk.SectorSize,
Sectors = disk.Sectors,
TableType = PartitionTableType.Unknown // Will be updated if partition table is found
},
Partitions = new List<PartitionGeometry>(),
Filesystems = new List<FilesystemGeometry>()
};
var table = await PartitionTableFactory.CreateAsync(disk, cancellationToken);
if (table != null)
{
// Update disk geometry with partition table type
geometryMetadata.Disk!.TableType = table.TableType;
// Add partition table geometry
geometryMetadata.PartitionTable = new PartitionTableGeometry
{
Type = table.TableType,
SectorSize = disk.SectorSize,
Size = 0, // Will be updated below
HasProtectiveMbr = table.TableType == PartitionTableType.GPT,
HeaderSize = table.TableType == PartitionTableType.GPT ? 92 : 0,
MbrSize = table.TableType == PartitionTableType.MBR ? 512 : 0
};
// Get the actual partition table data size
try
{
using var tableData = await table.GetPartitionTableDataAsync(cancellationToken);
geometryMetadata.PartitionTable.Size = tableData.Length;
}
catch
{
// Use estimated sizes if we can't get the actual data
geometryMetadata.PartitionTable.Size = table.TableType switch
{
PartitionTableType.MBR => 512,
PartitionTableType.GPT => 512 + 512 + (128 * 4),
_ => 0
};
}
// First, yield the consolidated geometry metadata file
yield return new GeometrySourceEntry(this.Path, geometryMetadata);
// Also yield the partition table as a regular entry
yield return new PartitionTableSourceEntry(this.Path, table, disk);
// Then enumerate partitions and collect their geometry
await foreach (var partition in table.EnumeratePartitions(cancellationToken))
{
yield return new PartitionSourceEntry(this.Path, partition);
// Add partition geometry to the consolidated metadata
geometryMetadata.Partitions.Add(new PartitionGeometry
{
Number = partition.PartitionNumber,
Type = partition.Type,
StartOffset = partition.StartOffset,
Size = partition.Size,
Name = partition.Name,
FilesystemType = partition.FilesystemType,
VolumeGuid = partition.VolumeGuid,
TableType = partition.PartitionTable.TableType
});
// Create partition source entry which will add filesystem geometry
var partitionEntry = new PartitionSourceEntry(this.Path, partition);
// Pre-enumerate to collect filesystem geometry
await foreach (var child in partitionEntry.Enumerate(cancellationToken))
{
if (child is FilesystemSourceEntry fsEntry)
{
// Collect filesystem geometry
var fsGeometry = await fsEntry.GetFilesystemGeometry(cancellationToken);
if (fsGeometry != null)
{
geometryMetadata.Filesystems.Add(fsGeometry);
}
}
yield return child;
}
}
}
else
{
// No partition table found, still yield the disk geometry
yield return new GeometrySourceEntry(this.Path, geometryMetadata);
}
}
public override async Task<Dictionary<string, string?>> GetMinorMetadata(CancellationToken cancellationToken)
@@ -17,14 +17,40 @@ internal class FilesystemSourceEntry(string parentPath, IFilesystem filesystem)
{
public override bool IsFolder => true;
public IFilesystem Filesystem => filesystem;
public override async IAsyncEnumerable<ISourceProviderEntry> Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
// Yield the files in the filesystem
await foreach (var file in filesystem.ListFilesAsync(cancellationToken))
{
yield return new FileSourceEntry(this.Path, filesystem, file);
}
}
/// <summary>
/// Gets the filesystem geometry metadata for this filesystem.
/// </summary>
public async Task<FilesystemGeometry?> GetFilesystemGeometry(CancellationToken cancellationToken)
{
var fsMetadata = await filesystem.GetFilesystemMetadataAsync(cancellationToken);
int blockSize = 1024 * 1024; // Default 1MB blocks
if (fsMetadata is UnkownFilesystemMetadata unknownMeta)
{
blockSize = unknownMeta.BlockSize;
}
return new FilesystemGeometry
{
Type = filesystem.Type,
PartitionNumber = filesystem.Partition.PartitionNumber,
PartitionStartOffset = filesystem.Partition.StartOffset,
BlockSize = blockSize,
Metadata = fsMetadata != null ? JsonSerializer.Serialize(fsMetadata) : null
};
}
public override async Task<Dictionary<string, string?>> GetMinorMetadata(CancellationToken cancellationToken)
{
var metadata = await base.GetMinorMetadata(cancellationToken);
@@ -0,0 +1,94 @@
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Interface;
namespace Duplicati.Proprietary.DiskImage.SourceItems;
/// <summary>
/// Represents a consolidated geometry metadata file as a source entry.
/// This file stores all geometry information for disk, partition table, partitions, and filesystems.
/// </summary>
internal class GeometrySourceEntry : DiskImageEntryBase
{
private readonly GeometryMetadata _metadata;
public GeometrySourceEntry(string parentPath, GeometryMetadata metadata)
: base(PathCombine(parentPath, "geometry.json"))
{
_metadata = metadata;
}
public override bool IsFolder => false;
public override bool IsMetaEntry => true;
public override long Size => Encoding.UTF8.GetByteCount(_metadata.ToJson());
public GeometryMetadata Metadata => _metadata;
public override Task<Stream> OpenRead(CancellationToken cancellationToken)
{
var json = _metadata.ToJson();
var bytes = Encoding.UTF8.GetBytes(json);
return Task.FromResult<Stream>(new MemoryStream(bytes));
}
public override async IAsyncEnumerable<ISourceProviderEntry> Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
// Geometry entries are leaf nodes - no children
await Task.CompletedTask;
yield break;
}
public override async Task<Dictionary<string, string?>> GetMinorMetadata(CancellationToken cancellationToken)
{
var metadata = await base.GetMinorMetadata(cancellationToken);
// Add geometry-specific metadata
metadata["diskimage:Type"] = "geometry";
metadata["geometry:Version"] = _metadata.Version.ToString();
// Add disk info if available
if (_metadata.Disk != null)
{
metadata["disk:DevicePath"] = _metadata.Disk.DevicePath;
metadata["disk:Size"] = _metadata.Disk.Size.ToString();
metadata["disk:SectorSize"] = _metadata.Disk.SectorSize.ToString();
metadata["disk:Sectors"] = _metadata.Disk.Sectors.ToString();
metadata["disk:PartitionTableType"] = _metadata.Disk.TableType.ToString();
}
// Add partition table info if available
if (_metadata.PartitionTable != null)
{
metadata["partition_table:Type"] = _metadata.PartitionTable.Type.ToString();
metadata["partition_table:Size"] = _metadata.PartitionTable.Size.ToString();
}
// Add partition count
if (_metadata.Partitions != null)
{
metadata["partitions:Count"] = _metadata.Partitions.Count.ToString();
}
// Add filesystem count
if (_metadata.Filesystems != null)
{
metadata["filesystems:Count"] = _metadata.Filesystems.Count.ToString();
}
return metadata;
}
private static string PathCombine(string p1, string p2)
{
if (string.IsNullOrEmpty(p1)) return p2;
if (string.IsNullOrEmpty(p2)) return p1;
return p1.TrimEnd('/') + "/" + p2.TrimStart('/');
}
}
@@ -18,8 +18,11 @@ internal class PartitionSourceEntry(string parentPath, IPartition partition)
public override bool IsFolder => true;
public override long Size => partition.Size;
public IPartition Partition => partition;
public override async IAsyncEnumerable<ISourceProviderEntry> Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
// Then yield the filesystem entry
if (partition.FilesystemType == FileSystemType.Unknown)
{
var fs = new UnknownFilesystem(partition);
@@ -357,6 +357,13 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule
// Should only happen if attempting to restore a symlink, which is not supported
=> throw new NotImplementedException("File deletion is not supported in Office365 RestoreProvider");
/// <inheritdoc />
public IList<string> GetPriorityFiles()
{
// Office365 restore doesn't have special priority files
return Array.Empty<string>();
}
/// <inheritdoc />
public async Task Finalize(Action<double>? progressCallback, CancellationToken cancel)
{