235 lines
9.0 KiB
C#
235 lines
9.0 KiB
C#
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Proprietary.DiskImage.Disk;
|
|
using Duplicati.Proprietary.DiskImage.General;
|
|
using Duplicati.Proprietary.DiskImage.Partition;
|
|
using Duplicati.Proprietary.DiskImage.SourceItems;
|
|
|
|
namespace Duplicati.Proprietary.DiskImage;
|
|
|
|
/// <summary>
|
|
/// Source provider for disk images. Provides access to disk, partition, and filesystem structures
|
|
/// as a virtual folder hierarchy for backup operations.
|
|
/// </summary>
|
|
public sealed class SourceProvider : ISourceProviderModule, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The path to the disk device.
|
|
/// </summary>
|
|
private readonly string _devicePath;
|
|
|
|
/// <summary>
|
|
/// The disk object representing the physical disk.
|
|
/// </summary>
|
|
private IRawDisk? _disk;
|
|
|
|
/// <summary>
|
|
/// Indicates whether the provider has been disposed.
|
|
/// </summary>
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// The mount point for the provider. For disk images, this is typically not used since the entire disk is treated as root.
|
|
/// </summary>
|
|
private string _mountPoint = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Cache for source provider entries to optimize repeated access. Keyed by entry path.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This cache is populated on demand when entries are accessed via GetEntry to avoid having to re-enumerate the disk structure.
|
|
/// </remarks>
|
|
private readonly ConcurrentDictionary<string, ISourceProviderEntry> _entryCache = new();
|
|
|
|
/// <summary>
|
|
/// Indicates whether to treat filesystems as unknown (force raw block-based backup).
|
|
/// </summary>
|
|
private readonly bool _treatFilesystemAsUnknown;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SourceProvider"/> class.
|
|
/// Default constructor for metadata loading.
|
|
/// </summary>
|
|
public SourceProvider()
|
|
{
|
|
_devicePath = null!;
|
|
_treatFilesystemAsUnknown = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="SourceProvider"/> class with the specified URL and options.
|
|
/// </summary>
|
|
/// <param name="url">The device URL (e.g., "diskimage://\\.\PhysicalDrive0").</param>
|
|
/// <param name="mountPoint">The mount point (not supported for disk images).</param>
|
|
/// <param name="options">Provider options.</param>
|
|
/// <exception cref="UserInformationException">Thrown when mount point is specified.</exception>
|
|
public SourceProvider(string url, string mountPoint, Dictionary<string, string?> options)
|
|
{
|
|
_mountPoint = mountPoint;
|
|
|
|
var uri = new Library.Utility.Uri(url);
|
|
_devicePath = uri.HostAndPath;
|
|
|
|
_treatFilesystemAsUnknown = !Library.Utility.Utility.ParseBoolOption(options, OptionsHelper.DISK_IMAGE_FILESYSTEM_PARSED_OPTION);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string MountedPath => $"{_mountPoint}root{System.IO.Path.DirectorySeparatorChar}";
|
|
|
|
/// <inheritdoc />
|
|
public string DisplayName => Strings.ProviderDisplayName;
|
|
|
|
/// <inheritdoc />
|
|
public string Description => Strings.ProviderDescription;
|
|
|
|
/// <inheritdoc />
|
|
public string Key => OptionsHelper.ModuleKey;
|
|
|
|
/// <inheritdoc />
|
|
public IList<ICommandLineArgument> SupportedCommands => OptionsHelper.SupportedCommands;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether to treat filesystems as unknown (force raw block-based backup).
|
|
/// </summary>
|
|
internal bool TreatFilesystemAsUnknown => _treatFilesystemAsUnknown;
|
|
|
|
/// <inheritdoc />
|
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrEmpty(_devicePath))
|
|
throw new UserInformationException("Disk device path is not specified.", "DiskDeviceNotSpecified");
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
_disk = new Windows(_devicePath.TrimEnd(Path.DirectorySeparatorChar));
|
|
if (!await _disk.InitializeAsync(cancellationToken))
|
|
throw new UserInformationException($"Failed to initialize disk: {_devicePath}", "DiskInitializeFailed");
|
|
}
|
|
else if (OperatingSystem.IsMacOS())
|
|
{
|
|
_disk = new Mac(_devicePath);
|
|
if (!await _disk.InitializeAsync(cancellationToken))
|
|
throw new UserInformationException($"Failed to initialize disk: {_devicePath}", "DiskInitializeFailed");
|
|
}
|
|
else if (OperatingSystem.IsLinux())
|
|
{
|
|
_disk = new Linux(_devicePath);
|
|
if (!await _disk.InitializeAsync(cancellationToken))
|
|
throw new UserInformationException($"Failed to initialize disk: {_devicePath}", "DiskInitializeFailed");
|
|
}
|
|
else
|
|
{
|
|
throw new PlatformNotSupportedException(Strings.PlatformNotSupported);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task TestAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (_disk == null)
|
|
throw new InvalidOperationException("Provider not initialized.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async IAsyncEnumerable<ISourceProviderEntry> EnumerateAsync([EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
if (_disk == null)
|
|
throw new InvalidOperationException("Provider not initialized.");
|
|
|
|
var root = new DiskSourceEntry(this, _disk);
|
|
yield return root;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<ISourceProviderEntry?> GetEntryAsync(string path, bool isFolder, CancellationToken cancellationToken)
|
|
{
|
|
if (_disk == null)
|
|
throw new InvalidOperationException("Provider not initialized.");
|
|
|
|
if (_entryCache.TryGetValue(path, out var cachedEntry))
|
|
return cachedEntry;
|
|
|
|
// Simple implementation: enumerate from root to find the entry
|
|
// In a real implementation, we would parse the path and resolve it efficiently
|
|
await foreach (var entry in EnumerateRecursive(new DiskSourceEntry(this, _disk), cancellationToken))
|
|
{
|
|
if (entry.Path == path && entry.IsFolder == isFolder)
|
|
{
|
|
_entryCache[path] = entry;
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively enumerates entries starting from the specified parent entry.
|
|
/// </summary>
|
|
/// <param name="parent">The parent entry to enumerate from.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>An async enumerable of all entries in the hierarchy.</returns>
|
|
private async IAsyncEnumerable<ISourceProviderEntry> EnumerateRecursive(ISourceProviderEntry parent, [EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
yield return parent;
|
|
if (parent.IsFolder && parent is DiskImageEntryBase dse)
|
|
{
|
|
await foreach (var child in dse.Enumerate(cancellationToken))
|
|
await foreach (var e in EnumerateRecursive(child, cancellationToken))
|
|
yield return e;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lists physical drives available on the system. This is a static method that can be used to discover available disks before initializing the provider.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>A list of physical drive information.</returns>
|
|
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
|
public static IAsyncEnumerable<PhysicalDriveInfo> ListPhysicalDrives(CancellationToken cancellationToken)
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
return Windows.ListPhysicalDrivesAsync(cancellationToken);
|
|
else if (OperatingSystem.IsMacOS())
|
|
return Mac.ListPhysicalDrivesAsync(cancellationToken);
|
|
else if (OperatingSystem.IsLinux())
|
|
return Linux.ListPhysicalDrivesAsync(cancellationToken);
|
|
else
|
|
throw new PlatformNotSupportedException(Strings.PlatformNotSupported);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the platform-specific prefix for disk entries (e.g., "\\.\" on Windows, "/dev/" on Unix). This is used to construct entry paths correctly based on the underlying platform.
|
|
/// </summary>
|
|
/// <returns>The platform-specific prefix for disk entries.</returns>
|
|
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
|
public static string GetDevicePrefix()
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
return Windows.Prefix;
|
|
else if (OperatingSystem.IsMacOS())
|
|
return Mac.Prefix;
|
|
else if (OperatingSystem.IsLinux())
|
|
return Linux.Prefix;
|
|
else
|
|
throw new PlatformNotSupportedException(Strings.PlatformNotSupported);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disk?.Dispose();
|
|
_disposed = true;
|
|
}
|
|
}
|