2026-04-16 21:25:49 -05:00
# 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 |
2026-04-16 21:36:35 -05:00
| `-RenamePattern` | No | — | Regex pattern to match in filename for renaming on the **remote ** side before upload |
2026-04-16 21:34:20 -05:00
| `-RenameReplacement` | No | — | Replacement string for `-RenamePattern` (supports capture groups like `$1` ) |
2026-04-16 21:36:35 -05:00
| `-ArchivePath` | No | — | Move successfully uploaded files to this local folder (created if missing). Cannot combine with `-DeleteAfterTransfer` |
| `-LocalRenamePattern` | No | — | Regex pattern to rename the **local ** file after upload (in place, or into `-ArchivePath` ) |
| `-LocalRenameReplacement` | No | — | Replacement string for `-LocalRenamePattern` (supports capture groups like `$1` ) |
2026-04-16 21:25:49 -05:00
| `-Recurse` | No | `false` | Scan subdirectories |
2026-04-16 21:36:35 -05:00
| `-DeleteAfterTransfer` | No | `false` | Delete local files after successful upload. Cannot combine with `-ArchivePath` |
2026-04-16 21:25:49 -05:00
| `-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$'
```
2026-04-16 21:36:35 -05:00
## Local Archive & Rename After Upload
After a successful upload you can archive or rename the local source file. These are mutually exclusive with `-DeleteAfterTransfer` .
### Archive to a folder
``` powershell
. \ Send-FilesToSftp . ps1 -LocalPath " C:\exports " -RemotePath " /incoming " `
-HostName " sftp.example.com " -UserName " uploader " `
-ArchivePath " C:\exports\sent "
```
### Archive and rename while archiving (e.g. add `_sent` before extension)
``` powershell
. \ Send-FilesToSftp . ps1 -LocalPath " C:\exports " -RemotePath " /incoming " `
-HostName " sftp.example.com " -UserName " uploader " `
-ArchivePath " C:\exports\sent " `
-LocalRenamePattern '^(.+?)(\.[^.]+)$' -LocalRenameReplacement '${1}_sent${2}'
```
### Rename local file in place (no archive, no delete)
``` powershell
. \ Send-FilesToSftp . ps1 -LocalPath " C:\exports " -RemotePath " /incoming " `
-HostName " sftp.example.com " -UserName " uploader " `
-LocalRenamePattern '^' -LocalRenameReplacement 'done_'
```
> `-LocalRenamePattern`/`-LocalRenameReplacement` only rename the local file — the remote name is controlled by `-RenamePattern`/`-RenameReplacement`. You can use both together.
## Renaming Files on the Remote Side
2026-04-16 21:34:20 -05:00
Use `-RenamePattern` (regex) and `-RenameReplacement` together to rename files on the remote side without touching the local files.
| Goal | Pattern | Replacement |
|------|---------|-------------|
| Add date before extension | `'^(.+?)(\.[^.]+)$'` | `'${1}_20260416${2}'` |
| Add prefix | `'^'` | `'processed_'` |
| Add suffix before extension | `'^(.+?)(\.[^.]+)$'` | `'${1}_done${2}'` |
| Strip `_draft` from name | `'_draft'` | `''` |
| Replace spaces with underscores | `' '` | `'_'` |
Rename uses PowerShell's `-replace` operator (regex, case-insensitive). The local file is **not ** modified — only the remote destination path changes.
### Add today's date to every filename
``` powershell
$date = Get-Date -Format 'yyyyMMdd'
. \ Send-FilesToSftp . ps1 -LocalPath " C:\exports " -RemotePath " /incoming " `
-HostName " sftp.example.com " -UserName " uploader " `
-RenamePattern '^(.+?)(\.[^.]+)$' -RenameReplacement " ${date} _ `$ 1 `$ 2 "
```
> **Tip:** Use `-DryRun` first to preview the renamed remote paths before committing to the transfer.
2026-04-16 21:25:49 -05:00
## 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)
` ``