Added functionality for priority files for restore providers
This commit is contained in:
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user