PowerShellでFTPしたい

この時代に FTP を使うことになったため、不本意ながら PowerShell でそれらしいものを作りました。
`FtpWebRequest` は公式にも新規開発には使うなと記載されている点には留意してください。
エラーハンドリングはしていませんので、外側で `$ErrorActionPreference = "Stop"` して `try catch` してください。
リソース管理はちょっと気にしたけど十分ではないかもしれない。

# 実装

```powershell
function New-FtpRequest {
    param(
        [parameter(Mandatory=$True)]$ip, 
        [parameter(Mandatory=$True)]$user, 
        [parameter(Mandatory=$True)]$password, 
        [parameter(Mandatory=$True)]$Path
    )
    $req = [System.Net.FtpWebRequest]::Create("ftp://${ip}${Path}")
    $req.Credentials = New-Object System.Net.NetworkCredential($user, $password)
    return $req
}
function Invoke-FtpOpen {
    param(
        [parameter(Mandatory=$True)]$Connection
    )
    $req = New-FtpRequest @Connection -Path "/"
    $req.Method = [System.Net.WebRequestMethods+ftp]::PrintWorkingDirectory
    $req.keepalive = $True
    $res = $req.GetResponse()
    return $res.WelcomeMessage
}
function Invoke-FtpPut {
    param(
        [parameter(Mandatory=$True)]$Connection, 
        [parameter(Mandatory=$True)]$File,
        [parameter(Mandatory=$False)]$Path = "/"
    )
    $File = Get-Item $File
    if ($Path -match "/$") {
        $Path = $Path + $File.Name
    }
    $req = New-FtpRequest @Connection -Path $Path
    $req.Method = [System.Net.WebRequestMethods+ftp]::UploadFile
    $reqStream = $req.GetRequestStream()

    $fileStream = [system.io.file]::OpenRead($File.FullName)

    $fileStream.CopyTo($reqStream)

    $fileStream.Dispose()
    $reqStream.Dispose()

    $res = $req.GetResponse()
    return $res.StatusDescription
}

function Invoke-FtpDir {
    param(
        [parameter(Mandatory=$True)]$Connection, 
        [parameter(Mandatory=$False)]$Path = "/",
        [switch]$Detail
    )
    $req = New-FtpRequest @Connection -Path $Path
    if ($Detail) {
        $req.Method = [System.Net.WebRequestMethods+ftp]::ListDirectoryDetails
    }else{
        $req.Method = [System.Net.WebRequestMethods+ftp]::ListDirectory
    }
    $res = $req.GetResponse()
    $sr = New-Object System.IO.StreamReader($res.GetResponseStream())
    $ret = $sr.ReadToEnd()
    $sr.Dispose()
    return $ret
}

function Invoke-FtpMkdir {
    param(
        [parameter(Mandatory=$True)]$Connection, 
        [parameter(Mandatory=$True)]$Path
    )
    $req = New-FtpRequest @Connection -Path $Path
    $req.Method = [System.Net.WebRequestMethods+ftp]::MakeDirectory
    $res = $req.GetResponse()
    return $res.StatusDescription
}
function Invoke-FtpRmdir {
    param(
        [parameter(Mandatory=$True)]$Connection, 
        [parameter(Mandatory=$True)]$Path
    )
    $req = New-FtpRequest @Connection -Path $Path
    $req.Method = [System.Net.WebRequestMethods+ftp]::RemoveDirectory
    $res = $req.GetResponse()
    return $res.StatusDescription
}
function Invoke-FtpDel {
    param(
        [parameter(Mandatory=$True)]$Connection, 
        [parameter(Mandatory=$True)]$Path
    )
    $req = New-FtpRequest @Connection -Path $Path
    $req.Method = [System.Net.WebRequestMethods+ftp]::DeleteFile
    $res = $req.GetResponse()
    return $res.StatusDescription
}

function Invoke-FtpGet {
    param(
        [parameter(Mandatory=$True)]$Connection,
        [parameter(Mandatory=$True)]$Path,
        [parameter(Mandatory=$False)]$DestinationPath = "."
    )

    if (Test-Path $DestinationPath -PathType Container) {
        $FileName = $Path -replace '.*/',''
        $DestinationPath = Resolve-Path $DestinationPath
        $DestinationPath = Join-Path $DestinationPath $File
    }
    $req = New-FtpRequest @Connection -Path $Path
    $req.Method = [System.Net.WebRequestMethods+ftp]::DownloadFile

    $res = $req.GetResponse()
    $resStream = $res.GetResponseStream()
    $fs = New-Object system.io.filestream($DestinationPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
    $resStream.CopyTo($fs)
    $fs.Dispose()
    $resStream.Dispose()
    return $res.StatusDescription
}

function Invoke-FtpBye {
    param([parameter(Mandatory=$True)]$Connection)
    $req = New-FtpRequest @Connection -Path "/"
    $req.Method = [System.Net.WebRequestMethods+ftp]::PrintWorkingDirectory
    $req.keepalive = $False
    $res = $req.GetResponse()
    return $res.ExitMessage
}
```

# 使い方

```PowerShell
# 接続先情報をハッシュに詰め込みます
$con = @{
    ip = "192.168.1.26"
    user = "user"
    password = "password"
}

# 適当にテスト用のファイルを作ります
echo test > a.txt

# テスト用のファイルを PUT します
Invoke-FtpOpen  $con
Invoke-FtpDir   $con -Path "/"
Invoke-FtpMkdir $con -Path "/hoge/"
Invoke-FtpPut   $con -Path "/hoge/" -File a.txt
Invoke-FtpDir   $con -Path "/hoge/" -Detail

# テスト用のファイルを削除して GET します
rm a.txt
Invoke-FtpGet   $con -Path "/hoge/a.txt"
get-item a.txt

# 後片付け
Invoke-FtpDel   $con -Path "/hoge/a.txt"
Invoke-FtpRmdir $con -Path "/hoge"
Invoke-FtpBye   $con
rm a.txt
```

# おわりに

リモートサーバ上のファイルサイズや更新日付を取得することもできるらしいのですが、伝統的な FTP の表示に寄せるデザインの才能が無かったので省略しました。テストもいい加減です。

参考にさせて頂きました。

https://dobon.net/vb/dotnet/internet/ftpwebrequest.html