Files
filestosftp/README.md
T

249 lines
10 KiB
Markdown

# 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 |
| `-RenamePattern` | No | — | Regex pattern to match in filename for renaming on the **remote** side before upload |
| `-RenameReplacement` | No | — | Replacement string for `-RenamePattern` (supports capture groups like `$1`) |
| `-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`) |
| `-Recurse` | No | `false` | Scan subdirectories |
| `-DeleteAfterTransfer` | No | `false` | Delete local files after successful upload. Cannot combine with `-ArchivePath` |
| `-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$'
```
## 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
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.
## 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)
```