从 multipart/form-data POST 读取文件输入

Posted

技术标签:

【中文标题】从 multipart/form-data POST 读取文件输入【英文标题】:Reading file input from a multipart/form-data POST 【发布时间】:2011-11-19 13:54:17 【问题描述】:

我通过 html 表单将文件发布到 WCF REST 服务,enctype 设置为 multipart/form-data 和单个组件:<input type="file" name="data">。服务器读取的结果流包含以下内容:

------WebKitFormBoundary
Content-Disposition: form-data; name="data"; filename="DSCF0001.JPG"
Content-Type: image/jpeg

<file bytes>
------WebKitFormBoundary--

问题是我不确定如何从流中提取文件字节。我需要这样做才能将文件写入磁盘。

【问题讨论】:

WCF service to accept a post encoded multipart/form-data的可能重复 @Darin:我不确定。我的服务已经接受了来自 POST 的 multipart/form-data,但是我想要读取传入的流并提取文件字节。 我在使用表单数据 ***.com/questions/39853604/…987654322@ 上传图片时仍然面临问题 【参考方案1】:

很抱歉迟到了,但有一种方法可以使用 Microsoft 公共 API

这是你需要的:

    System.Net.Http.dll 包含在 .NET 4.5 中 对于 .NET 4,通过 NuGet 获取 System.Net.Http.Formatting.dll 对于 .NET 4.5 获取 this NuGet package 对于 .NET 4 获取 this NuGet package

注意 Nuget 包带有更多的程序集,但在撰写本文时,您只需要上述内容。

一旦您引用了程序集,代码可能如下所示(为方便起见,使用 .NET 4.5):

public static async Task ParseFiles(
    Stream data, string contentType, Action<string, Stream> fileProcessor)

    var streamContent = new StreamContent(data);
    streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);

    var provider = await streamContent.ReadAsMultipartAsync();

    foreach (var httpContent in provider.Contents)
    
        var fileName = httpContent.Headers.ContentDisposition.FileName;
        if (string.IsNullOrWhiteSpace(fileName))
        
            continue;
        

        using (Stream fileContents = await httpContent.ReadAsStreamAsync())
        
            fileProcessor(fileName, fileContents);
        
    

至于用法,假设您有以下 WCF REST 方法:

[OperationContract]
[WebInvoke(Method = WebRequestMethods.Http.Post, UriTemplate = "/Upload")]
void Upload(Stream data);

你可以这样实现

public void Upload(Stream data)

    MultipartParser.ParseFiles(
           data, 
           WebOperationContext.Current.IncomingRequest.ContentType, 
           MyProcessMethod);

【讨论】:

这个方法会创建与 IIS 的依赖关系吗? 看来您也需要将操作设为异步任务?有没有办法在不让整个链异步的情况下使用异步方法? @GerardONeill 当您希望同步时,您始终可以执行 Task.Wait()。例如:ParseFiles(data, contentType, fileProcessor).Wait()。不知道你为什么要这样做...... 通过这种方式,我可以将此功能插入现有代码,而无需更改整个方法链。在我的特定地点,我只是在学习所有这些(从解析多部分数据开始),如果我不必学习如何做 12 件事就可以帮助我学习。 我忘了说谢谢 ;)。我只是让您知道 await 运算符的问题,尤其是对于新手。【参考方案2】:

您可以查看following blog post,它说明了一种可用于在服务器上使用Multipart Parser 解析multipart/form-data 的技术:

public void Upload(Stream stream)

    MultipartParser parser = new MultipartParser(stream);
    if (parser.Success)
    
        // Save the file
        SaveFile(parser.Filename, parser.ContentType, parser.FileContents);
    

另一种可能性是启用aspnet compatibility 并使用HttpContext.Current.Request,但这不是WCFish 的方式。

【讨论】:

使用aspnet兼容性有什么问题? @rafale,问题是明天您决定将 WCF 服务托管在 IIS 以外的其他东西(如 Windows 服务或其他)中,您将不得不重写它。除此之外没有错。 我忘了提到我的 WCF 服务作为托管 Windows 服务托管在 IIS 之外。我猜这意味着我无法使用 aspnet 兼容性,对吗? File.WriteAllBytes(filepath, parser.FileContents) 是我想做的(将文件保存在磁盘上) 多部分解析器是 LGPL。 @Lorenzo Polidori 的作品除了更完整之外,还获得了 MIT 许可(更宽松)。【参考方案3】:

我遇到了一些基于字符串解析的解析器问题,特别是对于大文件,我发现它会耗尽内存并且无法解析二进制数据。

为了解决这些问题,我开源了自己尝试的 C# multipart/form-data 解析器here

特点:

可以很好地处理非常大的文件。 (数据在读取时流入流出) 可以处理多个文件上传并自动检测一个部分是否是一个文件。 将文件作为流而不是字节[]返回(适用于大文件)。 库的完整文档,包括 MSDN 样式生成的网站。 完整的单元测试。

限制:

不处理非多部分数据。 代码比 Lorenzo 的更复杂

只需像这样使用 MultipartFormDataParser 类:

Stream data = GetTheStream();

// Boundary is auto-detected but can also be specified.
var parser = new MultipartFormDataParser(data, Encoding.UTF8);

// The stream is parsed, if it failed it will throw an exception. Now we can use
// your data!

// The key of these maps corresponds to the name field in your
// form
string username = parser.Parameters["username"].Data;
string password = parser.Parameters["password"].Data

// Single file access:
var file = parser.Files.First();
string filename = file.FileName;
Stream data = file.Data;

// Multi-file access
foreach(var f in parser.Files)

    // Do stuff with each file.

在 WCF 服务的上下文中,您可以像这样使用它:

public ResponseClass MyMethod(Stream multipartData)

    // First we need to get the boundary from the header, this is sent
    // with the HTTP request. We can do that in WCF using the WebOperationConext:
    var type = WebOperationContext.Current.IncomingRequest.Headers["Content-Type"];

    // Now we want to strip the boundary out of the Content-Type, currently the string
    // looks like: "multipart/form-data; boundary=---------------------124123qase124"
    var boundary = type.Substring(type.IndexOf('=')+1);

    // Now that we've got the boundary we can parse our multipart and use it as normal
    var parser = new MultipartFormDataParser(data, boundary, Encoding.UTF8);

    ...

或者像这样(稍微慢一些但对代码更友好):

public ResponseClass MyMethod(Stream multipartData)

    var parser = new MultipartFormDataParser(data, Encoding.UTF8);

文档也可用,当您克隆存储库时,只需导航到 HttpMultipartParserDocumentation/Help/index.html

【讨论】:

终于工作了,我真的很感谢你的出色工作,非常感谢@Jake Woods。 值得注意的是,这个解析器也可以通过 Nuget 获得:HttpMultipartParser。 (以防有人像我一样,只是按 ctrl+f 寻找 nuget :))【参考方案4】:

我开源了一个 C# Http 表单解析器here。

这比 CodePlex 上提到的另一个稍微灵活一些,因为您可以将它用于 Multipart 和非 Multipart form-data,并且它还为您提供了格式化为 Dictionary 对象的其他表单参数。

可以这样使用:

非多部分

public void Login(Stream stream)

    string username = null;
    string password = null;

    HttpContentParser parser = new HttpContentParser(stream);
    if (parser.Success)
    
        username = HttpUtility.UrlDecode(parser.Parameters["username"]);
        password = HttpUtility.UrlDecode(parser.Parameters["password"]);
    

多部分

public void Upload(Stream stream)

    HttpMultipartParser parser = new HttpMultipartParser(stream, "image");

    if (parser.Success)
    
        string user = HttpUtility.UrlDecode(parser.Parameters["user"]);
        string title = HttpUtility.UrlDecode(parser.Parameters["title"]);

        // Save the file somewhere
        File.WriteAllBytes(FILE_PATH + title + FILE_EXT, parser.FileContents);
    

【讨论】:

这消耗了多少堆:byte[] data = Misc.ToByteArray(stream);字符串内容 = encoding.GetString(data); 嗨,解析时我看到解析值中的内容类型:( @Lorenzo 我没有得到确切的值,它也与标题结合在一起 文件解析器不工作。这个库工作github.com/Http-Multipart-Data-Parser/…。【参考方案5】:

另一种方法是对 HttpRequest 使用 .Net 解析器。为此,您需要为 WorkerRequest 使用一些反射和简单的类。

首先创建派生自 HttpWorkerRequest 的类(为简单起见,您可以使用 SimpleWorkerRequest):

public class MyWorkerRequest : SimpleWorkerRequest

    private readonly string _size;
    private readonly Stream _data;
    private string _contentType;

    public MyWorkerRequest(Stream data, string size, string contentType)
        : base("/app", @"c:\", "aa", "", null)
    
        _size = size ?? data.Length.ToString(CultureInfo.InvariantCulture);
        _data = data;
        _contentType = contentType;
    

    public override string GetKnownRequestHeader(int index)
    
        switch (index)
        
            case (int)HttpRequestHeader.ContentLength:
                return _size;
            case (int)HttpRequestHeader.ContentType:
                return _contentType;
        
        return base.GetKnownRequestHeader(index);
    

    public override int ReadEntityBody(byte[] buffer, int offset, int size)
    
        return _data.Read(buffer, offset, size);
    

    public override int ReadEntityBody(byte[] buffer, int size)
    
        return ReadEntityBody(buffer, 0, size);
    

然后,无论您在哪里创建消息流并创建此类的实例。我在 WCF 服务中这样做:

[WebInvoke(Method = "POST",
               ResponseFormat = WebMessageFormat.Json,
               BodyStyle = WebMessageBodyStyle.Bare)]
    public string Upload(Stream data)
    
        HttpWorkerRequest workerRequest =
            new MyWorkerRequest(data,
                                WebOperationContext.Current.IncomingRequest.ContentLength.
                                    ToString(CultureInfo.InvariantCulture),
                                WebOperationContext.Current.IncomingRequest.ContentType
                );

然后使用激活器和非公共构造函数创建 HttpRequest

var r = (HttpRequest)Activator.CreateInstance(
            typeof(HttpRequest),
            BindingFlags.Instance | BindingFlags.NonPublic,
            null,
            new object[]
                
                    workerRequest,
                    new HttpContext(workerRequest)
                ,
            null);

var runtimeField = typeof (HttpRuntime).GetField("_theRuntime", BindingFlags.Static | BindingFlags.NonPublic);
if (runtimeField == null)

    return;


var runtime = (HttpRuntime) runtimeField.GetValue(null);
if (runtime == null)

    return;


var codeGenDirField = typeof(HttpRuntime).GetField("_codegenDir", BindingFlags.Instance | BindingFlags.NonPublic);
if (codeGenDirField == null)

    return;


codeGenDirField.SetValue(runtime, @"C:\MultipartTemp");

r.Files 之后,您将获得流中的文件。

【讨论】:

只要 .NET 更新不阻止它工作,这是迄今为止最好的解决方案(ASP.NET 可能比现有的开源实现更健壮和可靠,和它们一样好)可能是)。 我已经用一些必要的代码编辑了你的答案(否则某些文件可能会崩溃) 你能多说一下哪些文件会导致它崩溃吗?并返回您放在那里的返回是否意味着我们无法创建请求或者可能只是 r.Files 会引发异常? 解析器对大文件进行了优化,将它们存储在磁盘上。我已经为这个优化工作填充了必要的字段。您可以通过 app.config 中的 requestLengthDiskThreshold 配置元素控制瘦阈值。您可能想要更改的另一个配置元素是maxrequestlength。 return 语句也可能是异常,这意味着我们的反射失败了,我们很可能在大文件上失败。【参考方案6】:

解决此问题的人将其发布为 LGPL,您不得对其进行修改。当我看到它时,我什至没有点击它。 这是我的版本。这需要测试。可能有错误。请发布任何更新。没有保修。你可以随心所欲地修改它,称它为你自己的,将它打印在一张纸上,然后将它用于狗舍废料,......不要在意。

using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace DigitalBoundaryGroup

    class HttpNameValueCollection
    
        public class File
        
            private string _fileName;
            public string FileName  get  return _fileName ?? (_fileName = "");  set  _fileName = value;  

            private string _fileData;
            public string FileData  get  return _fileData ?? (_fileName = "");  set  _fileData = value;  

            private string _contentType;
            public string ContentType  get  return _contentType ?? (_contentType = "");  set  _contentType = value;  
        

        private NameValueCollection _post;
        private Dictionary<string, File> _files;
        private readonly HttpListenerContext _ctx;

        public NameValueCollection Post  get  return _post ?? (_post = new NameValueCollection());  set  _post = value;  
        public NameValueCollection Get  get  return _ctx.Request.QueryString;  
        public Dictionary<string, File> Files  get  return _files ?? (_files = new Dictionary<string, File>());  set  _files = value;  

        private void PopulatePostMultiPart(string post_string)
        
            var boundary_index = _ctx.Request.ContentType.IndexOf("boundary=") + 9;
            var boundary = _ctx.Request.ContentType.Substring(boundary_index, _ctx.Request.ContentType.Length - boundary_index);

            var upper_bound = post_string.Length - 4;

            if (post_string.Substring(2, boundary.Length) != boundary)
                throw (new InvalidDataException());

            var current_string = new StringBuilder();

            for (var x = 4 + boundary.Length; x < upper_bound; ++x)
            
                if (post_string.Substring(x, boundary.Length) == boundary)
                
                    x += boundary.Length + 1;

                    var post_variable_string = current_string.Remove(current_string.Length - 4, 4).ToString();

                    var end_of_header = post_variable_string.IndexOf("\r\n\r\n");

                    if (end_of_header == -1) throw (new InvalidDataException());

                    var filename_index = post_variable_string.IndexOf("filename=\"", 0, end_of_header);
                    var filename_starts = filename_index + 10;
                    var content_type_starts = post_variable_string.IndexOf("Content-Type: ", 0, end_of_header) + 14;
                    var name_starts = post_variable_string.IndexOf("name=\"") + 6;
                    var data_starts = end_of_header + 4;

                    if (filename_index != -1)
                    
                        var filename = post_variable_string.Substring(filename_starts, post_variable_string.IndexOf("\"", filename_starts) - filename_starts);
                        var content_type = post_variable_string.Substring(content_type_starts, post_variable_string.IndexOf("\r\n", content_type_starts) - content_type_starts);
                        var file_data = post_variable_string.Substring(data_starts, post_variable_string.Length - data_starts);
                        var name = post_variable_string.Substring(name_starts, post_variable_string.IndexOf("\"", name_starts) - name_starts);
                        Files.Add(name, new File()  FileName = filename, ContentType = content_type, FileData = file_data );
                    
                    else
                    
                        var name = post_variable_string.Substring(name_starts, post_variable_string.IndexOf("\"", name_starts) - name_starts);
                        var value = post_variable_string.Substring(data_starts, post_variable_string.Length - data_starts);
                        Post.Add(name, value);
                    

                    current_string.Clear();
                    continue;
                

                current_string.Append(post_string[x]);
            
        

        private void PopulatePost()
        
            if (_ctx.Request.HttpMethod != "POST" || _ctx.Request.ContentType == null) return;

            var post_string = new StreamReader(_ctx.Request.InputStream, _ctx.Request.ContentEncoding).ReadToEnd();

            if (_ctx.Request.ContentType.StartsWith("multipart/form-data"))
                PopulatePostMultiPart(post_string);
            else
                Post = HttpUtility.ParseQueryString(post_string);

        

        public HttpNameValueCollection(ref HttpListenerContext ctx)
        
            _ctx = ctx;
            PopulatePost();
        


    

【讨论】:

用法:var values = new HttpNameValueCollection(ref httplistenercontext); Console.WriteLine(values.Post["username"]); file.Write(values.Files["file"]); 我的是 MIT 许可证,所以你可以随意修改。【参考方案7】:

我已经为 ASP.NET 4 实现了MultipartReader NuGet 包,用于读取多部分表单数据。它基于Multipart Form Data Parser,但它支持多个文件。

【讨论】:

nuget 库页面上指向您的项目站点的链接已损坏【参考方案8】:

一些正则表达式怎么样?

我为文本文件写了这个,但我相信这对你有用

(如果您的文本文件包含以下面“匹配”的行开头的行 - 只需调整您的正则表达式)

    private static List<string> fileUploadRequestParser(Stream stream)
    
        //-----------------------------111111111111111
        //Content-Disposition: form-data; name="file"; filename="data.txt"
        //Content-Type: text/plain
        //...
        //...
        //-----------------------------111111111111111
        //Content-Disposition: form-data; name="submit"
        //Submit
        //-----------------------------111111111111111--

        List<String> lstLines = new List<string>();
        TextReader textReader = new StreamReader(stream);
        string sLine = textReader.ReadLine();
        Regex regex = new Regex("(^-+)|(^content-)|(^$)|(^submit)", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);

        while (sLine != null)
        
            if (!regex.Match(sLine).Success)
            
                lstLines.Add(sLine);
            
            sLine = textReader.ReadLine();
        

        return lstLines;
    

【讨论】:

【参考方案9】:

我已经处理了 WCF 处理大文件(几 GB)上传,其中无法将数据存储在内存中。我的解决方案是将消息流存储到临时文件中,并使用 seek 找出二进制数据的开始和结束。

【讨论】:

应该是评论

以上是关于从 multipart/form-data POST 读取文件输入的主要内容,如果未能解决你的问题,请参考以下文章

使用 multipart/form-data 时如何从请求中获取字符串参数? [复制]

从 multipart/form-data POST 读取文件输入

从 multipart/form-data 接收带有 servicestack 的文件

使用 multipart/form-data 将图像从移动应用程序上传到 API

使用 $http 发送带角度的 multipart/form-data 文件

Jersey API 中的 multipart/form-data 出现 CORS 错误