Ver código fonte

Add local archive and rename-after-upload options

Blance 1 dia atrás
pai
commit
8505e75757
2 arquivos alterados com 145 adições e 13 exclusões
  1. 34 3
      README.md
  2. 111 10
      Send-FilesToSftp.ps1

+ 34 - 3
README.md

@@ -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
Send-FilesToSftp.ps1

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