244 lines
8.9 KiB
C#
244 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Pipelines;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Common.IO;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Logging;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.Proprietary.DiskImage;
|
|
|
|
/// <summary>
|
|
/// Represents a physical drive returned by PowerShell Get-Disk.
|
|
/// </summary>
|
|
public class PhysicalDriveInfo
|
|
{
|
|
/// <summary>
|
|
/// The device path (e.g., \\.\PHYSICALDRIVE0).
|
|
/// </summary>
|
|
public string Path { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// The size of the drive in bytes.
|
|
/// </summary>
|
|
public ulong Size { get; set; }
|
|
|
|
/// <summary>
|
|
/// The display name of the drive.
|
|
/// </summary>
|
|
public string DisplayName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// The GUID of the drive.
|
|
/// </summary>
|
|
public string? Guid { get; set; }
|
|
|
|
/// <summary>
|
|
/// The drive letters associated with this drive.
|
|
/// </summary>
|
|
public string[] DriveLetters { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Whether the drive is online.
|
|
/// </summary>
|
|
public bool? Online { get; set; }
|
|
}
|
|
|
|
public class WebModule : IWebModule
|
|
{
|
|
private static readonly string LOGTAG = Log.LogTagFromType<WebModule>();
|
|
|
|
public string Key => OptionsHelper.ModuleKey;
|
|
|
|
public string DisplayName => Strings.WebModuleDisplayName;
|
|
|
|
public string Description => Strings.WebModuleDescription;
|
|
|
|
public enum Operation
|
|
{
|
|
ListDestination
|
|
}
|
|
|
|
private static readonly Operation DEFAULT_OPERATION = Operation.ListDestination;
|
|
private const string KEY_OPERATION = "operation";
|
|
private const string KEY_URL = "url";
|
|
private const string KEY_PATH = "path";
|
|
|
|
public IList<ICommandLineArgument> SupportedCommands => [
|
|
new CommandLineArgument(KEY_OPERATION, CommandLineArgument.ArgumentType.Enumeration, Strings.WebModuleOperationShort, Strings.WebModuleOperationLong, DEFAULT_OPERATION.ToString(), null, Enum.GetNames<Operation>()),
|
|
new CommandLineArgument(KEY_URL, CommandLineArgument.ArgumentType.String, Strings.WebModuleURLShort, Strings.WebModuleURLLong),
|
|
new CommandLineArgument(KEY_PATH, CommandLineArgument.ArgumentType.String, Strings.WebModulePathShort, Strings.WebModulePathLong)
|
|
];
|
|
|
|
public async Task<IDictionary<string, string>> Execute(IDictionary<string, string?> options, CancellationToken cancellationToken)
|
|
{
|
|
var op = Utility.ParseEnumOption(options.AsReadOnly(), KEY_OPERATION, DEFAULT_OPERATION);
|
|
options.TryGetValue(KEY_PATH, out var path);
|
|
|
|
if (op != Operation.ListDestination)
|
|
throw new UserInformationException($"Unsupported operation: {op}", "UnsupportedOperation");
|
|
|
|
if (string.IsNullOrWhiteSpace(path) || path == "/")
|
|
return await ListPhysicalDrives(cancellationToken);
|
|
|
|
var parts = path[4..].Split(Path.DirectorySeparatorChar, 2);
|
|
var physicalDrivePath = "\\\\.\\" + parts.First();
|
|
var subpath = parts.Last();
|
|
|
|
using var client = new SourceProvider("diskimage://" + physicalDrivePath, "", new Dictionary<string, string?>(options));
|
|
await client.Initialize(cancellationToken);
|
|
|
|
if (string.IsNullOrWhiteSpace(subpath))
|
|
return new Dictionary<string, string>()
|
|
{
|
|
{$"{physicalDrivePath}\\root\\", "{}"}
|
|
};
|
|
|
|
var targetEntry = await client.GetEntry(subpath, isFolder: true, cancellationToken).ConfigureAwait(false);
|
|
if (targetEntry == null)
|
|
throw new DirectoryNotFoundException($"Path not found: {path}");
|
|
|
|
var result = new Dictionary<string, string>();
|
|
await foreach (var entry in targetEntry.Enumerate(cancellationToken))
|
|
{
|
|
if (entry.Path.EndsWith("geometry.json", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
var metadata = await entry.GetMinorMetadata(cancellationToken);
|
|
if (metadata.ContainsKey("partition:Number"))
|
|
result[$"{physicalDrivePath}\\{entry.Path.TrimEnd(Path.DirectorySeparatorChar)}"] = JsonSerializer.Serialize(metadata);
|
|
else
|
|
result[$"{physicalDrivePath}\\{entry.Path}"] = JsonSerializer.Serialize(metadata);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<IDictionary<string, string>> ListPhysicalDrives(CancellationToken cancellationToken)
|
|
{
|
|
if (!OperatingSystem.IsWindows())
|
|
throw new PlatformNotSupportedException(Strings.RestorePlatformNotSupported);
|
|
|
|
var script = @"
|
|
$diskInfo =
|
|
Get-CimInstance Win32_DiskDrive |
|
|
ForEach-Object {
|
|
$wmi = $_
|
|
$diskNumber = [int]$wmi.Index
|
|
$disk = Get-Disk -Number $diskNumber -ErrorAction SilentlyContinue
|
|
|
|
$driveLetters = @(
|
|
Get-Partition -DiskNumber $diskNumber -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.DriveLetter } |
|
|
ForEach-Object { $_.DriveLetter.ToString() } |
|
|
Sort-Object -Unique
|
|
)
|
|
|
|
[pscustomobject]@{
|
|
Path = $wmi.DeviceID
|
|
Size = [uint64]$wmi.Size
|
|
DisplayName = if ($disk) { $disk.FriendlyName } else { $wmi.Model }
|
|
Guid = if ($disk) { $disk.Guid } else { $null }
|
|
DriveLetters = $driveLetters # Always an array
|
|
Online = if ($disk) { -not $disk.IsOffline } else { $null }
|
|
}
|
|
}
|
|
|
|
$diskInfo | ConvertTo-Json -Depth 4
|
|
";
|
|
var output = await RunPowerShellAsync(script, cancellationToken);
|
|
|
|
var result = new Dictionary<string, string>();
|
|
if (string.IsNullOrWhiteSpace(output))
|
|
return result;
|
|
|
|
try
|
|
{
|
|
// PowerShell ConvertTo-Json returns a single object for 1 item, array for multiple
|
|
using var doc = JsonDocument.Parse(output);
|
|
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var drives = JsonSerializer.Deserialize<PhysicalDriveInfo[]>(output);
|
|
if (drives != null)
|
|
{
|
|
foreach (var drive in drives)
|
|
{
|
|
AddDiskToResult(drive, result);
|
|
}
|
|
}
|
|
}
|
|
else if (doc.RootElement.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var drive = JsonSerializer.Deserialize<PhysicalDriveInfo>(output);
|
|
if (drive != null)
|
|
{
|
|
AddDiskToResult(drive, result);
|
|
}
|
|
}
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "FailedDriveOutputParsing", ex, "Failed to parse output");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void AddDiskToResult(PhysicalDriveInfo drive, Dictionary<string, string> result)
|
|
{
|
|
// Extract drive number from path (e.g., \\.\PHYSICALDRIVE0 -> 0)
|
|
var number = 0;
|
|
if (drive.Path.StartsWith("\\\\.\\PHYSICALDRIVE", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
int.TryParse(drive.Path.AsSpan("\\\\.\\PHYSICALDRIVE".Length), out number);
|
|
}
|
|
|
|
// For now, we only allow picking full disks, not individual partitions
|
|
// var resultpath = Util.AppendDirSeparator(drive.Path);
|
|
var resultpath = drive.Path.TrimEnd(Path.DirectorySeparatorChar);
|
|
var metadata = new Dictionary<string, string?>
|
|
{
|
|
{ "diskimage:Number", number.ToString() },
|
|
{ "diskimage:FriendlyName", drive.DisplayName },
|
|
{ "diskimage:Size", drive.Size.ToString() },
|
|
{ "diskimage:DevicePath", drive.Path },
|
|
{ "diskimage:Name", $"{drive.DisplayName} ({Library.Utility.Utility.FormatSizeString(drive.Size)})" },
|
|
};
|
|
|
|
result[resultpath] = JsonSerializer.Serialize(metadata);
|
|
}
|
|
|
|
private async Task<string> RunPowerShellAsync(string script, CancellationToken cancellationToken)
|
|
{
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "powershell.exe",
|
|
Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{script.Replace("\"", "\\\"")}\"",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = new Process { StartInfo = startInfo };
|
|
var output = new StringBuilder();
|
|
process.OutputDataReceived += (s, e) => { if (e.Data != null) output.AppendLine(e.Data); };
|
|
|
|
process.Start();
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
|
|
await process.WaitForExitAsync(cancellationToken);
|
|
|
|
return output.ToString();
|
|
}
|
|
|
|
public IDictionary<string, IDictionary<string, string>> GetLookups()
|
|
=> new Dictionary<string, IDictionary<string, string>>();
|
|
} |