Download-WinSCPPortable.ps1
|
<#PSScriptInfo
.VERSION 1.0.0 .GUID a3f9c812-5e2b-4d7a-b1f6-8c3e0d9a4b7e .AUTHOR Giovanni Solone .TAGS powershell winscp portable download tools .LICENSEURI https://opensource.org/licenses/MIT .PROJECTURI https://github.com/gioxx/Nebula.Scripts/blob/main/Utility/Download-WinSCPPortable.ps1 .RELEASENOTES v1.0.0 (2026-03-26): Initial release. #> #Requires -Version 7.0 <# .SYNOPSIS Downloads the latest WinSCP Portable version (and optionally the .NET assembly / COM library) to a specified folder. .DESCRIPTION This script checks the official WinSCP download page, retrieves the latest version number, and downloads the portable ZIP package directly from winscp.net by extracting the tokenized download URL from the download page. It extracts the contents directly into the destination folder. Optionally, the .NET assembly / COM library ZIP can also be downloaded, extracting only WinSCPnet.dll from the selected target framework folder (net40 by default, or netstandard2.0 if specified). If the destination folder already contains files from the same version, the download is skipped. .PARAMETER Destination The folder where the portable files will be extracted. Must be specified. .PARAMETER IncludeDotNet If specified, also downloads the .NET assembly / COM library package and extracts only WinSCPnet.dll. .PARAMETER DotNetTarget The target framework folder to extract WinSCPnet.dll from. Accepted values: net40, netstandard2.0. Defaults to net40. .EXAMPLE .\Download-WinSCPPortable.ps1 -Destination "C:\Tools\WinSCP" Downloads the latest WinSCP portable package and extracts it to C:\Tools\WinSCP. .EXAMPLE .\Download-WinSCPPortable.ps1 -Destination "C:\Tools\WinSCP" -IncludeDotNet Downloads the portable package and extracts WinSCPnet.dll from net40 to C:\Tools\WinSCP. .EXAMPLE .\Download-WinSCPPortable.ps1 -Destination "C:\Tools\WinSCP" -IncludeDotNet -DotNetTarget netstandard2.0 Downloads the portable package and extracts WinSCPnet.dll from netstandard2.0 to C:\Tools\WinSCP. .NOTES [Reflection.Assembly]::LoadFile("C:\path\file.dll").ImageRuntimeVersion to check .NET version of a DLL file. #> param ( [Parameter(Mandatory = $true)] [string] $Destination, [switch] $IncludeDotNet, [ValidateSet("net40", "netstandard2.0")] [string] $DotNetTarget = "net40" ) function Get-WinSCPVersion { $WinSCP_URL = "https://winscp.net/eng/download.php" $response = Invoke-WebRequest -Uri $WinSCP_URL -UseBasicParsing $versionRegex = 'WinSCP-(\d+\.\d+\.\d+)-Setup\.exe' $versionMatch = [regex]::Match($response.Content, $versionRegex) if (-not $versionMatch.Success) { Write-Error "Version number not found on WinSCP download page." exit 1 } return $versionMatch.Groups[1].Value } function Test-AlreadyUpToDate { param ( [string] $FilePath, [string] $Version ) if (Test-Path $FilePath) { $fileVersion = (Get-Item $FilePath).VersionInfo.FileVersion -replace ',', '.' -replace ' ', '' if ($fileVersion -like "$Version*") { return $true } } return $false } function Get-WinSCPPackage { param ( [string] $Version, [string] $FileName, [string] $DisplayName ) $tempPath = [System.IO.Path]::GetTempPath() $zipFile = Join-Path $tempPath $FileName $downloadPageUrl = "https://winscp.net/download/$FileName/download" Write-Host "Downloading $DisplayName..." # Load the download page to extract the tokenized URL $response = Invoke-WebRequest -Uri $downloadPageUrl -UseBasicParsing -SessionVariable session -MaximumRedirection 10 if ($response.Content -notmatch '/download/files/[^\s"'']+') { Write-Error "Could not find tokenized download URL in WinSCP download page." exit 1 } $tokenizedUrl = "https://winscp.net" + $matches[0] Write-Host "Token URL: $tokenizedUrl" # Download the actual file using the tokenized URL and the session cookie Invoke-WebRequest -Uri $tokenizedUrl -OutFile $zipFile -UseBasicParsing -WebSession $session # Verify it's a valid ZIP (PK header: 80 75) $fileHeader = Get-Content -Path $zipFile -AsByteStream -TotalCount 2 if ($fileHeader[0] -ne 80 -or $fileHeader[1] -ne 75) { Write-Error "The downloaded file does not appear to be a valid ZIP archive." Remove-Item -LiteralPath $zipFile -Force exit 1 } return $zipFile } function Expand-PortableZip { param ( [string] $ZipPath, [string] $Destination ) Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) $entries = $zip.Entries | Where-Object { -not $_.FullName.EndsWith('/') } # Read all entries into memory first, then close the ZIP before writing to disk $fileData = @() foreach ($entry in $entries) { $ms = [System.IO.MemoryStream]::new() $entryStream = $entry.Open() $entryStream.CopyTo($ms) $entryStream.Close() $fileData += @{ Name = (Split-Path $entry.FullName -Leaf); Bytes = $ms.ToArray() } $ms.Close() } $zip.Dispose() foreach ($file in $fileData) { $targetFile = Join-Path $Destination $file.Name [System.IO.File]::WriteAllBytes($targetFile, $file.Bytes) Write-Host " Extracted: $($file.Name)" } } function Expand-DotNetDll { param ( [string] $ZipPath, [string] $Destination, [string] $DotNetTarget ) Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) $entryPath = "$DotNetTarget/WinSCPnet.dll" $entry = $zip.Entries | Where-Object { $_.FullName -eq $entryPath } | Select-Object -First 1 if ($null -eq $entry) { $zip.Dispose() Write-Error "WinSCPnet.dll not found in '$DotNetTarget' inside the ZIP archive." exit 1 } # Read into memory first, then close ZIP before writing to disk $ms = [System.IO.MemoryStream]::new() $entryStream = $entry.Open() $entryStream.CopyTo($ms) $entryStream.Close() $zip.Dispose() $targetFile = Join-Path $Destination "WinSCPnet.dll" # Retry up to 3 times if the file is locked $maxRetries = 3 $attempt = 0 $success = $false while (-not $success -and $attempt -lt $maxRetries) { try { [System.IO.File]::WriteAllBytes($targetFile, $ms.ToArray()) $success = $true } catch { $attempt++ if ($attempt -lt $maxRetries) { Write-Host " WinSCPnet.dll is locked, retrying in 3 seconds... (attempt $attempt/$maxRetries)" Start-Sleep -Seconds 3 } else { $ms.Close() Write-Error "Could not write WinSCPnet.dll after $maxRetries attempts. The file may be in use by another process: $targetFile" exit 1 } } } $ms.Close() Write-Host " Extracted: WinSCPnet.dll (from $DotNetTarget)" } # --- Main --- $version = Get-WinSCPVersion Write-Output "Latest WinSCP version: $version" # Create destination folder if it doesn't exist if (-not (Test-Path $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null Write-Output "Created destination folder: $Destination" } # Portable package $exePath = Join-Path $Destination "WinSCP.exe" if (Test-AlreadyUpToDate -FilePath $exePath -Version $version) { Write-Output "WinSCP $version portable is already up to date in: $Destination" } else { $zipFile = Get-WinSCPPackage -Version $version -FileName "WinSCP-$version-Portable.zip" -DisplayName "WinSCP $version Portable" Write-Output "Extracting to: $Destination" Expand-PortableZip -ZipPath $zipFile -Destination $Destination Remove-Item -LiteralPath $zipFile -Force Write-Output "Portable package ready in: $Destination" } # .NET assembly / COM library (optional) if ($IncludeDotNet) { $dllPath = Join-Path $Destination "WinSCPnet.dll" if (Test-AlreadyUpToDate -FilePath $dllPath -Version $version) { Write-Output "WinSCPnet.dll $version is already up to date in: $Destination" } else { $zipFile = Get-WinSCPPackage -Version $version -FileName "WinSCP-$version-Automation.zip" -DisplayName "WinSCP $version .NET assembly / COM library" Write-Output "Extracting WinSCPnet.dll from $DotNetTarget to: $Destination" Expand-DotNetDll -ZipPath $zipFile -Destination $Destination -DotNetTarget $DotNetTarget Remove-Item -LiteralPath $zipFile -Force Write-Output ".NET assembly ready in: $Destination" } } |