Files
duplicati/proprietary/DiskImage/SourceProvider.cs
T
Kenneth Skovhede b67a73553a Added unified interface for remote browsing.
This PR adds a single API endpoint for listing contents of remote source or backend. With this API, we can greatly simplify the FE as it does not need to know about webmodules for each provider, but can just use the single API.

To support this, the fulldisk module was updated slightly to return enumerable meta entries, similar to ms365 and gws providers.
2026-06-12 11:06:21 +02:00

244 lines
9.4 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.Linq;
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)
{
// TODO: Should we redesign this?
// To support enumerating physical drives, we accept init with no disk
if (string.IsNullOrEmpty(_devicePath))
return;
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 (string.IsNullOrWhiteSpace(path) || path == "/")
return new MachineRootSourceEntry();
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<PhysicalDriveSourceEntry> ListPhysicalDrives(CancellationToken cancellationToken)
{
if (OperatingSystem.IsWindows())
return Windows.ListPhysicalDrivesAsync(cancellationToken)
.Select(x => new PhysicalDriveSourceEntry(x));
else if (OperatingSystem.IsMacOS())
return Mac.ListPhysicalDrivesAsync(cancellationToken)
.Select(x => new PhysicalDriveSourceEntry(x));
else if (OperatingSystem.IsLinux())
return Linux.ListPhysicalDrivesAsync(cancellationToken)
.Select(x => new PhysicalDriveSourceEntry(x));
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;
}
}