Public/Copy-OnePAMFile.ps1
|
function Copy-OnePAMFile { <# .SYNOPSIS Copies files to/from a OnePAM resource via SFTP over WebSocket. .DESCRIPTION Transfers files using the SFTP subsystem through the OnePAM gateway. Remote paths use resource:/path or user@resource:/path syntax. .PARAMETER Source One or more source paths. Can be local or remote (resource:/path). .PARAMETER Destination Destination path. Can be local or remote (resource:/path). .PARAMETER Recursive Enable recursive copy for directories. .EXAMPLE Copy-OnePAMFile -Source "myfile.txt" -Destination "my-server:/tmp/" .EXAMPLE Copy-OnePAMFile -Source "my-server:/var/log/app.log" -Destination "./" .EXAMPLE Copy-OnePAMFile -Source "./dir" -Destination "my-server:/opt/" -Recursive #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string[]]$Source, [Parameter(Mandatory, Position = 1)] [string]$Destination, [Alias('r')] [switch]$Recursive ) $allPaths = @($Source) + @($Destination) $remoteResource = $null $login = $null $remoteIndices = @() for ($i = 0; $i -lt $allPaths.Count; $i++) { $parsed = ConvertTo-OpRemoteSpec $allPaths[$i] if ($parsed) { $remoteIndices += $i if (-not $remoteResource) { $remoteResource = $parsed.Resource $login = $parsed.User } elseif ($parsed.Resource -ne $remoteResource) { throw "All remote paths must reference the same resource (found '$remoteResource' and '$($parsed.Resource)')." } if ($parsed.User) { $login = $parsed.User } } } if ($remoteIndices.Count -eq 0) { throw 'At least one source or destination must be a remote path (resource:/path).' } $res = Get-OnePAMResource -Name $remoteResource if (-not $res) { throw "Resource '$remoteResource' not found." } $resId = if ($res.id) { $res.id } elseif ($res.ID) { $res.ID } else { throw "Cannot determine resource ID." } Assert-OpSafePathSegment -Value $resId -Label 'resource ID' $connectBody = @{} if ($login) { $connectBody['ssh_user'] = $login } $session = Invoke-OpApi -Method POST -Path "/api/v1/resources/$resId/connect" -Body $connectBody if (-not $session.session_id) { throw 'Failed to create session.' } Assert-OpSafePathSegment -Value $session.session_id -Label 'session ID' $cfg = Get-OpConfig if ($session.direct) { $baseUri = [Uri]$cfg.api_base $wsScheme = if ($baseUri.Scheme -eq 'https') { 'wss' } else { 'ws' } $hostPort = $baseUri.Authority } else { if (-not $session.proxy_host) { throw 'Session response missing proxy_host.' } $wsScheme = 'wss' $hostPort = $session.proxy_host if ($session.proxy_port -and $session.proxy_port -ne 443) { $hostPort = "$($session.proxy_host):$($session.proxy_port)" } } $connectPath = if ($session.connect_url) { $session.connect_url } else { "/gateway/ssh/$($session.session_id)" } $queryParts = [System.Collections.Generic.List[string]]::new() if (-not $session.direct -and $session.token) { $queryParts.Add("token=$([System.Uri]::EscapeDataString($session.token))") } $queryParts.Add('mode=sftp') $wsUrl = "${wsScheme}://${hostPort}${connectPath}?$($queryParts -join '&')" $wsHeaders = @{} if ($session.direct) { $authToken = Get-OpToken if ($authToken) { $wsHeaders['Authorization'] = "Bearer $($authToken.access_token)" } } $ws = New-OpWebSocket -Uri $wsUrl -Headers $wsHeaders $destIsRemote = $remoteIndices -contains ($allPaths.Count - 1) try { if ($destIsRemote) { $destParsed = ConvertTo-OpRemoteSpec $Destination $destPath = if ($destParsed) { $destParsed.Path } else { $Destination } foreach ($src in $Source) { Copy-OpUpload -WebSocket $ws -LocalPath $src -RemotePath $destPath -Recursive:$Recursive } } else { foreach ($src in $Source) { $srcParsed = ConvertTo-OpRemoteSpec $src if ($srcParsed) { Copy-OpDownload -WebSocket $ws -RemotePath $srcParsed.Path -LocalPath $Destination -Recursive:$Recursive } } } } finally { Close-OpWebSocket -WebSocket $ws } } function ConvertTo-OpRemoteSpec { param([string]$PathString) if ($PathString -match '^[a-zA-Z]:[/\\]') { return $null } if ($PathString -match '^(?:([^@:]+)@)?([^@:]+):(.+)$') { return [PSCustomObject]@{ User = $Matches[1] Resource = $Matches[2] Path = $Matches[3] } } return $null } function Copy-OpUpload { param( [System.Net.WebSockets.ClientWebSocket]$WebSocket, [string]$LocalPath, [string]$RemotePath, [switch]$Recursive ) if (-not (Test-Path $LocalPath)) { throw "Local path '$LocalPath' not found." } $item = Get-Item $LocalPath if ($item.PSIsContainer) { if (-not $Recursive) { throw "'$LocalPath' is a directory. Use -Recursive for directory transfers." } $files = Get-ChildItem -Path $LocalPath -Recurse -File foreach ($file in $files) { $relPath = $file.FullName.Substring($item.FullName.Length).Replace('\', '/') $targetPath = $RemotePath.TrimEnd('/') + $relPath $parentDir = ($targetPath -split '/')[0..$(($targetPath -split '/').Count - 2)] -join '/' if ($parentDir) { $mkdirMsg = @{ type = 'sftp_mkdir'; path = $parentDir } | ConvertTo-Json -Compress Send-OpWsMessage -WebSocket $WebSocket -Message $mkdirMsg } Send-OpFileUpload -WebSocket $WebSocket -LocalFile $file.FullName -RemotePath $targetPath } } else { Send-OpFileUpload -WebSocket $WebSocket -LocalFile $LocalPath -RemotePath $RemotePath } } $script:MaxUploadSize = 500 * 1024 * 1024 # 500 MB function Send-OpFileUpload { param( [System.Net.WebSockets.ClientWebSocket]$WebSocket, [string]$LocalFile, [string]$RemotePath ) $fileInfo = [System.IO.FileInfo]::new($LocalFile) if ($fileInfo.Length -gt $script:MaxUploadSize) { throw "File '$LocalFile' is $([math]::Round($fileInfo.Length / 1MB, 1)) MB, exceeding the $($script:MaxUploadSize / 1MB) MB upload limit." } $fileBytes = [System.IO.File]::ReadAllBytes($LocalFile) $header = @{ type = 'sftp_upload' path = $RemotePath size = $fileBytes.Length } | ConvertTo-Json -Compress Send-OpWsMessage -WebSocket $WebSocket -Message $header if ($fileBytes.Length -gt 0) { Send-OpWsBinaryMessage -WebSocket $WebSocket -Data $fileBytes } $resp = Receive-OpWsMessage -WebSocket $WebSocket -TimeoutSeconds 60 if ($resp.MessageType -eq 'Close') { throw "Connection closed during upload of '$LocalFile'." } if ($resp.Text) { $msg = $resp.Text | ConvertFrom-Json -ErrorAction SilentlyContinue if ($msg -and $msg.type -eq 'error') { throw "Upload failed for '$LocalFile': $($msg.message)" } } $localName = [System.IO.Path]::GetFileName($LocalFile) Write-Host "$localName -> $RemotePath ($($fileBytes.Length) bytes)" -ForegroundColor Cyan } function Copy-OpDownload { param( [System.Net.WebSockets.ClientWebSocket]$WebSocket, [string]$RemotePath, [string]$LocalPath, [switch]$Recursive ) $reqMsg = @{ type = 'sftp_download' path = $RemotePath recursive = [bool]$Recursive } | ConvertTo-Json -Compress Send-OpWsMessage -WebSocket $WebSocket -Message $reqMsg $resp = Receive-OpWsMessage -WebSocket $WebSocket -TimeoutSeconds 120 if ($resp.MessageType -eq 'Close') { throw 'Connection closed during download.' } if ($resp.Text) { $msg = $resp.Text | ConvertFrom-Json -ErrorAction SilentlyContinue if ($msg -and $msg.type -eq 'error') { throw "Download failed for '$RemotePath': $($msg.message)" } } $localTarget = $LocalPath if ((Test-Path $LocalPath) -and (Get-Item $LocalPath).PSIsContainer) { $fileName = [System.IO.Path]::GetFileName($RemotePath) $localTarget = Join-Path $LocalPath $fileName } $resolvedTarget = [System.IO.Path]::GetFullPath($localTarget) $resolvedBase = [System.IO.Path]::GetFullPath($LocalPath) if (-not $resolvedTarget.StartsWith($resolvedBase, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Remote path '$RemotePath' would write outside the target directory." } if ($resp.Data) { $parentDir = [System.IO.Path]::GetDirectoryName($resolvedTarget) if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null } [System.IO.File]::WriteAllBytes($resolvedTarget, $resp.Data) $remoteName = [System.IO.Path]::GetFileName($RemotePath) Write-Host "$remoteName -> $resolvedTarget ($($resp.Data.Length) bytes)" -ForegroundColor Cyan } } |