|
|
@@ -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)"
|
|
|
}
|