From 3fe57b308d00a6d05a76edff06955504704d7a1c Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Mon, 9 Feb 2026 11:36:15 +0100 Subject: [PATCH] Added functionality for priority files for restore providers --- .../Interface/IRestoreDestinationProvider.cs | 9 +- .../Main/Operation/Restore/FileLister.cs | 42 +++- .../Library/Main/Operation/RestoreHandler.cs | 2 +- .../Builtin/FileRestoreDestinationProvider.cs | 7 + proprietary/DiskImage/GeometryMetadata.cs | 210 ++++++++++++++++++ proprietary/DiskImage/RestoreProvider.cs | 137 ++++++++++++ .../DiskImage/SourceItems/DiskSourceEntry.cs | 90 +++++++- .../SourceItems/FilesystemSourceEntry.cs | 26 +++ .../SourceItems/GeometrySourceEntry.cs | 94 ++++++++ .../SourceItems/PartitionSourceEntry.cs | 3 + .../RestoreProvider/RestoreProvider.cs | 7 + 11 files changed, 622 insertions(+), 5 deletions(-) create mode 100644 proprietary/DiskImage/GeometryMetadata.cs create mode 100644 proprietary/DiskImage/SourceItems/GeometrySourceEntry.cs diff --git a/Duplicati/Library/Interface/IRestoreDestinationProvider.cs b/Duplicati/Library/Interface/IRestoreDestinationProvider.cs index 711d546f5..3336b8993 100644 --- a/Duplicati/Library/Interface/IRestoreDestinationProvider.cs +++ b/Duplicati/Library/Interface/IRestoreDestinationProvider.cs @@ -148,5 +148,12 @@ public interface IRestoreDestinationProvider : IDisposable /// A task that completes when the deletion is done Task DeleteFile(string path, CancellationToken cancel); - + /// + /// 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. + /// + /// A list of file paths that should be restored first, in order of priority. + IList GetPriorityFiles(); } diff --git a/Duplicati/Library/Main/Operation/Restore/FileLister.cs b/Duplicati/Library/Main/Operation/Restore/FileLister.cs index dc7db7740..d8fc2bf45 100644 --- a/Duplicati/Library/Main/Operation/Restore/FileLister.cs +++ b/Duplicati/Library/Main/Operation/Restore/FileLister.cs @@ -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 /// The restore options /// The restore results public static Task Run(Channels channels, LocalRestoreDatabase db, Options options, RestoreResults result) + => Run(channels, db, options, result, null); + + /// + /// Runs the file lister process that lists the files that need to be restored + /// and sends them to the . + /// + /// The named channels for the restore operation. + /// The restore database, which is queried for the file list. + /// The restore options + /// The restore results + /// The restore destination provider, used to get priority files. + 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(); + 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(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) diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs index 23d50dabc..9d0430a0b 100644 --- a/Duplicati/Library/Main/Operation/RestoreHandler.cs +++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs @@ -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); diff --git a/Duplicati/Library/SourceProvider/Builtin/FileRestoreDestinationProvider.cs b/Duplicati/Library/SourceProvider/Builtin/FileRestoreDestinationProvider.cs index a6dbba7d2..01faf408a 100644 --- a/Duplicati/Library/SourceProvider/Builtin/FileRestoreDestinationProvider.cs +++ b/Duplicati/Library/SourceProvider/Builtin/FileRestoreDestinationProvider.cs @@ -166,4 +166,11 @@ public class FileRestoreDestinationProvider(string mountedPath) : IRestoreDestin SystemIO.IO_OS.SetMetadata(path, metadata, restorePermissions); return Task.FromResult(wrote_something); } + + /// + public IList GetPriorityFiles() + { + // File-based restore doesn't have special priority files + return Array.Empty(); + } } diff --git a/proprietary/DiskImage/GeometryMetadata.cs b/proprietary/DiskImage/GeometryMetadata.cs new file mode 100644 index 000000000..c82232a01 --- /dev/null +++ b/proprietary/DiskImage/GeometryMetadata.cs @@ -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; + +/// +/// 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. +/// +public class GeometryMetadata +{ + /// + /// The version of the metadata format. + /// + public int Version { get; set; } = 1; + + /// + /// Disk-level geometry information. + /// + public DiskGeometry? Disk { get; set; } + + /// + /// Partition table-level geometry information. + /// + public PartitionTableGeometry? PartitionTable { get; set; } + + /// + /// Array of partition geometries. + /// + public List? Partitions { get; set; } + + /// + /// Array of filesystem geometries, keyed by partition number. + /// + public List? Filesystems { get; set; } + + /// + /// Serializes this metadata to a JSON string. + /// + public string ToJson() + { + return JsonSerializer.Serialize(this, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true + }); + } + + /// + /// Deserializes a JSON string to a GeometryMetadata instance. + /// + public static GeometryMetadata? FromJson(string json) + { + return JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } +} + +/// +/// Disk-level geometry information. +/// +public class DiskGeometry +{ + /// + /// The device path of the source disk. + /// + public string? DevicePath { get; set; } + + /// + /// The total size of the disk in bytes. + /// + public long Size { get; set; } + + /// + /// The sector size in bytes. + /// + public int SectorSize { get; set; } + + /// + /// The number of sectors on the disk. + /// + public int Sectors { get; set; } + + /// + /// The type of partition table on the disk. + /// + public PartitionTableType TableType { get; set; } +} + +/// +/// Partition table-level geometry information. +/// +public class PartitionTableGeometry +{ + /// + /// The type of partition table (MBR, GPT, etc.). + /// + public PartitionTableType Type { get; set; } + + /// + /// The size of the partition table data in bytes. + /// + public long Size { get; set; } + + /// + /// The sector size of the disk. + /// + public int SectorSize { get; set; } + + /// + /// For GPT: whether the disk has a protective MBR. + /// + public bool HasProtectiveMbr { get; set; } + + /// + /// For GPT: the size of the GPT header in bytes. + /// + public int HeaderSize { get; set; } + + /// + /// For MBR: the size of the MBR in bytes (typically 512). + /// + public int MbrSize { get; set; } +} + +/// +/// Partition-level geometry information. +/// +public class PartitionGeometry +{ + /// + /// The partition number (1-based). + /// + public int Number { get; set; } + + /// + /// The partition type. + /// + public PartitionType Type { get; set; } + + /// + /// The starting offset of the partition in bytes. + /// + public long StartOffset { get; set; } + + /// + /// The size of the partition in bytes. + /// + public long Size { get; set; } + + /// + /// The partition name (if available). + /// + public string? Name { get; set; } + + /// + /// The filesystem type detected for this partition. + /// + public FileSystemType FilesystemType { get; set; } + + /// + /// The volume GUID for GPT partitions. + /// + public Guid? VolumeGuid { get; set; } + + /// + /// The partition table type this partition belongs to. + /// + public PartitionTableType TableType { get; set; } +} + +/// +/// Filesystem-level geometry information. +/// +public class FilesystemGeometry +{ + /// + /// The partition number this filesystem resides on. + /// + public int PartitionNumber { get; set; } + + /// + /// The type of filesystem. + /// + public FileSystemType Type { get; set; } + + /// + /// The starting offset of the partition in bytes. + /// + public long PartitionStartOffset { get; set; } + + /// + /// The block size used by the filesystem. + /// + public int BlockSize { get; set; } + + /// + /// Filesystem-specific metadata as a JSON string. + /// + public string? Metadata { get; set; } +} diff --git a/proprietary/DiskImage/RestoreProvider.cs b/proprietary/DiskImage/RestoreProvider.cs index 0f1f71557..5f1a39ea1 100644 --- a/proprietary/DiskImage/RestoreProvider.cs +++ b/proprietary/DiskImage/RestoreProvider.cs @@ -54,6 +54,12 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos /// private readonly ConcurrentDictionary _filesystemCache = new(); + /// + /// Stores geometry metadata parsed from restored geometry files. + /// Used to reconstruct disk, partition, and filesystem structures. + /// + private readonly Dictionary _geometryMetadata = new(); + /// /// Represents a pending write operation. /// @@ -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(wrapper); } + /// + /// Opens a stream for writing geometry metadata (stored in memory until Finalize). + /// + private Task OpenWriteGeometry(string path, Dictionary 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(wrapper); + } + /// /// 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 /// /// Gets or creates a partition instance for the specified partition number. + /// Uses geometry metadata if available to reconstruct the partition structure. /// private Task 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 /// /// Gets or creates a filesystem instance for the specified partition. + /// Uses geometry metadata if available to reconstruct the filesystem structure. /// private Task 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 })); } + /// + /// Finds partition geometry metadata for the specified partition number. + /// Searches through all consolidated geometry metadata entries. + /// + 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; + } + + /// + /// Finds filesystem geometry metadata for the specified partition number. + /// Searches through all consolidated geometry metadata entries. + /// + 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; + } + /// /// A simple partition table implementation for the target disk during restore. /// @@ -952,6 +1080,15 @@ public sealed class RestoreProvider : IRestoreDestinationProviderModule, IDispos public Task DeleteFile(string path, CancellationToken cancel) => Task.CompletedTask; + /// + public IList 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 { "geometry.json" }; + } + /// public async Task Finalize(Action? progressCallback, CancellationToken cancel) { diff --git a/proprietary/DiskImage/SourceItems/DiskSourceEntry.cs b/proprietary/DiskImage/SourceItems/DiskSourceEntry.cs index 321a9c726..607db2c2c 100644 --- a/proprietary/DiskImage/SourceItems/DiskSourceEntry.cs +++ b/proprietary/DiskImage/SourceItems/DiskSourceEntry.cs @@ -21,14 +21,102 @@ internal class DiskSourceEntry(SourceProvider provider, IRawDisk disk) public override async IAsyncEnumerable 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(), + Filesystems = new List() + }; + 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> GetMinorMetadata(CancellationToken cancellationToken) diff --git a/proprietary/DiskImage/SourceItems/FilesystemSourceEntry.cs b/proprietary/DiskImage/SourceItems/FilesystemSourceEntry.cs index 935127471..cbbae5814 100644 --- a/proprietary/DiskImage/SourceItems/FilesystemSourceEntry.cs +++ b/proprietary/DiskImage/SourceItems/FilesystemSourceEntry.cs @@ -17,14 +17,40 @@ internal class FilesystemSourceEntry(string parentPath, IFilesystem filesystem) { public override bool IsFolder => true; + public IFilesystem Filesystem => filesystem; + public override async IAsyncEnumerable 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); } } + /// + /// Gets the filesystem geometry metadata for this filesystem. + /// + public async Task 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> GetMinorMetadata(CancellationToken cancellationToken) { var metadata = await base.GetMinorMetadata(cancellationToken); diff --git a/proprietary/DiskImage/SourceItems/GeometrySourceEntry.cs b/proprietary/DiskImage/SourceItems/GeometrySourceEntry.cs new file mode 100644 index 000000000..97b516907 --- /dev/null +++ b/proprietary/DiskImage/SourceItems/GeometrySourceEntry.cs @@ -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; + +/// +/// Represents a consolidated geometry metadata file as a source entry. +/// This file stores all geometry information for disk, partition table, partitions, and filesystems. +/// +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 OpenRead(CancellationToken cancellationToken) + { + var json = _metadata.ToJson(); + var bytes = Encoding.UTF8.GetBytes(json); + return Task.FromResult(new MemoryStream(bytes)); + } + + public override async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) + { + // Geometry entries are leaf nodes - no children + await Task.CompletedTask; + yield break; + } + + public override async Task> 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('/'); + } +} diff --git a/proprietary/DiskImage/SourceItems/PartitionSourceEntry.cs b/proprietary/DiskImage/SourceItems/PartitionSourceEntry.cs index d6c9a2fb1..92d79c61b 100644 --- a/proprietary/DiskImage/SourceItems/PartitionSourceEntry.cs +++ b/proprietary/DiskImage/SourceItems/PartitionSourceEntry.cs @@ -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 Enumerate([EnumeratorCancellation] CancellationToken cancellationToken) { + // Then yield the filesystem entry if (partition.FilesystemType == FileSystemType.Unknown) { var fs = new UnknownFilesystem(partition); diff --git a/proprietary/Office365/RestoreProvider/RestoreProvider.cs b/proprietary/Office365/RestoreProvider/RestoreProvider.cs index b6e2842ce..4daf9e469 100644 --- a/proprietary/Office365/RestoreProvider/RestoreProvider.cs +++ b/proprietary/Office365/RestoreProvider/RestoreProvider.cs @@ -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"); + /// + public IList GetPriorityFiles() + { + // Office365 restore doesn't have special priority files + return Array.Empty(); + } + /// public async Task Finalize(Action? progressCallback, CancellationToken cancel) {