Lectura de entrada de archivo desde una POST multipart / form-data

Estoy PUBLICANDO un archivo a un servicio WCF REST a través de un formulario HTML, con enctype configurado en multipart/form-data y un componente único: . La secuencia resultante que lee el servidor contiene lo siguiente:

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

El problema es que no estoy seguro de cómo extraer los bytes del archivo de la transmisión. Necesito hacer esto para escribir el archivo en el disco.

Puede echar un vistazo a la siguiente publicación de blog que ilustra una técnica que podría usarse para analizar multipart/form-data en el servidor utilizando el Analizador de 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); } } 

Otra posibilidad es habilitar la compatibilidad aspnet y usar HttpContext.Current.Request pero no es una forma muy WCFish.

Perdón por unirme tarde a la fiesta, pero hay una forma de hacerlo con la API pública de Microsoft .

Esto es lo que necesitas:

  1. System.Net.Http.dll
    • Incluido en .NET 4.5
    • Para .NET 4, obténgalo a través de NuGet
  2. System.Net.Http.Formatting.dll
    • Para .NET 4.5, obtén este paquete NuGet
    • Para .NET 4, obtén este paquete NuGet

Nota: Los paquetes Nuget vienen con más ensamblajes, pero al momento de escribir solo necesita lo anterior.

Una vez que haya hecho referencia a los ensamblados, el código puede verse así (usando .NET 4.5 por conveniencia):

 public static async Task ParseFiles( Stream data, string contentType, Action 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); } } } 

En cuanto al uso, supongamos que tiene el siguiente método WCF REST:

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

Podrías implementarlo así

 public void Upload(Stream data) { MultipartParser.ParseFiles( data, WebOperationContext.Current.IncomingRequest.ContentType, MyProcessMethod); } 

He tenido algunos problemas con el analizador que se basan en el análisis sintáctico de cadenas, particularmente con archivos de gran tamaño, encontré que se quedaría sin memoria y no se analizarían los datos binarios.

Para hacer frente a estos problemas, he abierto mi propio bash de un analizador de partes múltiples / datos de formulario C # aquí.

caracteristicas:

  • Maneja bien los archivos muy grandes. (Los datos se transmiten y transmiten durante la lectura)
  • Puede manejar múltiples cargas de archivos y detecta automáticamente si una sección es un archivo o no.
  • Devuelve archivos como una secuencia no como un byte [] (bueno para archivos grandes).
  • Documentación completa para la biblioteca, incluido un sitio web generado por MSDN.
  • Pruebas unitarias completas.

Restricciones

  • No maneja datos no multiparte.
  • El código es más complicado que el de Lorenzo

Simplemente use la clase MultipartFormDataParser de la siguiente manera:

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

En el contexto de un servicio WCF puede usarlo así:

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

O de esta manera (un poco más lento pero más amigable con el código):

 public ResponseClass MyMethod(Stream multipartData) { var parser = new MultipartFormDataParser(data, Encoding.UTF8); } 

La documentación también está disponible, cuando clones el repository simplemente HttpMultipartParserDocumentation/Help/index.html a HttpMultipartParserDocumentation/Help/index.html

He abierto un analizador de formularios C # Http aquí .

Esto es un poco más flexible que el otro que se menciona en CodePlex, ya que puede usarlo tanto para form-data Multipart como para otros formatos, y también le proporciona otros parámetros de formulario formateados en un objeto Dictionary .

Esto se puede usar de la siguiente manera:

no multiparte

 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"]); } } 

multiparte

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

Otra forma sería usar .Net parser para HttpRequest. Para hacer eso necesitas usar un poco de reflexión y una clase simple para WorkerRequest.

Primero crea una clase que se deriva de HttpWorkerRequest (para simplificar puedes usar 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); } } 

Luego, donde sea que tengas, crea un flujo de mensajes e instancia de esta clase. Lo estoy haciendo así en WCF Service:

 [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 ); 

Y luego crea HttpRequest usando activador y constructor no público

 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"); 

Después de eso, en r.Files , tendrás archivos de tu transmisión.

El tipo que resolvió esto lo publicó como LGPL y no tiene permitido modificarlo. Ni siquiera hice clic en eso cuando lo vi. Aquí está mi versión. Esto necesita ser probado. Probablemente hay errores. Por favor publique cualquier actualización. Sin garantía. Puede modificar esto todo lo que quiera, llámalo tuyo, imprímelo en un pedazo de papel y úsalo para chatarra, … no importa.

 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 _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 Files { get { return _files ?? (_files = new Dictionary()); } 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(); } } } 

Implementé el paquete MultipartReader NuGet para ASP.NET 4 para leer datos de formulario multiparte. Se basa en el analizador de datos de formularios multiparte , pero admite más de un archivo.

¿Qué tal algunos Regex?

Escribí esto para un texto de un archivo, pero creo que esto podría funcionar para usted

(En caso de que su archivo de texto contenga una línea que comienza exactamente con los “emparejados” a continuación, simplemente adapte su Regex)

  private static List 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 lstLines = new List(); 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; } 

He tratado WCF con carga de archivo grande (serveral GB) donde almacenar datos en la memoria no es una opción. Mi solución es almacenar el flujo de mensajes en un archivo temporal y usar buscar para descubrir el comienzo y el final de los datos binarios.