// 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;
///
/// Source provider for disk images. Provides access to disk, partition, and filesystem structures
/// as a virtual folder hierarchy for backup operations.
///
public sealed class SourceProvider : ISourceProviderModule, IDisposable
{
///
/// The path to the disk device.
///
private readonly string _devicePath;
///
/// The disk object representing the physical disk.
///
private IRawDisk? _disk;
///
/// Indicates whether the provider has been disposed.
///
private bool _disposed;
///
/// The mount point for the provider. For disk images, this is typically not used since the entire disk is treated as root.
///
private string _mountPoint = string.Empty;
///
/// Cache for source provider entries to optimize repeated access. Keyed by entry path.
///
///
/// This cache is populated on demand when entries are accessed via GetEntry to avoid having to re-enumerate the disk structure.
///
private readonly ConcurrentDictionary _entryCache = new();
///
/// Initializes a new instance of the class.
/// Default constructor for metadata loading.
///
public SourceProvider()
{
_devicePath = null!;
}
///
/// Initializes a new instance of the class with the specified URL and options.
///
/// The device URL (e.g., "diskimage://\\.\PhysicalDrive0").
/// The mount point (not supported for disk images).
/// Provider options.
/// Thrown when mount point is specified.
public SourceProvider(string url, string mountPoint, Dictionary options)
{
_mountPoint = mountPoint;
var uri = new Library.Utility.Uri(url);
_devicePath = uri.HostAndPath;
}
///
public string MountedPath => $"{_mountPoint}root{System.IO.Path.DirectorySeparatorChar}";
///
public string DisplayName => Strings.ProviderDisplayName;
///
public string Description => Strings.ProviderDescription;
///
public string Key => OptionsHelper.ModuleKey;
///
public IList SupportedCommands => OptionsHelper.SupportedCommands;
///
public async Task Initialize(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);
}
}
///
public Task Test(CancellationToken cancellationToken)
{
if (_disk == null)
throw new InvalidOperationException("Provider not initialized.");
return Task.CompletedTask;
}
///
public async IAsyncEnumerable Enumerate([EnumeratorCancellation] CancellationToken cancellationToken)
{
if (_disk == null)
throw new InvalidOperationException("Provider not initialized.");
var root = new DiskSourceEntry(this, _disk);
yield return root;
}
///
public async Task GetEntry(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;
}
///
/// Recursively enumerates entries starting from the specified parent entry.
///
/// The parent entry to enumerate from.
/// Cancellation token.
/// An async enumerable of all entries in the hierarchy.
private async IAsyncEnumerable 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;
}
}
///
/// Lists physical drives available on the system. This is a static method that can be used to discover available disks before initializing the provider.
///
/// Cancellation token.
/// A list of physical drive information.
/// Thrown when the platform is not supported.
public static IAsyncEnumerable ListPhysicalDrives(CancellationToken cancellationToken)
{
if (OperatingSystem.IsWindows())
return Windows.ListPhysicalDrivesAsync(cancellationToken);
else if (OperatingSystem.IsMacOS())
return Mac.ListPhysicalDrivesAsync(cancellationToken);
else
throw new PlatformNotSupportedException(Strings.PlatformNotSupported);
}
///
/// 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.
///
/// The platform-specific prefix for disk entries.
/// Thrown when the platform is not supported.
public static string GetDevicePrefix()
{
if (OperatingSystem.IsWindows())
return Windows.Prefix;
else if (OperatingSystem.IsMacOS())
return Mac.Prefix;
else
throw new PlatformNotSupportedException(Strings.PlatformNotSupported);
}
///
public void Dispose()
{
if (_disposed) return;
_disk?.Dispose();
_disposed = true;
}
}