web-dev-qa-db-ja.com

Powershellスクリプトから複数のファイルをアップロードする

次のようなhtmlフォームのPOSTを処理できるWebアプリケーションがあります。

<form action="x" method="post" enctype="multipart/form-data">
  <input name="xfa" type="file">
  <input name="pdf" type="file">
  <input type="submit" value="Submit">
</form>

2つのtype="file"<input>要素があることに注意してください。

PowershellスクリプトからこれをPOSTするスクリプトを作成するにはどうすればよいですか?これを実行して、サービスの簡単なテストフレームワークを作成する予定です。

WebClient.UploadFile() が見つかりましたが、処理できるのは1つのファイルのみです。

お時間を割いていただきありがとうございます。

12

今日はPowerShellを使用してマルチパートHTTP POSTを作成しています。以下のコードがお役に立てば幸いです。

  • PowerShell自体はマルチパートフォームのアップロードを実行できません
  • それについてのサンプルも多くありません。 thisthis に基づいてコードを作成しました。
  • 承知しました、 Invoke-RestMethodにはPowerShell3.0が必要ですが、上記のリンクの後半のコードは、HTTP POST .NETで直接実行する方法を示しています。これにより、これをWindowsで実行できますXP同様に。

幸運を!動作したかどうか教えてください。

function Send-Results {
    param (
        [parameter(Mandatory=$True,Position=1)] [ValidateScript({ Test-Path -PathType Leaf $_ })] [String] $ResultFilePath,
        [parameter(Mandatory=$True,Position=2)] [System.URI] $ResultURL
    )
    $fileBin = [IO.File]::ReadAllBytes($ResultFilePath)
    $computer= $env:COMPUTERNAME

    # Convert byte-array to string (without changing anything)
    #
    $enc = [System.Text.Encoding]::GetEncoding("iso-8859-1")
    $fileEnc = $enc.GetString($fileBin)

    <#
    # PowerShell does not (yet) have built-in support for making 'multipart' (i.e. binary file upload compatible)
    # form uploads. So we have to craft one...
    #
    # This is doing similar to: 
    # $ curl -i -F "[email protected]" -F "computer=MYPC" http://url
    #
    # Boundary is anything that is guaranteed not to exist in the sent data (i.e. string long enough)
    #    
    # Note: The protocol is very precise about getting the number of line feeds correct (both CRLF or LF work).
    #>
    $boundary = [System.Guid]::NewGuid().ToString()    # 

    $LF = "`n"
    $bodyLines = (
        "--$boundary",
        "Content-Disposition: form-data; name=`"file`"$LF",   # filename= is optional
        $fileEnc,
        "--$boundary",
        "Content-Disposition: form-data; name=`"computer`"$LF",
        $computer,
        "--$boundary--$LF"
        ) -join $LF

    try {
        # Returns the response gotten from the server (we pass it on).
        #
        Invoke-RestMethod -Uri $URL -Method Post -ContentType "multipart/form-data; boundary=`"$boundary`"" -TimeoutSec 20 -Body $bodyLines
    }
    catch [System.Net.WebException] {
        Write-Error( "FAILED to reach '$URL': $_" )
        throw $_
    }
}
23
akauppi

私はこのことに悩まされ、満足のいく解決策を見つけられませんでした。ここで提案されている要点はyobを実行できますが、大きなファイルを送信する場合は効率的ではありません。 .NET 4.5に存在するHttpClientクラスに基づいて、その解決策を提案するブログ投稿を作成しました。それが問題にならない場合は、次のアドレスで私の解決策を確認できます http://blog.majcica.com/2016/01/13/powershell-tips-and-tricks-multipartform-data-リクエスト/

編集:

function Invoke-MultipartFormDataUpload
{
    [CmdletBinding()]
    PARAM
    (
        [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$InFile,
        [string]$ContentType,
        [Uri][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Uri,
        [System.Management.Automation.PSCredential]$Credential
    )
    BEGIN
    {
        if (-not (Test-Path $InFile))
        {
            $errorMessage = ("File {0} missing or unable to read." -f $InFile)
            $exception =  New-Object System.Exception $errorMessage
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'MultipartFormDataUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $InFile
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }

        if (-not $ContentType)
        {
            Add-Type -AssemblyName System.Web

            $mimeType = [System.Web.MimeMapping]::GetMimeMapping($InFile)

            if ($mimeType)
            {
                $ContentType = $mimeType
            }
            else
            {
                $ContentType = "application/octet-stream"
            }
        }
    }
    PROCESS
    {
        Add-Type -AssemblyName System.Net.Http

        $httpClientHandler = New-Object System.Net.Http.HttpClientHandler

        if ($Credential)
        {
            $networkCredential = New-Object System.Net.NetworkCredential @($Credential.UserName, $Credential.Password)
            $httpClientHandler.Credentials = $networkCredential
        }

        $httpClient = New-Object System.Net.Http.Httpclient $httpClientHandler

        $packageFileStream = New-Object System.IO.FileStream @($InFile, [System.IO.FileMode]::Open)

        $contentDispositionHeaderValue = New-Object System.Net.Http.Headers.ContentDispositionHeaderValue "form-data"
        $contentDispositionHeaderValue.Name = "fileData"
        $contentDispositionHeaderValue.FileName = (Split-Path $InFile -leaf)

        $streamContent = New-Object System.Net.Http.StreamContent $packageFileStream
        $streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue
        $streamContent.Headers.ContentType = New-Object System.Net.Http.Headers.MediaTypeHeaderValue $ContentType

        $content = New-Object System.Net.Http.MultipartFormDataContent
        $content.Add($streamContent)

        try
        {
            $response = $httpClient.PostAsync($Uri, $content).Result

            if (!$response.IsSuccessStatusCode)
            {
                $responseBody = $response.Content.ReadAsStringAsync().Result
                $errorMessage = "Status code {0}. Reason {1}. Server reported the following message: {2}." -f $response.StatusCode, $response.ReasonPhrase, $responseBody

                throw [System.Net.Http.HttpRequestException] $errorMessage
            }

            $responseBody = [xml]$response.Content.ReadAsStringAsync().Result

            return $responseBody
        }
        catch [Exception]
        {
            $PSCmdlet.ThrowTerminatingError($_)
        }
        finally
        {
            if($null -ne $httpClient)
            {
                $httpClient.Dispose()
            }

            if($null -ne $response)
            {
                $response.Dispose()
            }
        }
    }
    END { }
}

乾杯

4
Mario Majcica

@ akauppiの回答 をより一般的なソリューションであるコマンドレットにリミックスしました。

  • ファイルをアップロードするためにGet-ChildItemからパイプライン入力を受け取ることができます
  • 位置パラメータとしてURLを取ります
  • 辞書を位置パラメータとして受け取り、追加のフォームデータとして送信します
  • (オプションの)-Credentialパラメーターを取ります
  • (オプションの)-FilesKeyパラメーターを使用して、ファイルアップロード部分のformdataキーを指定します
  • -WhatIfをサポート
  • -Verboseロギングがあります
  • 何か問題が発生した場合、エラーで終了します

これは次のように呼び出すことができます:

$url ="http://localhost:12345/home/upload"
$form = @{ description = "Test 123." }
$pwd = ConvertTo-SecureString "s3cr3t" -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential ("john", $pwd)

Get-ChildItem *.txt | Send-MultiPartFormToApi $url $form $creds -Verbose -WhatIf

完全なコマンドレットのコードは次のとおりです。

function Send-MultiPartFormToApi {
    # Attribution: [@akauppi's post](https://stackoverflow.com/a/25083745/419956)
    # Remixed in: [@jeroen's post](https://stackoverflow.com/a/41343705/419956)
    [CmdletBinding(SupportsShouldProcess = $true)] 
    param (
        [Parameter(Position = 0)]
        [string]
        $Uri,

        [Parameter(Position = 1)]
        [HashTable]
        $FormEntries,

        [Parameter(Position = 2, Mandatory = $false)]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter(
            ParameterSetName = "FilePath",
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias("Path")]
        [string[]]
        $FilePath,

        [Parameter()]
        [string]
        $FilesKey = "files"
    );

    begin {
        $LF = "`n"
        $boundary = [System.Guid]::NewGuid().ToString()

        Write-Verbose "Setting up body with boundary $boundary"

        $bodyArray = @()

        foreach ($key in $FormEntries.Keys) {
            $bodyArray += "--$boundary"
            $bodyArray += "Content-Disposition: form-data; name=`"$key`""
            $bodyArray += ""
            $bodyArray += $FormEntries.Item($key)
        }

        Write-Verbose "------ Composed multipart form (excl files) -----"
        Write-Verbose ""
        foreach($x in $bodyArray) { Write-Verbose "> $x"; }
        Write-Verbose ""
        Write-Verbose "------ ------------------------------------ -----"

        $i = 0
    }

    process {
        $fileName = (Split-Path -Path $FilePath -Leaf)

        Write-Verbose "Processing $fileName"

        $fileBytes = [IO.File]::ReadAllBytes($FilePath)
        $fileDataAsString = ([System.Text.Encoding]::GetEncoding("iso-8859-1")).GetString($fileBytes)

        $bodyArray += "--$boundary"
        $bodyArray += "Content-Disposition: form-data; name=`"$FilesKey[$i]`"; filename=`"$fileName`""
        $bodyArray += "Content-Type: application/x-msdownload"
        $bodyArray += ""
        $bodyArray += $fileDataAsString

        $i += 1
    }

    end {
        Write-Verbose "Finalizing and invoking rest method after adding $i file(s)."

        if ($i -eq 0) { throw "No files were provided from pipeline." }

        $bodyArray += "--$boundary--"

        $bodyLines = $bodyArray -join $LF

        # $bodyLines | Out-File data.txt # Uncomment for extra debugging...

        try {
            if (!$WhatIfPreference) {
                Invoke-RestMethod `
                    -Uri $Uri `
                    -Method Post `
                    -ContentType "multipart/form-data; boundary=`"$boundary`"" `
                    -Credential $Credential `
                    -Body $bodyLines
            } else {
                Write-Host "WHAT IF: Would've posted to $Uri body of length " + $bodyLines.Length
            }
        } catch [Exception] {
            throw $_ # Terminate CmdLet on this situation.
        }

        Write-Verbose "Finished!"
    }
}
3
Jeroen

Multipart/form-dataがどのように構築されるかを研究した後、問題の解決策を見つけました。多くの助けが http://www.paraesthesia.com/archive/2009/12/16/posting-multipartform-data-using-.net-webrequest.aspx の形でもたらされました。

解決策は、その規則に従って手動でリクエストの本文を作成することです。正しいContent-Lengthsなどの優れた点を残しました。

これが私が今使っているものの抜粋です:

    $path = "/Some/path/to/data/"

    $boundary_id = Get-Date -Format yyyyMMddhhmmssfffffff
    $boundary = "------------------------------" + $boundary_id

    $url = "http://..."
    [System.Net.HttpWebRequest] $req = [System.Net.WebRequest]::create($url)
    $req.Method = "POST"
    $req.ContentType = "multipart/form-data; boundary=$boundary"
    $ContentLength = 0
    $req.TimeOut = 50000

    $reqst = $req.getRequestStream()

    <#
    Any time you write a file to the request stream (for upload), you'll write:
        Two dashes.
        Your boundary.
        One CRLF (\r\n).
        A content-disposition header that tells the name of the form field corresponding to the file and the name of the file. That looks like:
        Content-Disposition: form-data; name="yourformfieldname"; filename="somefile.jpg" 
        One CRLF.
        A content-type header that says what the MIME type of the file is. That looks like:
        Content-Type: image/jpg
        Two CRLFs.
        The entire contents of the file, byte for byte. It's OK to include binary content here. Don't base-64 encode it or anything, just stream it on in.
        One CRLF.
    #>

    <# Upload #1: XFA #> 
    $xfabuffer = [System.IO.File]::ReadAllBytes("$path\P7-T.xml")

    <# part-header #>
    $header = "--$boundary`r`nContent-Disposition: form-data; name=`"xfa`"; filename=`"xfa`"`r`nContent-Type: text/xml`r`n`r`n"
    $buffer = [Text.Encoding]::ascii.getbytes($header)        
    $reqst.write($buffer, 0, $buffer.length)
    $ContentLength = $ContentLength + $buffer.length

    <# part-data #>
    $reqst.write($xfabuffer, 0, $xfabuffer.length)
    $ContentLength = $ContentLength + $xfabuffer.length

    <# part-separator "One CRLF" #>
    $terminal = "`r`n"
    $buffer = [Text.Encoding]::ascii.getbytes($terminal)        
    $reqst.write($buffer, 0, $buffer.length)
    $ContentLength = $ContentLength + $buffer.length

    <# Upload #1: PDF template #>
    $pdfbuffer = [System.IO.File]::ReadAllBytes("$path\P7-T.pdf")

    <# part-header #>
    $header = "--$boundary`r`nContent-Disposition: form-data; name=`"pdf`"; filename=`"pdf`"`r`nContent-Type: application/pdf`r`n`r`n"
    $buffer = [Text.Encoding]::ascii.getbytes($header)        
    $reqst.write($buffer, 0, $buffer.length)
    $ContentLength = $ContentLength + $buffer.length

    <# part-data #>
    $reqst.write($pdfbuffer, 0, $pdfbuffer.length)
    $ContentLength = $ContentLength + $pdfbuffer.length

    <# part-separator "One CRLF" #>
    $terminal = "`r`n"
    $buffer = [Text.Encoding]::ascii.getbytes($terminal)        
    $reqst.write($buffer, 0, $buffer.length)
    $ContentLength = $ContentLength + $buffer.length

    <#
    At the end of your request, after writing all of your fields and files to the request, you'll write:

    Two dashes.
    Your boundary.
    Two more dashes.
    #>
    $terminal = "--$boundary--"
    $buffer = [Text.Encoding]::ascii.getbytes($terminal)        
    $reqst.write($buffer, 0, $buffer.length)
    $ContentLength = $ContentLength + $buffer.length

    $reqst.flush()
    $reqst.close()

    # Dump request to console
    #$req

    [net.httpWebResponse] $res = $req.getResponse()

    # Dump result to console
    #$res

    # Dump result-body to filesystem
<#    
    $resst = $res.getResponseStream()
    $sr = New-Object IO.StreamReader($resst)
    $result = $sr.ReadToEnd()
    $res.close()
#>

    $null = New-Item -ItemType Directory -Force -Path "$path\result"
    $target = "$path\result\P7-T.pdf"

    # Create a stream to write to the file system.
    $targetfile = [System.IO.File]::Create($target)

    # Create the buffer for copying data.
    $buffer = New-Object Byte[] 1024

    # Get a reference to the response stream (System.IO.Stream).
    $resst = $res.GetResponseStream()

    # In an iteration...
    Do {
        # ...attemt to read one kilobyte of data from the web response stream.
        $read = $resst.Read($buffer, 0, $buffer.Length)

        # Write the just-read bytes to the target file.
        $targetfile.Write($buffer, 0, $read)

        # Iterate while there's still data on the web response stream.
    } While ($read -gt 0)

    # Close the stream.
    $resst.Close()
    $resst.Dispose()

    # Flush and close the writer.
    $targetfile.Flush()
    $targetfile.Close()
    $targetfile.Dispose()
1