Add local archive and rename-after-upload options

This commit is contained in:
2026-04-16 21:36:35 -05:00
parent 6a451044dd
commit 8505e75757
2 changed files with 145 additions and 13 deletions
+34 -3
View File
@@ -42,10 +42,13 @@ The script auto-searches these locations in order:
| `-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 before upload |
| `-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 |
| `-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` |
@@ -127,7 +130,35 @@ The `-FileFilter` parameter uses PowerShell regex (case-insensitive by default).
-Recurse -FileFilter '\.pdf$'
```
## Renaming Files Before Upload
## 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.
+111 -10
View File
@@ -52,8 +52,22 @@
Replacement string for the -RenamePattern match. Supports regex capture groups ($1, ${1}, etc.).
Use '$0' to reference the full match.
.PARAMETER ArchivePath
Move successfully uploaded files to this local folder instead of leaving them in place.
The folder will be created if it does not exist.
Cannot be combined with -DeleteAfterTransfer.
.PARAMETER LocalRenamePattern
Regex pattern to match in the filename when renaming the local file after a successful upload.
Must be used with -LocalRenameReplacement.
Works standalone (rename in place) or with -ArchivePath (rename while archiving).
.PARAMETER LocalRenameReplacement
Replacement string for -LocalRenamePattern. Supports regex capture groups ($1, ${1}, etc.).
.PARAMETER DeleteAfterTransfer
Delete local files after successful upload (move behavior).
Cannot be combined with -ArchivePath.
.PARAMETER DryRun
Show what would be transferred without actually uploading.
@@ -81,6 +95,25 @@
-HostName "sftp.example.com" -UserName "uploader" `
-RenamePattern '^' -RenameReplacement 'processed_'
.EXAMPLE
# Archive files to a local folder after upload
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
-HostName "sftp.example.com" -UserName "uploader" `
-ArchivePath "C:\exports\sent"
.EXAMPLE
# Archive and rename locally (append date) after upload
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
-HostName "sftp.example.com" -UserName "uploader" `
-ArchivePath "C:\exports\sent" `
-LocalRenamePattern '^(.+?)(\.[^.]+)$' -LocalRenameReplacement "`$1_sent`$2"
.EXAMPLE
# Rename local file in place after upload (no archive)
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
-HostName "sftp.example.com" -UserName "uploader" `
-LocalRenamePattern '^' -LocalRenameReplacement 'done_'
.EXAMPLE
# Unattended with saved credentials and move behavior
.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
@@ -131,6 +164,12 @@ param(
[string]$RenameReplacement,
[string]$ArchivePath,
[string]$LocalRenamePattern,
[string]$LocalRenameReplacement,
[switch]$Recurse,
[switch]$DeleteAfterTransfer,
@@ -194,18 +233,34 @@ function Find-WinScpDll {
# ── Main ─────────────────────────────────────────────────────────────────────
try {
# ── Validate rename parameters ───────────────────────────────────────
# ── Validate parameters ──────────────────────────────────────────────
if (($RenamePattern -and -not $RenameReplacement) -or ($RenameReplacement -and -not $RenamePattern)) {
Write-Log "-RenamePattern and -RenameReplacement must be used together." -Level ERROR
exit 1
}
if (($LocalRenamePattern -and -not $LocalRenameReplacement) -or ($LocalRenameReplacement -and -not $LocalRenamePattern)) {
Write-Log "-LocalRenamePattern and -LocalRenameReplacement must be used together." -Level ERROR
exit 1
}
if ($ArchivePath -and $DeleteAfterTransfer) {
Write-Log "-ArchivePath and -DeleteAfterTransfer cannot be used together." -Level ERROR
exit 1
}
# ── Create archive folder if needed ─────────────────────────────────
if ($ArchivePath -and -not (Test-Path $ArchivePath)) {
New-Item -ItemType Directory -Path $ArchivePath -Force | Out-Null
Write-Log "Created archive folder: $ArchivePath"
}
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 ($RenamePattern) { Write-Log "Rename : '$RenamePattern' → '$RenameReplacement'" }
if ($RenamePattern) { Write-Log "Remote rename : '$RenamePattern' → '$RenameReplacement'" }
if ($LocalRenamePattern) { Write-Log "Local rename : '$LocalRenamePattern' → '$LocalRenameReplacement'" }
if ($ArchivePath) { Write-Log "Archive to : $ArchivePath" }
if ($DryRun) { Write-Log "*** DRY RUN MODE - No files will be transferred ***" -Level WARN }
# ── Find and load WinSCP ─────────────────────────────────────────────
@@ -260,13 +315,26 @@ try {
if ($DryRun) {
Write-Log "Files that would be transferred:" -Level INFO
foreach ($f in $allFiles) {
$destName = if ($RenamePattern) { $f.Name -replace $RenamePattern, $RenameReplacement } else { $f.Name }
$relativePath = $f.FullName.Substring($LocalPath.TrimEnd('\').Length)
$remoteDir = ($RemotePath.TrimEnd('/') + ($f.DirectoryName.Substring($LocalPath.TrimEnd('\').Length) -replace '\\', '/'))
$remoteDest = "$remoteDir/$destName"
$sizeKB = [math]::Round($f.Length / 1KB, 1)
$renameNote = if ($RenamePattern -and $destName -ne $f.Name) { " [renamed from $($f.Name)]" } else { '' }
Write-Log " $($f.FullName)$remoteDest (${sizeKB} KB)$renameNote"
$destName = if ($RenamePattern) { $f.Name -replace $RenamePattern, $RenameReplacement } else { $f.Name }
$localFinalName = if ($LocalRenamePattern) { $f.Name -replace $LocalRenamePattern, $LocalRenameReplacement } else { $f.Name }
$remoteDir = $RemotePath.TrimEnd('/') + ($f.DirectoryName.Substring($LocalPath.TrimEnd('\').Length) -replace '\\', '/')
$remoteDest = "$remoteDir/$destName"
$sizeKB = [math]::Round($f.Length / 1KB, 1)
$remoteRenameNote = if ($RenamePattern -and $destName -ne $f.Name) { " [remote name: $destName]" } else { '' }
Write-Log " UPLOAD : $($f.FullName)$remoteDest (${sizeKB} KB)$remoteRenameNote"
if ($DeleteAfterTransfer) {
Write-Log " LOCAL : DELETE $($f.FullName)"
}
elseif ($ArchivePath) {
$archiveDest = Join-Path $ArchivePath $localFinalName
$localNote = if ($LocalRenamePattern -and $localFinalName -ne $f.Name) { " [renamed from $($f.Name)]" } else { '' }
Write-Log " LOCAL : MOVE → $archiveDest$localNote"
}
elseif ($LocalRenamePattern -and $localFinalName -ne $f.Name) {
$renameDest = Join-Path $f.DirectoryName $localFinalName
Write-Log " LOCAL : RENAME → $renameDest"
}
}
Write-Log "DRY RUN complete. $($allFiles.Count) file(s) would be transferred." -Level SUCCESS
exit 0
@@ -347,12 +415,42 @@ try {
$transferOptions = New-Object WinSCP.TransferOptions
$transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
$result = $session.PutFiles($file.FullName, $targetPath, $DeleteAfterTransfer, $transferOptions)
# Always upload without auto-delete; handle local disposition ourselves
$result = $session.PutFiles($file.FullName, $targetPath, $false, $transferOptions)
$result.Check()
$sizeKB = [math]::Round($file.Length / 1KB, 1)
Write-Log "Transferred: $($file.Name)$targetPath (${sizeKB} KB)" -Level SUCCESS
$successCount++
# ── Local post-transfer disposition ──────────────────────
$localFinalName = if ($LocalRenamePattern) { $file.Name -replace $LocalRenamePattern, $LocalRenameReplacement } else { $file.Name }
if ($DeleteAfterTransfer) {
Remove-Item -LiteralPath $file.FullName -Force
Write-Log "Deleted local: $($file.FullName)"
}
elseif ($ArchivePath) {
# Preserve subdirectory structure when recursing
$archiveDir = if ($Recurse -and $relativePath) {
$sub = $file.DirectoryName.Substring($LocalPath.TrimEnd('\').Length).TrimStart('\')
Join-Path $ArchivePath $sub
} else { $ArchivePath }
if (-not (Test-Path $archiveDir)) {
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
}
$archiveDest = Join-Path $archiveDir $localFinalName
Move-Item -LiteralPath $file.FullName -Destination $archiveDest -Force
$archiveNote = if ($localFinalName -ne $file.Name) { " (renamed from $($file.Name))" } else { '' }
Write-Log "Archived: $($file.FullName)$archiveDest$archiveNote"
}
elseif ($LocalRenamePattern -and $localFinalName -ne $file.Name) {
$renameDest = Join-Path $file.DirectoryName $localFinalName
Rename-Item -LiteralPath $file.FullName -NewName $localFinalName -Force
Write-Log "Renamed local: $($file.Name)$localFinalName"
}
}
catch {
Write-Log "FAILED: $($file.Name)$($_.Exception.Message)" -Level ERROR
@@ -373,6 +471,9 @@ try {
if ($DeleteAfterTransfer) {
Write-Log " Mode : MOVE (source files deleted on success)"
}
elseif ($ArchivePath) {
Write-Log " Mode : ARCHIVE → $ArchivePath"
}
else {
Write-Log " Mode : COPY (source files retained)"
}