【问题标题】:Upload multiple files from Powershell script从 Powershell 脚本上传多个文件
【发布时间】:2014-08-01 07:25:22
【问题描述】:

我有一个可以像这样处理 html 表单的 POSTing 的 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>

请注意,有两个 type="file" &lt;input&gt; 元素。

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

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

感谢您抽出宝贵时间。

【问题讨论】:

  • 升级到 Powershell 3 或更高版本并使用Invoke-WebRequest:technet.microsoft.com/en-us/library/hh849901.aspx。或者使用foreach 循环浏览您的文件。
  • 不幸的是,升级 powershell 不是一种选择,因为我们仍在使用 Windows XP。此外,我在链接的页面上找不到文件上传的提及,只有简单的表单名称-值对。我不明白使用 foreach 的建议。您是否建议对 WebClient.UploadFile() 进行两次单独的调用?那是行不通的,因为服务器端需要在同一个请求中包含两个文件的内容,并返回对它们进行一些处理的结果作为响应。
  • 如果您搜索“powershell post”,则会出现大量替代答案。

标签: http powershell file-upload multipartform-data


【解决方案1】:

我今天一直在使用 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 $_
    }
}

【讨论】:

  • 请注意,如果要传送二进制内容,边界字符串的随机性实际上并不那么重要。长度很重要。拥有足够长的乱码静态字符串可能与使用 GUID 一样可靠。
  • ISO-8859-1 仅在字节值 == 代码点值的情况下进行编码,您应该使用$enc = [System.Text.Encoding]::GetEncoding("ISO-8859-1")
  • @GuilhermeTorresCastro 所以 ASCII 仅表示 7 位 ASCII,ISO 8859 表示 8 位。好的。
  • 无论如何,我当前的工作代码与此有点不同。如果有人有问题,请告知。
  • 在 $LF 从“backtick n”切换到“backtick r backtick n”后为我工作
【解决方案2】:

我被这件事困扰着,还没有找到满意的解决方案。虽然这里提出的要点可以做到这一点,但在传输大文件的情况下效率不高。我写了一篇博客文章,提出了一个解决方案,我的 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 { }
}

干杯

【讨论】:

  • 在您的博客上至少发布一个解决方案的摘要会很有帮助,以防链接中断/更改,或者您的服务器将来出现故障。
  • 你完全正确。我添加了解决方案代码,希望够用了。
【解决方案3】:

在研究了 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()

【讨论】:

  • 您也可以只使用 'n 而不是 'r'n。此外,不需要 $ContentLength(或者如果它有用,您的代码还没有将它放在任何地方)。
  • @akauppi:我发现rn 是必需的。如果只使用 `n,我试图调用的 API 会遇到异常并返回 500 状态码。虽然在 Powershell 中只需一个换行符就可以使其在视觉上看起来不错,但回车和换行都是预期的。
  • 使用 "`r`n" 最终是我的解决方案 对于附加上下文:我使用 Invoke-RestMethod 开始了整个过程,并尝试使用 Form 参数传递多部分正文。这在我的情况下不起作用,因为 API 服务器不断返回 "Corrupt form data: no leading boundary"。我还尝试使用其他来源提出的MultipartFormDataContent 手动制作Body 参数无济于事:github.com/PowerShell/PowerShell/issues/…get-powershellblog.blogspot.com/2017/09/…
【解决方案4】:

我已将 @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!"
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-02-03
    • 2015-10-25
    • 2012-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-31
    相关资源
    最近更新 更多