Initial commit: Send-FilesToSftp.ps1 and README
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# Send-FilesToSftp.ps1
|
||||
|
||||
PowerShell script to transfer files to an SFTP server with regex filtering, secure credential handling, and logging.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**WinSCP .NET Assembly** (`WinSCPnet.dll`) is required. Install using one of these methods:
|
||||
|
||||
| Method | Steps |
|
||||
|--------|-------|
|
||||
| **Drop-in** | Download the [.NET assembly package](https://winscp.net/eng/downloads.php), extract `WinSCPnet.dll` next to the script |
|
||||
| **NuGet** | `Install-Package WinSCP -Source nuget.org` |
|
||||
| **WinSCP Installer** | Install WinSCP and check the ".NET assembly" option during setup |
|
||||
|
||||
The script auto-searches these locations in order:
|
||||
1. `-WinScpDllPath` parameter (if provided)
|
||||
2. Same directory as the script
|
||||
3. `lib\` subdirectory of the script folder
|
||||
4. `C:\Program Files (x86)\WinSCP\`
|
||||
5. `C:\Program Files\WinSCP\`
|
||||
6. NuGet package cache (`~\.nuget\packages\winscp`)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```powershell
|
||||
# Basic usage — prompted for password interactively
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader"
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `-LocalPath` | Yes | — | Local source folder to scan |
|
||||
| `-RemotePath` | Yes | — | Remote SFTP destination folder |
|
||||
| `-HostName` | Yes | — | SFTP server hostname or IP |
|
||||
| `-UserName` | Yes | — | SFTP username |
|
||||
| `-FileFilter` | No | `.*` | Regex pattern to match filenames |
|
||||
| `-Port` | No | `22` | SFTP port |
|
||||
| `-Credential` | No | — | `PSCredential` object |
|
||||
| `-CredentialFile` | No | — | Path to saved credential XML (see below) |
|
||||
| `-KeyFilePath` | No | — | Path to SSH private key (`.ppk`) |
|
||||
| `-SshHostKeyFingerprint` | No | — | SSH host key fingerprint for verification |
|
||||
| `-Recurse` | No | `false` | Scan subdirectories |
|
||||
| `-DeleteAfterTransfer` | No | `false` | Delete local files after successful upload |
|
||||
| `-DryRun` | No | `false` | Preview transfers without uploading |
|
||||
| `-LogFile` | No | — | Path to log file (logs to console if omitted) |
|
||||
| `-WinScpDllPath` | No | — | Explicit path to `WinSCPnet.dll` |
|
||||
|
||||
## Authentication
|
||||
|
||||
The script supports three auth methods, checked in this order:
|
||||
|
||||
### 1. Credential Object (interactive or pipeline)
|
||||
```powershell
|
||||
$cred = Get-Credential
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
|
||||
-HostName "sftp.example.com" -UserName "brad" -Credential $cred
|
||||
```
|
||||
|
||||
### 2. Saved Credential File (unattended / scheduled tasks)
|
||||
```powershell
|
||||
# One-time setup — run as the same user that will run the scheduled task
|
||||
Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
|
||||
|
||||
# Use in script
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
|
||||
-HostName "sftp.example.com" -UserName "svc_upload" `
|
||||
-CredentialFile "C:\secure\sftp_cred.xml"
|
||||
```
|
||||
|
||||
> **Note:** `Export-Clixml` encrypts credentials using Windows DPAPI, tied to the user account and machine that created the file. Only that same user on that same machine can decrypt it.
|
||||
|
||||
### 3. SSH Private Key
|
||||
```powershell
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
|
||||
-HostName "sftp.example.com" -UserName "svc_upload" `
|
||||
-KeyFilePath "C:\keys\id_rsa.ppk"
|
||||
```
|
||||
|
||||
### 4. Interactive Prompt
|
||||
If none of the above are provided and no key file is specified, you'll be prompted for a password at runtime.
|
||||
|
||||
## File Filtering (Regex)
|
||||
|
||||
The `-FileFilter` parameter uses PowerShell regex (case-insensitive by default).
|
||||
|
||||
| Filter | Matches |
|
||||
|--------|---------|
|
||||
| `'\.csv$'` | All CSV files |
|
||||
| `'\.xlsx?$'` | `.xls` and `.xlsx` files |
|
||||
| `'^report_\d{8}'` | Files starting with `report_` + 8 digits (e.g. `report_20260415.csv`) |
|
||||
| `'(?-i)^Data'` | Files starting with `Data` (case-sensitive) |
|
||||
| `'badge.*\.xlsx$'` | Excel files with `badge` anywhere in the name |
|
||||
| `'\.(csv\|txt)$'` | CSV or TXT files |
|
||||
| `'.*'` | Everything (default) |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Transfer all CSVs
|
||||
```powershell
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" -FileFilter '\.csv$'
|
||||
```
|
||||
|
||||
### Move files (delete after upload) with logging
|
||||
```powershell
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "svc_upload" `
|
||||
-CredentialFile "C:\secure\cred.xml" `
|
||||
-DeleteAfterTransfer -LogFile "C:\logs\sftp_transfer.log"
|
||||
```
|
||||
|
||||
### Dry run — preview without transferring
|
||||
```powershell
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" -DryRun
|
||||
```
|
||||
|
||||
### Recursive with subdirectories
|
||||
```powershell
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data\projects" -RemotePath "/archive" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" `
|
||||
-Recurse -FileFilter '\.pdf$'
|
||||
```
|
||||
|
||||
## SSH Host Key Fingerprint
|
||||
|
||||
For production use, always provide the host key fingerprint to prevent MITM attacks:
|
||||
|
||||
```powershell
|
||||
# Get the fingerprint from the server
|
||||
ssh-keyscan sftp.example.com | ssh-keygen -lf -
|
||||
|
||||
# Use it in the script
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" `
|
||||
-SshHostKeyFingerprint "ssh-rsa 2048 aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99"
|
||||
```
|
||||
|
||||
Pass `"*"` to accept any key (development/testing only — **not recommended for production**).
|
||||
|
||||
## Scheduled Task Setup
|
||||
|
||||
To run unattended via Windows Task Scheduler:
|
||||
|
||||
1. **Save credentials** as the service account user:
|
||||
```powershell
|
||||
Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
|
||||
```
|
||||
|
||||
2. **Create the scheduled task:**
|
||||
```powershell
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "powershell.exe" `
|
||||
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\scripts\Send-FilesToSftp.ps1" -LocalPath "C:\exports" -RemotePath "/incoming" -HostName "sftp.example.com" -UserName "svc_upload" -CredentialFile "C:\secure\sftp_cred.xml" -FileFilter "\.csv$" -DeleteAfterTransfer -LogFile "C:\logs\sftp.log"'
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At "2:00AM"
|
||||
|
||||
Register-ScheduledTask -TaskName "SFTP Upload" `
|
||||
-Action $action -Trigger $trigger `
|
||||
-User "DOMAIN\svc_upload" -Password "****" `
|
||||
-RunLevel Highest
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `0` | All files transferred (or no files matched filter) |
|
||||
| `1` | One or more failures occurred |
|
||||
|
||||
## Log Output
|
||||
|
||||
Logs include timestamps and severity levels. Sample output:
|
||||
|
||||
```
|
||||
[2026-04-16 14:30:01] [INFO] ═══ SFTP Transfer Starting ═══
|
||||
[2026-04-16 14:30:01] [INFO] Local path : C:\exports
|
||||
[2026-04-16 14:30:01] [INFO] Remote path : /incoming
|
||||
[2026-04-16 14:30:01] [INFO] File filter : \.csv$
|
||||
[2026-04-16 14:30:01] [INFO] Found 3 file(s) matching filter
|
||||
[2026-04-16 14:30:02] [SUCCESS] Connected to sftp.example.com
|
||||
[2026-04-16 14:30:03] [SUCCESS] Transferred: report_20260415.csv → /incoming/report_20260415.csv (42.3 KB)
|
||||
[2026-04-16 14:30:03] [SUCCESS] Transferred: badges_export.csv → /incoming/badges_export.csv (18.1 KB)
|
||||
[2026-04-16 14:30:04] [SUCCESS] Transferred: access_log.csv → /incoming/access_log.csv (7.8 KB)
|
||||
[2026-04-16 14:30:04] [INFO] ═══ Transfer Complete ═══
|
||||
[2026-04-16 14:30:04] [INFO] Succeeded : 3
|
||||
[2026-04-16 14:30:04] [INFO] Mode : COPY (source files retained)
|
||||
```
|
||||
@@ -0,0 +1,346 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Transfers files to an SFTP server with filtering and logging.
|
||||
|
||||
.DESCRIPTION
|
||||
Moves or copies files from a local folder to a remote SFTP destination.
|
||||
Supports regex/wildcard file filtering, recursive scanning, and secure
|
||||
credential storage. Uses the WinSCP .NET assembly for SFTP transport.
|
||||
|
||||
.PARAMETER LocalPath
|
||||
Local source folder to scan for files.
|
||||
|
||||
.PARAMETER RemotePath
|
||||
Remote SFTP destination folder (e.g. /uploads/incoming).
|
||||
|
||||
.PARAMETER FileFilter
|
||||
Regex pattern to match filenames (e.g. '\.csv$' or '^report_\d{8}').
|
||||
Default: '.*' (all files).
|
||||
|
||||
.PARAMETER HostName
|
||||
SFTP server hostname or IP.
|
||||
|
||||
.PARAMETER Port
|
||||
SFTP port. Default: 22.
|
||||
|
||||
.PARAMETER UserName
|
||||
SFTP username.
|
||||
|
||||
.PARAMETER SshHostKeyFingerprint
|
||||
SSH host key fingerprint for server verification.
|
||||
Use "ssh-rsa 2048 xx:xx:xx..." format, or pass "*" to accept any (NOT recommended for production).
|
||||
|
||||
.PARAMETER Credential
|
||||
PSCredential object. If omitted, you'll be prompted interactively.
|
||||
|
||||
.PARAMETER CredentialFile
|
||||
Path to a saved credential file (Export-Clixml). For scheduled/unattended runs.
|
||||
Create one with: Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
|
||||
|
||||
.PARAMETER KeyFilePath
|
||||
Path to a private key file for key-based auth (optional).
|
||||
|
||||
.PARAMETER Recurse
|
||||
Scan subdirectories in LocalPath.
|
||||
|
||||
.PARAMETER DeleteAfterTransfer
|
||||
Delete local files after successful upload (move behavior).
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be transferred without actually uploading.
|
||||
|
||||
.PARAMETER LogFile
|
||||
Path to a log file. If omitted, logs to console only.
|
||||
|
||||
.PARAMETER WinScpDllPath
|
||||
Path to WinSCPnet.dll. Default: looks in script directory, then common install paths.
|
||||
|
||||
.EXAMPLE
|
||||
# Interactive - transfer all CSVs
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" -FileFilter '\.csv$'
|
||||
|
||||
.EXAMPLE
|
||||
# Unattended with saved credentials and move behavior
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" `
|
||||
-CredentialFile "C:\secure\sftp_cred.xml" `
|
||||
-FileFilter '^report_\d{8}' -DeleteAfterTransfer -LogFile "C:\logs\sftp.log"
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run to preview what would transfer
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
|
||||
-HostName "sftp.example.com" -UserName "uploader" -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
# Key-based auth, recursive scan
|
||||
.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/archive" `
|
||||
-HostName "sftp.example.com" -UserName "svcaccount" `
|
||||
-KeyFilePath "C:\keys\id_rsa.ppk" -Recurse
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateScript({ Test-Path $_ -PathType Container })]
|
||||
[string]$LocalPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RemotePath,
|
||||
|
||||
[string]$FileFilter = '.*',
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$HostName,
|
||||
|
||||
[int]$Port = 22,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]$UserName,
|
||||
|
||||
[string]$SshHostKeyFingerprint = $null,
|
||||
|
||||
[PSCredential]$Credential,
|
||||
|
||||
[string]$CredentialFile,
|
||||
|
||||
[string]$KeyFilePath,
|
||||
|
||||
[switch]$Recurse,
|
||||
|
||||
[switch]$DeleteAfterTransfer,
|
||||
|
||||
[switch]$DryRun,
|
||||
|
||||
[string]$LogFile,
|
||||
|
||||
[string]$WinScpDllPath
|
||||
)
|
||||
|
||||
# ── Logging ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ValidateSet('INFO','WARN','ERROR','SUCCESS')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
|
||||
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
$entry = "[$timestamp] [$Level] $Message"
|
||||
|
||||
switch ($Level) {
|
||||
'ERROR' { Write-Host $entry -ForegroundColor Red }
|
||||
'WARN' { Write-Host $entry -ForegroundColor Yellow }
|
||||
'SUCCESS' { Write-Host $entry -ForegroundColor Green }
|
||||
default { Write-Host $entry }
|
||||
}
|
||||
|
||||
if ($LogFile) {
|
||||
$entry | Out-File -FilePath $LogFile -Append -Encoding utf8
|
||||
}
|
||||
}
|
||||
|
||||
# ── Locate WinSCP .NET Assembly ──────────────────────────────────────────────
|
||||
|
||||
function Find-WinScpDll {
|
||||
$searchPaths = @(
|
||||
$WinScpDllPath
|
||||
(Join-Path $PSScriptRoot 'WinSCPnet.dll')
|
||||
(Join-Path $PSScriptRoot 'lib\WinSCPnet.dll')
|
||||
'C:\Program Files (x86)\WinSCP\WinSCPnet.dll'
|
||||
'C:\Program Files\WinSCP\WinSCPnet.dll'
|
||||
) | Where-Object { $_ }
|
||||
|
||||
foreach ($path in $searchPaths) {
|
||||
if (Test-Path $path) {
|
||||
return $path
|
||||
}
|
||||
}
|
||||
|
||||
# Try NuGet package in common locations
|
||||
$nugetPath = Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages\winscp" -Filter 'WinSCPnet.dll' -Recurse -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($nugetPath) { return $nugetPath.FullName }
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
Write-Log "═══ SFTP Transfer Starting ═══"
|
||||
Write-Log "Local path : $LocalPath"
|
||||
Write-Log "Remote path : $RemotePath"
|
||||
Write-Log "File filter : $FileFilter"
|
||||
Write-Log "Host : ${HostName}:${Port}"
|
||||
if ($DryRun) { Write-Log "*** DRY RUN MODE - No files will be transferred ***" -Level WARN }
|
||||
|
||||
# ── Find and load WinSCP ─────────────────────────────────────────────
|
||||
$dllPath = Find-WinScpDll
|
||||
if (-not $dllPath) {
|
||||
Write-Log "WinSCPnet.dll not found. Install options:" -Level ERROR
|
||||
Write-Log " 1) Install-Package WinSCP -Source nuget.org" -Level ERROR
|
||||
Write-Log " 2) Download from https://winscp.net/eng/downloads.php (.NET assembly)" -Level ERROR
|
||||
Write-Log " 3) Place WinSCPnet.dll in the same folder as this script" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
Add-Type -Path $dllPath
|
||||
Write-Log "Loaded WinSCP from: $dllPath"
|
||||
|
||||
# ── Resolve credentials ──────────────────────────────────────────────
|
||||
$password = $null
|
||||
|
||||
if ($Credential) {
|
||||
$password = $Credential.GetNetworkCredential().Password
|
||||
}
|
||||
elseif ($CredentialFile) {
|
||||
if (-not (Test-Path $CredentialFile)) {
|
||||
Write-Log "Credential file not found: $CredentialFile" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
$savedCred = Import-Clixml -Path $CredentialFile
|
||||
$password = $savedCred.GetNetworkCredential().Password
|
||||
Write-Log "Loaded credentials from file"
|
||||
}
|
||||
elseif (-not $KeyFilePath) {
|
||||
$promptCred = Get-Credential -UserName $UserName -Message "Enter SFTP password for $HostName"
|
||||
if (-not $promptCred) {
|
||||
Write-Log "No credentials provided. Aborting." -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
$password = $promptCred.GetNetworkCredential().Password
|
||||
}
|
||||
|
||||
# ── Collect files ────────────────────────────────────────────────────
|
||||
$gciParams = @{ Path = $LocalPath; File = $true }
|
||||
if ($Recurse) { $gciParams['Recurse'] = $true }
|
||||
|
||||
$allFiles = Get-ChildItem @gciParams | Where-Object { $_.Name -match $FileFilter }
|
||||
|
||||
if (-not $allFiles -or $allFiles.Count -eq 0) {
|
||||
Write-Log "No files matched filter '$FileFilter' in $LocalPath" -Level WARN
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Log "Found $($allFiles.Count) file(s) matching filter"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Log "Files that would be transferred:" -Level INFO
|
||||
foreach ($f in $allFiles) {
|
||||
$relativePath = $f.FullName.Substring($LocalPath.TrimEnd('\').Length)
|
||||
$remoteDest = ($RemotePath.TrimEnd('/') + $relativePath) -replace '\\', '/'
|
||||
$sizeKB = [math]::Round($f.Length / 1KB, 1)
|
||||
Write-Log " $($f.FullName) → $remoteDest (${sizeKB} KB)"
|
||||
}
|
||||
Write-Log "DRY RUN complete. $($allFiles.Count) file(s) would be transferred." -Level SUCCESS
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Configure session ────────────────────────────────────────────────
|
||||
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
|
||||
Protocol = [WinSCP.Protocol]::Sftp
|
||||
HostName = $HostName
|
||||
PortNumber = $Port
|
||||
UserName = $UserName
|
||||
}
|
||||
|
||||
if ($password) {
|
||||
$sessionOptions.Password = $password
|
||||
}
|
||||
|
||||
if ($KeyFilePath) {
|
||||
if (-not (Test-Path $KeyFilePath)) {
|
||||
Write-Log "Private key file not found: $KeyFilePath" -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
$sessionOptions.SshPrivateKeyPath = $KeyFilePath
|
||||
Write-Log "Using key-based auth: $KeyFilePath"
|
||||
}
|
||||
|
||||
if ($SshHostKeyFingerprint) {
|
||||
if ($SshHostKeyFingerprint -eq '*') {
|
||||
Write-Log "Accepting any SSH host key — NOT SAFE FOR PRODUCTION" -Level WARN
|
||||
$sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey = $true
|
||||
}
|
||||
else {
|
||||
$sessionOptions.SshHostKeyFingerprint = $SshHostKeyFingerprint
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log "No SSH host key fingerprint provided — accepting any key" -Level WARN
|
||||
Write-Log " Get your fingerprint with: ssh-keyscan $HostName | ssh-keygen -lf -" -Level WARN
|
||||
$sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey = $true
|
||||
}
|
||||
|
||||
# ── Open session and transfer ────────────────────────────────────────
|
||||
$session = New-Object WinSCP.Session
|
||||
$successCount = 0
|
||||
$failCount = 0
|
||||
|
||||
try {
|
||||
$session.Open($sessionOptions)
|
||||
Write-Log "Connected to $HostName" -Level SUCCESS
|
||||
|
||||
# Ensure remote directory exists
|
||||
if (-not $session.FileExists($RemotePath)) {
|
||||
Write-Log "Creating remote directory: $RemotePath"
|
||||
$session.CreateDirectory($RemotePath)
|
||||
}
|
||||
|
||||
foreach ($file in $allFiles) {
|
||||
$relativePath = ''
|
||||
if ($Recurse) {
|
||||
$relativePath = $file.DirectoryName.Substring($LocalPath.TrimEnd('\').Length) -replace '\\', '/'
|
||||
}
|
||||
|
||||
$targetDir = $RemotePath.TrimEnd('/') + $relativePath
|
||||
$targetPath = "$targetDir/$($file.Name)"
|
||||
|
||||
try {
|
||||
# Ensure subdirectory exists on remote when recursing
|
||||
if ($Recurse -and $relativePath -and (-not $session.FileExists($targetDir))) {
|
||||
$session.CreateDirectory($targetDir)
|
||||
Write-Log "Created remote dir: $targetDir"
|
||||
}
|
||||
|
||||
$transferOptions = New-Object WinSCP.TransferOptions
|
||||
$transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
|
||||
|
||||
$result = $session.PutFiles($file.FullName, $targetPath, $DeleteAfterTransfer, $transferOptions)
|
||||
$result.Check()
|
||||
|
||||
$sizeKB = [math]::Round($file.Length / 1KB, 1)
|
||||
Write-Log "Transferred: $($file.Name) → $targetPath (${sizeKB} KB)" -Level SUCCESS
|
||||
$successCount++
|
||||
}
|
||||
catch {
|
||||
Write-Log "FAILED: $($file.Name) — $($_.Exception.Message)" -Level ERROR
|
||||
$failCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($session) { $session.Dispose() }
|
||||
}
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────
|
||||
Write-Log "═══ Transfer Complete ═══"
|
||||
Write-Log " Succeeded : $successCount"
|
||||
if ($failCount -gt 0) {
|
||||
Write-Log " Failed : $failCount" -Level ERROR
|
||||
}
|
||||
if ($DeleteAfterTransfer) {
|
||||
Write-Log " Mode : MOVE (source files deleted on success)"
|
||||
}
|
||||
else {
|
||||
Write-Log " Mode : COPY (source files retained)"
|
||||
}
|
||||
|
||||
if ($failCount -gt 0) { exit 1 }
|
||||
}
|
||||
catch {
|
||||
Write-Log "Fatal error: $($_.Exception.Message)" -Level ERROR
|
||||
Write-Log $_.ScriptStackTrace -Level ERROR
|
||||
exit 1
|
||||
}
|
||||
Verwijs in nieuw issue
Block a user