WinSCPでファイル移動を自動化|PowerShellとタスクスケジューラーの設定

WinSCPのファイル移動をタスクスケジューラーで自動化する場合は、サーバー内の移動とPCへのダウンロードを区別します。以下はSFTPサーバーの受信フォルダーから完了済みCSVを別フォルダーへ移し、失敗を終了コード1で通知する例です。

接続先・フォルダー・実行ユーザーを決めてから、検証用ファイルで手動実行を確認します。WinSCPが起動しただけでは移動成功と判断せず、ログと移動後のファイルを確認してください。

目次

移動・ダウンロード・同期の違い

目的WinSCPの処理対象
同じサーバー内の移動・名前変更MoveFile/CLIのmvリモート→リモート
PCへ取得GetFiles/CLIのgetリモート→ローカル
PCから送信PutFiles/CLIのputローカル→リモート
同期synchronize local/remote/bothlocalはリモートの変更をPCへ、remoteはPCの変更をリモートへ、bothは双方

synchronize remoteは双方向同期ではありません。また、同期は別世代を保存するバックアップとは異なります。ダウンロード後に元ファイルを消す運用なら、取得結果を確認してから削除する別設計が必要です。この記事の例はサーバー内の移動で、PCへコピーを作りません。

実行前に用意するもの

  • Windows PowerShell 5.1と、同じバージョンのWinSCPnet.dll・winscp.exe。Automationパッケージのルートにある.NET Framework版DLLを使う。
  • 例のC:\SftpJobとC:\SftpJob\logsを事前に作り、タスク実行ユーザーへ必要な読み取り・書き込み権限を付ける。
  • SFTP管理者へホスト名・利用者・接続先のSSHホスト鍵フィンガープリントを確認する。鍵を「*」等に置き換えて検証を省かない。
  • サーバー側に/inboxと/processedを用意し、対象アカウントで一覧取得・移動できることを確認する。
  • 送信側が完成時だけCSV名にする運用を決める。書き込み中は.tmp等を使い、拡張子だけで完了を判定しない。
  • この例では対象を直下の通常ファイル*.csvに限定し、下位フォルダーやシンボリックリンクを扱わない。

PowerShell 7を使う場合は.NET Standard版DLL等の構成が異なります。以下をそのまま別ランタイムへ流用せず、公式のDLL選択と実行ファイルの組み合わせを確認してください。

1.同じWindowsユーザーで資格情報を保存する

タスクを実行するWindowsユーザーで、同じPC上のWindows PowerShellを開いて次を実行します。入力するのはSFTPのユーザー名とパスワードです。WindowsではExport-Clixmlが資格情報をDPAPIで保護しますが、別のユーザーや別PCでそのまま復号できません。フォルダーのアクセス権も制限します。

Get-Credential | Export-Clixml -Path 'C:\SftpJob\sftp-credential.xml' -NoClobber

既にファイルがある場合は上書きせず止まります。更新時は実行を止め、承認された手順で入れ替えます。CLIXMLはWindows以外では同じ暗号化にならないため、この手順をmacOS・Linuxへ当てはめません。鍵認証が必要な環境では、別途SshPrivateKeyPath等を設定し、鍵とパスフレーズの運用を管理者と決めてください。

2.サーバー内のCSVを移動するPowerShell例

次をC:\SftpJob\move-csv.ps1へ保存します。ホスト名とフィンガープリントは仮の値です。DLL・実行ファイルは例ではC:\SftpJob\WinSCPへ置き、実際の配置に合わせます。移動先に同名があれば上書きせず失敗として止めます。

$ErrorActionPreference = 'Stop'
$exitStatus = 0
$session = $null
$jobFolder = 'C:\SftpJob'
$sourceDirectory = '/inbox'
$targetDirectory = '/processed'

try {
    if ($sourceDirectory -eq $targetDirectory) {
        throw '移動元と移動先は別のフォルダーを指定してください。'
    }
    $dllPath = Join-Path $jobFolder 'WinSCP\WinSCPnet.dll'
    $exePath = Join-Path $jobFolder 'WinSCP\winscp.exe'
    $logDirectory = Join-Path $jobFolder 'logs'
    if (-not (Test-Path -LiteralPath $logDirectory -PathType Container)) {
        throw 'logsフォルダーを事前に作成してください。'
    }
    foreach ($requiredFile in @($dllPath, $exePath)) {
        if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) {
            throw "必要なファイルがありません: $requiredFile"
        }
    }
    Add-Type -Path $dllPath
    $credential = Import-Clixml -LiteralPath (Join-Path $jobFolder 'sftp-credential.xml')
    if ($credential -isnot [System.Management.Automation.PSCredential]) {
        throw 'SFTPのPSCredentialを保存したファイルが必要です。'
    }
    $options = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Sftp
        HostName = 'sftp.example.com'
        UserName = $credential.UserName
        SecurePassword = $credential.Password
        SshHostKeyFingerprint = 'ssh-ed25519 255 SHA256:REPLACE_WITH_VERIFIED_KEY'
    }
    $session = New-Object WinSCP.Session
    $session.ExecutablePath = $exePath
    $runId = [guid]::NewGuid().ToString('N')
    $session.SessionLogPath = Join-Path $logDirectory ("winscp-$runId.log")
    $session.Open($options)
    if (-not $session.GetFileInfo($targetDirectory).IsDirectory) {
        throw '移動先は存在するディレクトリを指定してください。'
    }
    $movedCount = 0
    foreach ($entry in $session.ListDirectory($sourceDirectory).Files) {
        if ($entry.FileType -ne '-' -or $entry.Name -notlike '*.csv') { continue }
        $destination = $targetDirectory.TrimEnd('/') + '/' + $entry.Name
        if ($session.FileExists($destination)) {
            throw "同名の移動先が存在します: $destination"
        }
        $session.MoveFile($entry.FullName, $destination)
        $movedCount++
    }
    Write-Output "Moved: $movedCount"
}
catch {
    [Console]::Error.WriteLine($_.Exception.Message)
    $exitStatus = 1
}
finally {
    if ($null -ne $session) { $session.Dispose() }
}
exit $exitStatus

移動対象が0件ならこの例は正常終了0、接続・一覧・移動・同名衝突等の失敗は1で終了します。途中まで移して失敗した場合、済んだ移動は元に戻りません。ログと両フォルダーを確認してから再実行します。移動先の事前確認と実際の移動は一つの原子操作ではないため、別の処理が同時に同名ファイルを作らない運用も必要です。

サーバーの権限・chroot・ファイルシステム構成によって移動できる範囲は異なります。ルート権限を付けたり、フォルダーの権限を一律に広げたりして解決せず、必要な移動先を管理者へ確認します。接続前の失敗ではWinSCPログがまだ作られない場合もあります。

3.タスクスケジューラーへ登録する

項目この例の設定
操作プログラムの開始
プログラムC:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe(Windowsの場所に合わせる)
引数-NoProfile -NonInteractive -File “C:\SftpJob\move-csv.ps1”
開始(オプション)C:\SftpJob
実行ユーザーCLIXMLを作成した同じWindowsユーザー
トリガー開始時刻を決め、繰り返し間隔15分・必要な継続時間を設定
既にタスクが実行中の場合新しいインスタンスを開始しない

「ログオンしているかどうかにかかわらず実行」を使う場合は、非対話で接続できる条件を確認します。「パスワードを保存しない」はローカルリソースのみの条件に注意し、GUIで使うPageantやネットワークドライブがタスクでも利用できると考えないでください。管理者権限は必要な場合に限定し、実行ポリシーは組織の方針と署名等を確認します。

手動では動くのにタスクで失敗するとき

症状確認する点
DLLや実行ファイルが見つからない絶対パス、DLLの種類・バージョン、実行ユーザーの読み取り権限
資格情報を読み込めないCLIXML作成者とタスク実行ユーザー・PCが同じか
接続先の鍵の確認で止まる管理者から確認したフィンガープリントと接続先が合うか
移動が失敗する対象の完了状態、移動先の存在・同名衝突・サーバー権限
終了0だが対象がない空のフォルダー・対象拡張子・処理済みかを確認する
ログが増え続ける保存期間・容量・権限を決め、必要な証跡を残して整理する

CLIを使う場合、/commandへは各コマンドを別々の引数として渡します。「/command:」へコマンドを&で連結する形式ではありません。また、PowerShellの$Hostは組み込み変数なので接続先名の保存に使いません。今回の.NET例はそのコマンド文字列の組み立てを使わず、接続と移動をメソッドで呼び出します。

登録後に確認すること

  • まず検証用の完成済みCSVと同名衝突を用意し、期待する移動と失敗を確認する。
  • 手動実行後、同じ実行ユーザーのタスクから実行し、最終実行結果とWinSCPログを確認する。
  • ログオフ時と繰り返し時も確認し、対象0件を正常とする運用でよいか決める。
  • 移動をバックアップと扱わず、必要な保管・復元方法を別に用意する。

公式資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次