从多部分/表单数据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来做到这一点。
这是你需要的:
-
System.Net.Http.dll
- 包含在.NET 4.5中
- 对于.NET 4通过NuGet获取它
-
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中的文件。