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