从多部分/表单数据POST中读取文件input

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

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

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

您可以查看以下博客文章 ,其中阐述了一种可用于使用Multipart Parser在服务器上parsingmultipart/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兼容性,并使用HttpContext.Current.Request但这不是一个非常WCFish的方式。

很抱歉join晚会,但有一种方法可以用Microsoft公共API来做到这一点。

这是你需要的:

  1. System.Net.Http.dll
    • 包含在.NET 4.5中
    • 对于.NET 4通过NuGet获取它
  2. System.Net.Http.Formatting.dll
    • 对于.NET 4.5获得这个NuGet包
    • 对于.NET 4,得到这个NuGet包

注意 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); } 

我有parsing器的一些问题,基于stringparsing,特别是对于大型文件,我发现它将耗尽内存,无法parsing二进制数据。

为了处理这些问题,我在这里打开了一个C#multipart / form-dataparsing器的尝试

特征:

  • 处理非常大的文件。 (数据在读取时stream入并stream出)
  • 可以处理多个file upload,并自动检测一个部分是否是文件。
  • 以文件streamforms返回文件而不是byte [](适用于大文件)。
  • 包含MSDN样式生成的网站的库的完整文档。
  • 完整的unit testing。

限制:

  • 不处理非多部分数据。
  • 然后代码更复杂洛伦佐的

只需使用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

我在这里开源了一个C#Http表单parsing器。

这比在CodePlex上提到的另外一个稍微灵活一些,因为您可以将它用于Multipart和Non-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); } } 

解决这个问题的人把它公布为LGPL,你不能修改它。 当我看到的时候我甚至没有点击它。 这是我的版本。 这需要testing。 有可能是错误。 请张贴任何更新。 没有保修。 你可以修改这一切你想要的,称之为你自己的,在一张纸上打印出来,并用它作为狗窝废料,…不在乎。

 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(); } } } 

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

我已经处理WCF与大文件(serveral GB)上传在内存中的存储数据不是一个选项。 我的解决scheme是将消息stream存储到临时文件,并使用seek来查找二进制数据的开始和结束。

那么一些正则expression式呢?

我写了一个文件的文件,但我相信这可以为你工作

(如果你的文本文件包含的行正好与下面的“匹配”的行开始 – 只需调整你的正则expression式)

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

另一种方法是使用.Netparsing器来处理HttpRequest。 要做到这一点,你需要对WorkerRequest使用一些reflection和简单的类。

首先创build派生自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); } } 

然后,无论你有什么消息stream创build和这个类的实例。 我在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 ); 

然后使用激活和非公共构造函数创buildHttpRequest

 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你将有你的stream中的文件。