diff --git a/.gitea/workflows/sshupload.ps1 b/.gitea/workflows/sshupload.ps1 new file mode 100644 index 0000000..74eb78a --- /dev/null +++ b/.gitea/workflows/sshupload.ps1 @@ -0,0 +1,364 @@ +# PowerShell script to upload files to SFTP server with remote folder cleanup +# Works on clean Windows without additional utilities (uses WinSCP) + +# ========================================== +# PARAMETERS (can override config values via command line) +# ========================================== +param( + [Parameter(Mandatory=$false, HelpMessage="SFTP server IP address or hostname")] + [string]$ServerAddress, + + [Parameter(Mandatory=$false, HelpMessage="Username for connection")] + [string]$Username, + + [Parameter(Mandatory=$false, HelpMessage="Password for connection")] + [string]$PasswordParam, + + [Parameter(Mandatory=$false, HelpMessage="Local file path or pattern (e.g., C:\files\* or dist/builds/x64/Rosetta-*.exe)")] + [string]$LocalFilePath, + + [Parameter(Mandatory=$false, HelpMessage="Remote folder on server")] + [string]$RemoteFolderPath, + + [Parameter(Mandatory=$false, HelpMessage="SSH port")] + [int]$Port, + + [Parameter(Mandatory=$false, HelpMessage="Path to WinSCP executable (auto-detect if not provided)")] + [string]$WinSCPPath +) + +# ========================================== +# CONFIGURATION - Default fallback values +# ========================================== +# These values are used only if not provided via command-line parameters or environment variables +$CONFIG_ServerAddress = "" +$CONFIG_Username = "" +$CONFIG_Password = "" +$CONFIG_LocalFilePath = "" +$CONFIG_RemoteFolderPath = "" +$CONFIG_Port = 22 +$CONFIG_WinSCPPath = "" + +# Priority: Command-line Parameters (highest) > Environment Variables > Config Values (lowest) +# If parameter not provided via command line, check environment variable, then use config value +if (-not $ServerAddress) { + $ServerAddress = if ($env:SFTP_SERVER) { $env:SFTP_SERVER } else { $CONFIG_ServerAddress } +} +if (-not $Username) { + $Username = if ($env:SFTP_USERNAME) { $env:SFTP_USERNAME } else { $CONFIG_Username } +} +if (-not $PasswordParam) { + $PasswordParam = if ($env:SFTP_PASSWORD) { $env:SFTP_PASSWORD } else { $CONFIG_Password } +} +if (-not $LocalFilePath) { + $LocalFilePath = if ($env:SFTP_LOCAL_PATH) { $env:SFTP_LOCAL_PATH } else { $CONFIG_LocalFilePath } +} +if (-not $RemoteFolderPath) { + $RemoteFolderPath = if ($env:SFTP_REMOTE_PATH) { $env:SFTP_REMOTE_PATH } else { $CONFIG_RemoteFolderPath } +} +if (-not $Port -or $Port -eq 0) { + $Port = if ($env:SFTP_PORT) { [int]$env:SFTP_PORT } else { $CONFIG_Port } +} +if (-not $WinSCPPath) { + $WinSCPPath = if ($env:WINSCP_PATH) { $env:WINSCP_PATH } else { $CONFIG_WinSCPPath } +} + +# Validate required parameters +$requiredParams = @( + @{Name = "ServerAddress"; Value = $ServerAddress}, + @{Name = "Username"; Value = $Username}, + @{Name = "Password"; Value = $PasswordParam}, + @{Name = "LocalFilePath"; Value = $LocalFilePath}, + @{Name = "RemoteFolderPath"; Value = $RemoteFolderPath} +) + +$missingParams = @() +foreach ($param in $requiredParams) { + if ([string]::IsNullOrWhiteSpace($param.Value)) { + $missingParams += $param.Name + } +} + +if ($missingParams.Count -gt 0) { + Write-Host "ERROR: Missing required parameters: $($missingParams -join ', ')" -ForegroundColor Red + Write-Host "Please configure values in the script CONFIG section or pass them as parameters." -ForegroundColor Red + exit 1 +} + +# Logging function +function Write-Log { + param( + [Parameter(Mandatory=$false)] + [string]$Message = "(empty message)", + + [Parameter(Mandatory=$false)] + [ValidateSet("Info", "Warning", "Error", "Success")] + [string]$Level = "Info" + ) + + # Handle null or empty messages + if ([string]::IsNullOrWhiteSpace($Message)) { + $Message = "(empty message)" + } + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $color = switch ($Level) { + "Error" { "Red" } + "Warning" { "Yellow" } + "Success" { "Green" } + default { "White" } + } + + Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color +} + +# Function to find WinSCP installation +function Find-WinSCP { + $possiblePaths = @( + "C:\Program Files\WinSCP\WinSCP.com", + "C:\Program Files (x86)\WinSCP\WinSCP.com", + "C:\Program Files\WinSCP\WinSCP.exe", + "C:\Program Files (x86)\WinSCP\WinSCP.exe", + "C:\Program Files\WinSCP\WinSCPPortable.exe", + "C:\Program Files (x86)\WinSCP\WinSCPPortable.exe" + ) + + foreach ($path in $possiblePaths) { + if (Test-Path $path) { + Write-Log "Found WinSCP at: $path" "Info" + return $path + } + } + + Write-Log "WinSCP not found. Please install it from https://winscp.net/" "Error" + return $null +} + +# Main upload function using WinSCP +function Upload-ToSFTP { + param( + [Parameter(Mandatory=$true)] + [string]$Server, + + [Parameter(Mandatory=$true)] + [string]$User, + + [Parameter(Mandatory=$true)] + [string]$Pass, + + [Parameter(Mandatory=$true)] + [string[]]$FileList, + + [Parameter(Mandatory=$true)] + [string]$RemotePath, + + [Parameter(Mandatory=$true)] + [int]$PortNum, + + [Parameter(Mandatory=$true)] + [string]$WinSCPExe + ) + + # Password is already a plain string, use it directly + $plainPassword = $Pass + + # Escape special characters in password that could break URL or WinSCP syntax + # Replace @ with %40, : with %3A, # with %23, $ with %24, & with %26 + $escapedPassword = $plainPassword + $escapedPassword = $escapedPassword -replace '@', '%40' + $escapedPassword = $escapedPassword -replace ':', '%3A' + $escapedPassword = $escapedPassword -replace '#', '%23' + $escapedPassword = $escapedPassword -replace '\$', '%24' + $escapedPassword = $escapedPassword -replace '`', '%60' + $escapedPassword = $escapedPassword -replace '&', '%26' + + # Create temporary file paths BEFORE script content (needed for variable expansion) + $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss_fff' + $debugDir = Join-Path $env:TEMP "winscp_debug" + if (-not (Test-Path $debugDir)) { + New-Item -ItemType Directory -Path $debugDir -Force | Out-Null + } + $scriptPath = Join-Path $debugDir "script_$timestamp.txt" + $logFile = Join-Path $debugDir "log_$timestamp.txt" + $outputPath = Join-Path $debugDir "output_$timestamp.txt" + $errorPath = Join-Path $debugDir "error_$timestamp.txt" + + # Create WinSCP script file WITH password (use @"..."@ to expand variables) + $scriptContent = @" +option batch abort +option confirm off +option echo off +option reconnecttime 3 +"@ + + # Add connection string with auto-accept of host key + $scriptContent += "`r`nopen sftp://$User`:$escapedPassword@$Server`:$PortNum -hostkey=`"*`"`r`n" + # Try to clear remote folder by removing all .exe files (ignore if none exist) + $scriptContent += "call rm -f $RemotePath/*.exe`r`n" + + # Add files to WinSCP script + if ($FileList.Count -eq 0) { + Write-Log "No files found matching pattern" "Warning" + $scriptContent += "exit`r`n" + } + else { + foreach ($filePath in $FileList) { + # For local Windows paths, keep backslashes as-is (don't convert to forward slashes) + # WinSCP needs native Windows paths for local files + $remoteFilename = Split-Path $filePath -Leaf + $scriptContent += "put `"$filePath`" `"$RemotePath/$remoteFilename`"`r`n" + } + $scriptContent += "close`r`nexit`r`n" + } + + # Save script to temporary file + try { + Set-Content -Path $scriptPath -Value $scriptContent -Encoding UTF8 + + Write-Log "Created WinSCP script at: $scriptPath" "Info" + Write-Log "Script content:" "Info" + Get-Content $scriptPath | ForEach-Object { Write-Log "$_" "Info" } + + Write-Log "Executing WinSCP: $WinSCPExe" "Info" + + try { + # Determine if this is .com (command-line) or .exe (GUI) + $isCom = $WinSCPExe -like "*.com" + + if ($isCom) { + # WinSCP.com uses /log= for logging + $process = Start-Process -FilePath $WinSCPExe ` + -ArgumentList "/log=$logFile /script=$scriptPath" ` + -NoNewWindow ` + -PassThru ` + -Wait ` + -RedirectStandardOutput $outputPath ` + -RedirectStandardError $errorPath + } + else { + # WinSCP.exe (GUI) - needs option logfile in script + $scriptContent += "`r`noption logfile=$logFile" + Set-Content -Path $scriptPath -Value $scriptContent -Encoding UTF8 + + $process = Start-Process -FilePath $WinSCPExe ` + -ArgumentList "/console /script=$scriptPath" ` + -NoNewWindow ` + -PassThru ` + -Wait ` + -RedirectStandardOutput $outputPath ` + -RedirectStandardError $errorPath + } + } + catch { + Write-Log "Error starting process: $_" "Error" + throw + } + + Write-Log "WinSCP process finished with exit code: $($process.ExitCode)" "Info" + + # Read WinSCP logs + $winscp_log = Get-Content $logFile -ErrorAction SilentlyContinue -Raw + $output = Get-Content $outputPath -ErrorAction SilentlyContinue -Raw + $error_output = Get-Content $errorPath -ErrorAction SilentlyContinue -Raw + + if ($winscp_log) { + Write-Log "WinSCP Log:`r`n$winscp_log" "Info" + } + + if ($output) { + Write-Log "Output:`r`n$output" "Info" + } + else { + Write-Log "No standard output from WinSCP" "Info" + } + + if ($error_output) { + Write-Log "Standard Error:`r`n$error_output" "Error" + } + + if ($process.ExitCode -eq 0) { + Write-Log "Upload completed successfully" "Success" + return $true + } + else { + Write-Log "Upload failed with exit code: $($process.ExitCode)" "Error" + return $false + } + } + catch { + Write-Log "Error during upload: $_" "Error" + return $false + } + finally { + # Cleanup temporary files + Start-Sleep -Milliseconds 500 + if (Test-Path $scriptPath) { + Remove-Item $scriptPath -Force -ErrorAction SilentlyContinue + } + if (Test-Path $logFile) { + Remove-Item $logFile -Force -ErrorAction SilentlyContinue + } + if (Test-Path $outputPath) { + Remove-Item $outputPath -Force -ErrorAction SilentlyContinue + } + if (Test-Path $errorPath) { + Remove-Item $errorPath -Force -ErrorAction SilentlyContinue + } + } +} + +# ================= +# MAIN LOGIC +# ================= + +Write-Log "========== STARTING FILE UPLOAD PROCESS ==========" "Info" +Write-Log "Server: $ServerAddress`:$Port" "Info" +Write-Log "Username: $Username" "Info" +Write-Log "File pattern: $LocalFilePath" "Info" +Write-Log "Remote folder: $RemoteFolderPath" "Info" +Write-Log "=============================================" "Info" + +# Find WinSCP if path not provided +if (-not $WinSCPPath) { + $WinSCPPath = Find-WinSCP + if (-not $WinSCPPath) { + exit 1 + } +} + +# Verify WinSCP exists +if (-not (Test-Path $WinSCPPath)) { + Write-Log "Error: WinSCP not found at: $WinSCPPath" "Error" + exit 1 +} + +# Get files matching pattern +$files = @(Get-Item -Path $LocalFilePath -ErrorAction SilentlyContinue | Where-Object {-not $_.PSIsContainer}) + +if ($files.Count -eq 0) { + Write-Log "Error: No files found matching pattern: $LocalFilePath" "Error" + Write-Log "Current directory: $(Get-Location)" "Error" + Write-Log "Checking if path exists: $(Test-Path $LocalFilePath)" "Error" + exit 1 +} + +Write-Log "Found $($files.Count) file(s) to upload" "Info" +$filePathList = @($files | ForEach-Object {$_.FullName}) + +# Perform upload +$success = Upload-ToSFTP -Server $ServerAddress ` + -User $Username ` + -Pass $PasswordParam ` + -FileList $filePathList ` + -RemotePath $RemoteFolderPath ` + -PortNum $Port ` + -WinSCPExe $WinSCPPath + +Write-Log "========== PROCESS COMPLETED ==========" "Info" + +if ($success) { + exit 0 +} +else { + exit 1 +} diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/windows.yaml similarity index 65% rename from .gitea/workflows/build.yaml rename to .gitea/workflows/windows.yaml index a04a8ab..999ac48 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/windows.yaml @@ -31,13 +31,7 @@ jobs: - name: Build the application run: npm run kernel:win - - name: Upload to SSH - uses: appleboy/ssh-action@v1.0.3 # or a Gitea mirror - with: - host: ${{ secrets.SSH_HOST }} - username: ${{ secrets.SSH_USERNAME }} - password: ${{ secrets.SSH_PASSWORD }} - port: ${{ secrets.SSH_PORT }} - source: "dist/builds/win/x64/*.exe" - target: "${{ secrets.SSH_TARGET_DIR }}" - strip_components: 3 \ No newline at end of file + - name: Upload to SSH using WinSCP Powershell + shell: powershell + run: | + .\sshupload.ps1 -LocalFilePath "dist/builds/win/x64/Rosetta-*.exe" -RemoteFolderPath ${{ secrets.SSH_TARGET_DIR }} -ServerAddress ${{ secrets.SSH_HOST }} -Username ${{ secrets.SSH_USERNAME }} -PasswordParam ${{ secrets.SSH_PASSWORD }}