2026-02-05 11:14:01 +01:00
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2026-02-20 15:29:25 +01:00
using System.IO ;
2026-02-05 11:14:01 +01:00
using System.Runtime.CompilerServices ;
using System.Threading ;
using System.Threading.Tasks ;
using Duplicati.Library.Interface ;
using Duplicati.Proprietary.DiskImage.Disk ;
2026-03-05 15:30:36 +01:00
using Duplicati.Proprietary.DiskImage.General ;
2026-02-06 06:39:22 +01:00
using Duplicati.Proprietary.DiskImage.Partition ;
2026-02-05 11:14:01 +01:00
using Duplicati.Proprietary.DiskImage.SourceItems ;
namespace Duplicati.Proprietary.DiskImage ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Source provider for disk images. Provides access to disk, partition, and filesystem structures
/// as a virtual folder hierarchy for backup operations.
/// </summary>
2026-02-05 11:14:01 +01:00
public sealed class SourceProvider : ISourceProviderModule , IDisposable
{
2026-02-15 12:45:59 +01:00
/// <summary>
/// The path to the disk device.
/// </summary>
2026-02-05 11:14:01 +01:00
private readonly string _devicePath ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// The disk object representing the physical disk.
/// </summary>
2026-02-05 11:14:01 +01:00
private IRawDisk ? _disk ;
2026-02-15 12:45:59 +01:00
/// <summary>
/// Indicates whether the provider has been disposed.
/// </summary>
2026-02-05 11:14:01 +01:00
private bool _disposed ;
2026-02-19 18:16:55 +01:00
/// <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 ;
2026-02-15 12:45:59 +01:00
/// <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>
2026-02-05 11:14:01 +01:00
private readonly ConcurrentDictionary < string , ISourceProviderEntry > _entryCache = new ();
2026-02-15 12:45:59 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="SourceProvider"/> class.
/// Default constructor for metadata loading.
/// </summary>
2026-02-05 11:14:01 +01:00
public SourceProvider ()
{
_devicePath = null !;
}
2026-02-15 12:45:59 +01:00
/// <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>
2026-02-05 11:14:01 +01:00
public SourceProvider ( string url , string mountPoint , Dictionary < string , string? > options )
{
2026-02-19 18:16:55 +01:00
_mountPoint = mountPoint ;
2026-02-06 12:16:23 +01:00
2026-02-06 10:53:44 +01:00
var uri = new Library . Utility . Uri ( url );
_devicePath = uri . HostAndPath ;
2026-02-05 11:14:01 +01:00
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-19 18:16:55 +01:00
public string MountedPath => $"{_mountPoint}root{System.IO.Path.DirectorySeparatorChar}" ;
2026-02-05 11:14:01 +01:00
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public string DisplayName => Strings . ProviderDisplayName ;
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public string Description => Strings . ProviderDescription ;
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public string Key => OptionsHelper . ModuleKey ;
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public IList < ICommandLineArgument > SupportedCommands => OptionsHelper . SupportedCommands ;
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public async Task Initialize ( CancellationToken cancellationToken )
{
if ( string . IsNullOrEmpty ( _devicePath ))
throw new UserInformationException ( "Disk device path is not specified." , "DiskDeviceNotSpecified" );
if ( OperatingSystem . IsWindows ())
{
2026-02-20 15:29:25 +01:00
_disk = new Windows ( _devicePath . TrimEnd ( Path . DirectorySeparatorChar ));
2026-02-05 11:14:01 +01:00
if (! await _disk . InitializeAsync ( cancellationToken ))
throw new UserInformationException ( $"Failed to initialize disk: {_devicePath}" , "DiskInitializeFailed" );
}
2026-02-16 13:41:28 +01:00
else if ( OperatingSystem . IsMacOS ())
{
_disk = new Mac ( _devicePath );
if (! await _disk . InitializeAsync ( cancellationToken ))
throw new UserInformationException ( $"Failed to initialize disk: {_devicePath}" , "DiskInitializeFailed" );
}
2026-03-01 11:12:15 +01:00
else if ( OperatingSystem . IsLinux ())
{
_disk = new Linux ( _devicePath );
if (! await _disk . InitializeAsync ( cancellationToken ))
throw new UserInformationException ( $"Failed to initialize disk: {_devicePath}" , "DiskInitializeFailed" );
}
2026-02-05 11:14:01 +01:00
else
{
2026-02-16 13:41:28 +01:00
throw new PlatformNotSupportedException ( Strings . PlatformNotSupported );
2026-02-05 11:14:01 +01:00
}
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public Task Test ( CancellationToken cancellationToken )
{
if ( _disk == null )
throw new InvalidOperationException ( "Provider not initialized." );
return Task . CompletedTask ;
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public async IAsyncEnumerable < ISourceProviderEntry > Enumerate ([ EnumeratorCancellation ] CancellationToken cancellationToken )
{
if ( _disk == null )
throw new InvalidOperationException ( "Provider not initialized." );
var root = new DiskSourceEntry ( this , _disk );
yield return root ;
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public async Task < ISourceProviderEntry ?> 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 ;
}
2026-02-15 12:45:59 +01:00
/// <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>
2026-02-05 11:14:01 +01:00
private async IAsyncEnumerable < ISourceProviderEntry > EnumerateRecursive ( ISourceProviderEntry parent , [ EnumeratorCancellation ] CancellationToken cancellationToken )
{
yield return parent ;
2026-02-15 16:35:26 +01:00
if ( parent . IsFolder && parent is DiskImageEntryBase dse )
2026-02-05 11:14:01 +01:00
{
2026-02-15 16:35:26 +01:00
await foreach ( var child in dse . Enumerate ( cancellationToken ))
await foreach ( var e in EnumerateRecursive ( child , cancellationToken ))
yield return e ;
2026-02-05 11:14:01 +01:00
}
}
2026-03-05 15:30:36 +01:00
/// <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
throw new PlatformNotSupportedException ( Strings . PlatformNotSupported );
}
2026-03-05 16:08:32 +01:00
/// <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
throw new PlatformNotSupportedException ( Strings . PlatformNotSupported );
}
2026-02-15 12:45:59 +01:00
/// <inheritdoc />
2026-02-05 11:14:01 +01:00
public void Dispose ()
{
if ( _disposed ) return ;
_disk ?. Dispose ();
_disposed = true ;
}
}