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)
{