从Powershell脚本上传多个文件

时间:2014-08-01 07:25:23

标签: http powershell file-upload multipartform-data

我有一个web应用程序,可以处理这样的html表单的POST:

<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>

请注意,有两个type="file" <input>元素。

如何从Powershell脚本编写POST脚本?我计划这样做,为服务创建一个简单的测试框架。

我找到了WebClient.UploadFile(),但这只能处理一个文件。

感谢您抽出宝贵时间。

4 个答案:

答案 0 :(得分:23)

我今天一直在使用PowerShell制作多部分HTTP POST。我希望下面的代码对您有所帮助。

  • PowerShell本身无法执行多部分表单上传。
  • 关于它的样本也不多。我根据thisthis构建了代码。
  • 当然,Invoke-RestMethod需要PowerShell 3.0,但上面链接中后面的代码显示了如何直接使用.NET进行HTTP POST,允许您在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 "file=@file.any" -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 $_
    }
}

答案 1 :(得分:4)

我对此事感到困扰,并没有找到满意的解决方案。虽然这里提出的要点可以做yob,但是在大文件传输的情况下效率不高。我写了一篇博客文章,提出了一个解决方案,将我的cmdlet基于.NET 4.5中的HttpClient类。如果这不是您的问题,您可以在以下地址http://blog.majcica.com/2016/01/13/powershell-tips-and-tricks-multipartform-data-requests/

查看我的解决方案

编辑:

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 { }
}

干杯

答案 2 :(得分:3)

我已将@akauppi's answer重新混合到更通用的解决方案中,该cmdlet:

  • 可以从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

以下是完整cmdlet的代码:

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 :(得分:1)

在研究了如何构建multipart / form-data之后,我找到了解决问题的方法。很多帮助以http://www.paraesthesia.com/archive/2009/12/16/posting-multipartform-data-using-.net-webrequest.aspx的形式出现。

然后,解决方案是根据该约定手动构建请求的主体。我留下了像正确的内容长度等的细节。

以下是我现在使用的摘录:

    $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()