0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WinSCP】FTPS ディレクトリ同期バッチ例 (WinSCP スクリプト使用例 & .NETアセンブリ使用例)

Last updated at Posted at 2025-09-30

この記事は「リモート FTP サーバがパスワード認証である」「バッチでディレクトリ同期したいが同期前に変更をプレビューしたい」「パスワード平文保持は回避したい」を前提にしています。回避したほうが安全という保証はありません & 方針は組織に依ります。お気付きの点がありましたらご指摘いただけますと幸いです。

概要

Windows マシンとリモート FTP サーバをディレクトリ同期するには、WinSCP で FTP サーバに接続し、メニューの「コマンド」から「同期」を選択すれば可能です。ただし、WinSCP にはスクリプティングインタフェースもあり [文献1]、これを使ったバッチファイルを書けば叩くだけでファイル同期できて便利です [文献2]。

しかしこのバッチファイル方式では、1 回の接続で「1. どのファイルが同期されるかプレビューする」「2. それを確認した上で実際に同期する」を同時にできないです (※1)。そのため、リモート FTP サーバがパスワード認証だと 2 回パスワードを入力することになり、GUI よりかえって不便です (※2)

「スクリプトを叩くだけ」かつ「1 回以下のパスワード入力」で「プレビュー&同期」するには、以下のような方法があると思います。

  • 【ア】 (非推奨) WinSCP にパスワードを保存しておいてスクリプトから利用する [文献3]: これならパスワード入力は 0 回ですが、パスワード保存は非推奨であり、保存されたパスワードは (その Windows マシンにそのユーザとしてログインしていれば) 簡単に表示できます [文献4]。なのでマスターパスワード設定が必須とされますが [文献5]、それならマスターパスワードが訊かれるのではと思います (やっていないのでわかりません)。
  • 【イ】 プレビューだけ先に別の手段でやっておく: 例えば同期の目的が Web サイトのデプロイなら、ローカルファイルに対応する Web ページを curl して差分を取れば実質プレビューできると思います (アクセス制限がある場所は curl できないかもしれませんが)。
    • これを GitHub Actions に組み込めば便利そうです (GitHub Actions で続けて lftp でデプロイすればよいですが、今回はデプロイ前に変更をプレビューしたいとします)。
  • 【ウ】 PowerShell で受け取ったパスワードを暗号化して保持し再利用する: PowerShell にはユーザ入力を暗号化して保持する仕組みがあるので ( -AsSecureString [文献6])、これでパスワードを保持しておけばプレビューの接続にも同期の接続にも利用できます。
  • 【エ】 PowerShell から WinSCP .NET アセンブリをつかう: PowerShell をつかうなら、WinSCP .NET アセンブリ [文献7] の API を直接利用すれば 1 回の接続でプレビューと同期ができます (ただこの場合も API に渡すためにパスワードを暗号化して保持させます)。
  • 【オ】 暗号化したパスワードを設定ファイル保存しておいて PowerShell から利用する: 暗号化してあれば保存してもよいなら、パスワード入力を 0 回にできます [文献8]。


この記事では WinSCP スクリプティングインターフェースを利用したバッチファイル例 (パスワード入力が 2 回必要) と、上記の【エ】の PowerShell スクリプトの例を示します。


※1. これは [文献1] のコマンドにユーザ確認を待機できるものがないためです (待機せずプレビュー → 同期はできますが、これでは何の意味もない)。なお、接続先 FTP サーバに pause や sleep のようなコマンドがあれば call PAUSE などで待機する (またはユーザに中断を促す) ことができるかもしれないですが、今回はないものとします。
※2. 今回は安全上、バッチファイル (または別の設定ファイル) へのパスワードべた書きや、ユーザ入力から受け取ったパスワードの平文保持もしないものとします。また、外部パスワード管理アプリも使用しないものとします (結局アプリへのログインが生じそうなため)。

参考文献

  1. Scripting and Task Automation :: WinSCP
  2. WinSCPのコマンドでフォルダを同期するようにした | nameless name
  3. WinSCPスクリプト 及び実行 #winscp - Qiita
  4. 【Tips】【WinSCP】サーバーのパスワードを忘れた時は・・・ #SSH - Qiita
  5. Can I recover password stored in WinSCP session? :: WinSCP
  6. ConvertFrom-SecureString (Microsoft.PowerShell.Security) - PowerShell | Microsoft Learn
  7. WinSCP .NET Assembly and COM Library :: WinSCP
  8. Protecting credentials used for automation :: WinSCP

  • 以降のスクリプトは以下を前提としています。必要に応じて修正してください。
    • リモート FTP サーバに FTPS (Explicit) で接続します。通信プロトコルが異なる場合は修正してください ([文献1] や [文献7] を参照)。
    • スクリプトがあるディレクトリ以下の .\site\ をリモートの /home/{FTPアカウント}/www に送る同期をします。適宜修正してください。
    • お手元の WinSCP のパスが異なる場合はそれも修正してください。

WinSCP スクリプティングインターフェースのコマンドを利用したバッチファイル

以下の synchronize0.bat synchronize1.bat synchronize2.bat は WinSCP スクリプティングインターフェースを利用したバッチファイルの例です。FTP サーバに接続する度にパスワードを訊かれます (そのため、プレビュー時にも同期時にもパスワード入力が必要です)。

バッチファイルの ftps://{FTPアカウント}@{FTPサーバ名} の箇所を ftps://{FTPアカウント}:{パスワード}@{FTPサーバ名} にすればパスワードを訊かれなくなります。が、セキュリティ上、バッチファイルにパスワードを書き込むことは推奨されません。

synchronize0.bat ( 以下だとプレビューなので実際に同期するには -preview を除去して再実行する )
@echo off
"C:\Program Files (x86)\WinSCP\WinSCP.com" /log=%~dp0"log.txt" /ini=nul /command ^
  "open ftps://{FTPアカウント}@{FTPサーバ名} -explicit -rawsettings Utf=1" ^
  "synchronize remote "%~dp0"site /home/{FTPアカウント}/www -preview" ^
  "exit"
pause
synchronize1.bat ( プレビューするか即座に同期するかをユーザに確認するようにした版 )
@echo off
setlocal

set "ARG_PREVIEW=-preview"
set /p USER_INPUT="Sync right away? (y/n): "
if /i "%USER_INPUT%"=="y" set "ARG_PREVIEW="
echo ARG_PREVIEW is [%ARG_PREVIEW%]

"C:\Program Files (x86)\WinSCP\WinSCP.com" /log=%~dp0"log.txt" /ini=nul ^
  /command ^
    "open ftps://{FTPアカウント}@{FTPサーバ名} -explicit -rawsettings Utf=1" ^
    "synchronize remote "%~dp0"site /home/{FTPアカウント}/www "%ARG_PREVIEW% ^
    "exit"

pause
endlocal
synchronize2.bat ( プレビューしてから同期するのに便利なように無限ループする版 )
@echo off

:LOOP
call :SYNC
goto :LOOP

:SYNC
setlocal

set "ARG_PREVIEW=-preview"
set /p USER_INPUT="Sync right away? (y/n): "
if /i "%USER_INPUT%"=="y" set "ARG_PREVIEW="
echo ARG_PREVIEW is [%ARG_PREVIEW%]

"C:\Program Files (x86)\WinSCP\WinSCP.com" /log=%~dp0"log.txt" /ini=nul ^
  /command ^
    "open ftps://{FTPアカウント}@{FTPサーバ名} -explicit -rawsettings Utf=1" ^
    "synchronize remote "%~dp0"site /home/{FTPアカウント}/www "%ARG_PREVIEW% ^
    "exit"

endlocal

WinSCP .NET アセンブリを利用した PowerShell スクリプト

以下のスクリプトはユーザにパスワード入力を促した後、1 回の接続内でプレビューも同期もします (なお、PowerShell スクリプトはファイルを右クリックして「PowerShell で実行」をクリックするなどで実行できます)。

synchronize.ps1
<#
PowerShell: WinSCP を使って FTPS (explicit) で同期するスクリプト
#>

param(
  [string] $accountName = "{FTPアカウント}",
  [string] $HostName = "{FTPサーバ名}",
  [string] $LocalPath = (Join-Path $PSScriptRoot "site"),
  [string] $RemotePath = "/home/{FTPアカウント}/www",
  [string] $LogFile = (Join-Path $PSScriptRoot "log.txt")
)

# ----- WinSCP .NET アセンブリ読み込み -----
$assemblyPath = "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
if (-not (Test-Path $AssemblyPath)) { throw "WinSCPnet.dll not found: $AssemblyPath" }
Add-Type -Path $AssemblyPath

# ----- Credential 入力 -----
$pass = Read-Host "Enter password for $accountName" -AsSecureString  # パスワード入力を促す
$cred = New-Object System.Management.Automation.PSCredential($accountName, $pass)

# ----- セッションオプション設定 -----
$sessionOptions = New-Object WinSCP.SessionOptions -Property @{
  Protocol = [WinSCP.Protocol]::Ftp
  HostName = $HostName
  UserName = $cred.UserName
  FtpSecure = [WinSCP.FtpSecure]::Explicit
}

# ----- 同期の設定 -----
$direction = [WinSCP.SynchronizationMode]::Remote  # 同期の向き: ローカルからリモートに送る
$remove = $false  # 送り側にないファイルを削除するか: しない
$mirror = $false  # 送り側のタイムスタンプのほうが古いとき上書きするか: しない
$to = New-Object WinSCP.TransferOptions
$criteria = [WinSCP.SynchronizationCriteria]::Time

$session = New-Object WinSCP.Session
$session.SessionLogPath = $LogFile
try {
  # パスワードを取り出して接続
  $sessionOptions.Password = $cred.GetNetworkCredential().Password
  $session.Open($sessionOptions)
  $sessionOptions.Password = $null  # 接続したら削除
  $cred = $null  # 接続したら削除

  # --- 1) プレビュー ---
  $diffs = $session.CompareDirectories(
    $direction, $LocalPath, $RemotePath, $remove, $mirror, $criteria, $to
  )
  if ($diffs.Count -eq 0) { Write-Host "No changes."; return }
  Write-Host "Planned changes:"
  foreach ($d in $diffs) {
    $path = if ($d.Local) { $d.Local.FileName } else { $d.Remote.FileName }
    Write-Host ("  [{0}] {1}" -f $d.Action, $path)
  }

  # --- 2) 確認 ---
  $ans = Read-Host "Proceed with actual sync? [y/N]"
  if ($ans -notmatch '^(y|yes)$') { Write-Host "Aborted."; return }

  # --- 3) 同期 ---
  $result = $session.SynchronizeDirectories(
    $direction, $LocalPath, $RemotePath, $remove, $mirror, $criteria, $to
  )
  $result.Check()
  Write-Host "Synchronized:"
  foreach ($u in $result.Uploads) { Write-Host ("  [Upload] {0} -> {1}" -f $u.FileName, $u.Destination) }
  foreach ($d in $result.Downloads) { Write-Host ("  [Download] {0} -> {1}" -f $d.FileName, $d.Destination) }
  foreach ($r in $result.Removals) { Write-Host ("  [Remove] {0}" -f $r.FileName) }
} catch {
  Write-Error $_.Exception.Message
} finally {
  if ($session) { $session.Dispose() }
  Read-Host "Press Enter to exit"
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?