Add local archive and rename-after-upload options

This commit is contained in:
2026-04-16 21:36:35 -05:00
orang tua 6a451044dd
melakukan 8505e75757
2 mengubah file dengan 145 tambahan dan 13 penghapusan
+111 -10
Melihat 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)"
}