Files
duplicati/proprietary/DiskImage/SourceProvider.cs
T
2026-06-03 21:35:14 +02:00

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;
}
}