Public/Import-PVEStorageContent.ps1

function Import-PVEStorageContent {
  <#
  .SYNOPSIS
  Puts an ISO image, container template or import file onto a PVE storage, from a URL or a local file.
  .DESCRIPTION
  -Url makes the node download the file itself (POST /nodes/{node}/storage/{storage}/download-url), which
  is usually fastest. -Path uploads a local file (POST .../upload, multipart). Either way the cmdlet waits
  for PVE's task, then writes the stored PVEPS.StorageContent object.

  -Checksum with -ChecksumAlgorithm makes PVE verify the file and fail the task on a mismatch. The API
  documentation warns that PVE normalises the file name; if the stored name differs, a warning says so.
  .PARAMETER Url
  URL for the node to download from.
  .PARAMETER Path
  Local file to upload.
  .PARAMETER Storage
  Target storage. It must allow the content type.
  .PARAMETER Content
  iso, vztmpl (container template) or import.
  .PARAMETER FileName
  Name to store the file under. Defaults to the last part of the URL, or the local file's name.
  .PARAMETER Checksum
  Expected checksum of the file.
  .PARAMETER ChecksumAlgorithm
  md5, sha1, sha224, sha256, sha384 or sha512.
  .PARAMETER Compression
  With -Url: decompress the download with this algorithm (for example zst or gz).
  .PARAMETER SkipCertificateCheck
  With -Url: the node does not verify the download server's TLS certificate.
  .PARAMETER Node
  Node to store the file on. Can be omitted on a single-node server.
  .PARAMETER RunAsync
  Return the task at once instead of waiting.
  .PARAMETER Server
  Connection to use. Defaults to the default connection.
  .EXAMPLE
  Import-PVEStorageContent -Url https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.1.0-amd64-netinst.iso -Storage local -Content iso
  .EXAMPLE
  Import-PVEStorageContent -Path ./virtio-win.iso -Storage local -Content iso -Checksum $sha256 -ChecksumAlgorithm sha256
  #>

  [CmdletBinding(DefaultParameterSetName = 'Url', SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [OutputType('PVEPS.StorageContent', 'PVEPS.Task')]
  param(
    [Parameter(Mandatory, ParameterSetName = 'Url', Position = 0)]
    [ValidatePattern('^(https?|ftp)://')]
    [string]$Url,

    [Parameter(Mandatory, ParameterSetName = 'Path')]
    [ValidateNotNullOrEmpty()]
    [string]$Path,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Storage,

    [Parameter(Mandatory)]
    [ValidateSet('iso', 'vztmpl', 'import')]
    [string]$Content,

    [ValidateLength(1, 255)]
    [string]$FileName,

    [ValidateNotNullOrEmpty()]
    [string]$Checksum,

    [ValidateSet('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512')]
    [string]$ChecksumAlgorithm,

    [Parameter(ParameterSetName = 'Url')]
    [ValidateNotNullOrEmpty()]
    [string]$Compression,

    [Parameter(ParameterSetName = 'Url')]
    [switch]$SkipCertificateCheck,

    [ValidateNotNullOrEmpty()]
    [string]$Node,

    [switch]$RunAsync,

    [Alias('Connection')]
    [object]$Server
  )

  if ([bool]$Checksum -ne [bool]$ChecksumAlgorithm) {
    $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message '-Checksum and -ChecksumAlgorithm go together.' -Target $Checksum))
  }
  if ($Path) {
    $localPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)
    if (-not (Test-Path -LiteralPath $localPath -PathType Leaf)) {
      $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
          [System.IO.FileNotFoundException]::new("Local file '$Path' was not found.", $localPath),
          'PVELocalFileNotFound',
          [System.Management.Automation.ErrorCategory]::ObjectNotFound,
          $Path))
    }
  }
  $name = if ($FileName) {
    $FileName
  }
  elseif ($Path) {
    Split-Path -Path $localPath -Leaf
  }
  else {
    [uri]::UnescapeDataString(([uri]$Url).Segments[-1])
  }
  if (-not $name -or $name -eq '/') {
    $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message "No file name in '$Url'; give -FileName." -Target $Url))
  }

  try {
    $connection = Resolve-PVESingleConnection -Server $Server -Reason 'a file is stored on exactly one'
    $nodeName = Resolve-PVENodeName -Connection $connection -Node $Node
  }
  catch {
    $PSCmdlet.WriteError($_)
    return
  }

  $source = if ($Path) { "local file $localPath" } else { $Url }
  if (-not $PSCmdlet.ShouldProcess("$Storage on $nodeName", "Store $source as $Content/$name")) {
    return
  }

  try {
    if ($Path) {
      $form = [ordered]@{ content = $Content }
      if ($Checksum) {
        $form.checksum = $Checksum
        $form['checksum-algorithm'] = $ChecksumAlgorithm
      }
      $form.filename = @{ Path = $localPath; FileName = $name }
      $apiPath = Format-PVEPath '/nodes/{node}/storage/{storage}/upload' @{ node = $nodeName; storage = $Storage }
      $upid = [string](Invoke-PVERequest -Connection $connection -Method POST -Path $apiPath -Form $form)
    }
    else {
      $body = @{ url = $Url; content = $Content; filename = $name }
      if ($Checksum) {
        $body.checksum = $Checksum
        $body['checksum-algorithm'] = $ChecksumAlgorithm
      }
      if ($Compression) {
        $body.compression = $Compression
      }
      if ($SkipCertificateCheck) {
        $body['verify-certificates'] = 0
      }
      $apiPath = Format-PVEPath '/nodes/{node}/storage/{storage}/download-url' @{ node = $nodeName; storage = $Storage }
      $upid = [string](Invoke-PVERequest -Connection $connection -Method POST -Path $apiPath -Parameters $body)
    }
  }
  catch {
    $PSCmdlet.WriteError($_)
    return
  }

  $operations = [System.Collections.Generic.List[psobject]]::new()
  $operations.Add([pscustomobject]@{ Upid = $upid; Connection = $connection })
  if ($RunAsync -and $upid -match '^UPID:') {
    Get-PVETask -Upid $upid -Server $connection
    return
  }
  if (@(Wait-PVEOperation -Operation $operations).Count -eq 0) {
    return
  }
  try {
    Find-PVEStoredFile -Connection $connection -Node $nodeName -Storage $Storage -Content $Content -FileName $name
  }
  catch {
    $PSCmdlet.WriteError($_)
  }
}