wake-up-neo.net

Dateieingabe aus einem Multipart/Formulardaten lesen POST

Ich POSTET eine Datei über ein HTML-Formular an einen WCF REST Dienst, wobei enctype auf multipart/form-data und eine einzelne Komponente eingestellt ist: <input type="file" name="data">. Der resultierende Stream, der vom Server gelesen wird, enthält Folgendes:

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

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

Das Problem ist, dass ich nicht sicher bin, wie die Dateibytes aus dem Stream extrahiert werden. Ich muss das tun, um die Datei auf die Festplatte zu schreiben.

38
rafale

Sie können einen Blick auf den folgenden Blogbeitrag werfen, der eine Methode veranschaulicht, mit der multipart/form-data auf dem Server mithilfe des Multipart Parser analysiert werden kann:

public void Upload(Stream stream)
{
    MultipartParser parser = new MultipartParser(stream);
    if (parser.Success)
    {
        // Save the file
        SaveFile(parser.Filename, parser.ContentType, parser.FileContents);
    }
}

Eine andere Möglichkeit besteht darin, die Kompatibilität mit aspnet zu aktivieren und HttpContext.Current.Request zu verwenden. Dies ist jedoch keine sehr einfache WCFish-Methode.

29
Darin Dimitrov

Es tut uns leid, dass Sie der Partei zu spät beigetreten sind, aber es gibt eine Möglichkeit, dies mit Microsoft public API zu tun.

Hier ist was Sie brauchen:

  1. System.Net.Http.dll
    • In .NET 4.5 enthalten
    • Für .NET 4 erhalten Sie es über NuGet
  2. System.Net.Http.Formatting.dll

Note Die Nuget-Pakete enthalten mehr Assemblys, aber zum Zeitpunkt des Schreibens benötigen Sie nur die oben genannten. 

Nachdem Sie die Assemblys referenziert haben, kann der Code folgendermaßen aussehen (mit .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);
        }
    }
}

Angenommen, Sie haben die folgende WCF-Methode REST:

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

Sie könnten es so umsetzen

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

Ich hatte einige Probleme mit Parser, die auf dem String-Parsing basieren, insbesondere bei großen Dateien, bei denen festgestellt wurde, dass der Speicher ausgeht und die binären Daten nicht analysiert werden können. 

Um mit diesen Problemen fertig zu werden, habe ich meinen eigenen Versuch eines multipartigen C #/Formulardaten-Parsers ausfindig gemacht. Hier

Eigenschaften:

  • Behandelt sehr große Dateien gut. (Daten werden während des Lesens gestreamt und gestreamt)
  • Kann mehrere Datei-Uploads verarbeiten und erkennt automatisch, ob ein Abschnitt eine Datei ist oder nicht.
  • Gibt Dateien als Stream zurück und nicht als Byte [] (gut für große Dateien).
  • Vollständige Dokumentation für die Bibliothek, einschließlich einer im MSDN-Stil erstellten Website.
  • Vollständige Unit-Tests.

Beschränkungen:

  • Verarbeitet keine nicht mehrteiligen Daten. 
  • Code ist komplizierter als bei Lorenzo

Verwenden Sie einfach die MultipartFormDataParser-Klasse wie folgt:

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

Im Zusammenhang mit einem WCF-Dienst können Sie es folgendermaßen verwenden:

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

    ...
}

Oder so (etwas langsamer, aber Code-freundlicher):

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

Dokumentation ist auch verfügbar, wenn Sie das Repository klonen, navigieren Sie einfach zu HttpMultipartParserDocumentation/Help/index.html.

23
Jake Woods

Ich habe einen C # HTTP-Form-Parser hier - in der Open-Source-Umgebung.

Dies ist etwas flexibler als das andere, das in CodePlex erwähnt wird, da Sie es sowohl für Multipart- als auch für Nicht-Multipart-form-data verwenden können. Außerdem erhalten Sie andere Formularparameter, die in einem Dictionary-Objekt formatiert sind.

Dies kann wie folgt verwendet werden:

nicht multipart

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

multipart

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

Eine andere Möglichkeit wäre die Verwendung von .Net-Parser für HttpRequest. Dazu müssen Sie ein wenig Reflexion und eine einfache Klasse für WorkerRequest verwenden. 

Erstellen Sie zunächst eine Klasse, die von HttpWorkerRequest abgeleitet ist (zur Vereinfachung können Sie SimpleWorkerRequest verwenden):

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

Dann, wo immer Sie Ihren Nachrichtenstrom haben, erstellen Sie eine Instanz dieser Klasse. Ich mache das so in 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
                );

Erstellen Sie anschließend HttpRequest mit Aktivator und einem nicht öffentlichen Konstruktor

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

Danach haben Sie in r.Files Dateien aus Ihrem Stream. 

2
Lukasz Salamon

Wie wäre es mit Regex?

Ich habe für einen Text eine Datei geschrieben, aber ich glaube, das könnte für Sie funktionieren

(Falls Ihre Textdatei eine Zeile enthält, die genau mit den "übereinstimmenden" Zeilen beginnt, passen Sie einfach Ihre Regex an.)

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

Ich habe das MultipartReader NuGet-Paket für ASP.NET 4 zum Lesen von mehrteiligen Formulardaten implementiert. Es basiert auf Multipart Form Data Parser , unterstützt jedoch mehr als eine Datei.

1
Václav Dajbych

Der Kerl, der das Problem gelöst hat, hat es als LGPL gepostet, und Sie dürfen es nicht ändern. Ich habe nicht einmal darauf geklickt, als ich das gesehen habe ... Hier ist meine Version. Dies muss getestet werden. Es gibt wahrscheinlich Fehler. Bitte poste alle Updates. Keine Garantie. Sie können dies beliebig ändern, es selbst nennen, auf ein Blatt Papier drucken und für Zwinger-Schrott verwenden.

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


    }
}
1
Bluebaron

Ich habe WCF mit großen Dateien (mehrere GB) hochgeladen, bei denen das Speichern von Daten im Speicher keine Option ist. Meine Lösung besteht darin, den Nachrichtenstrom in einer temporären Datei zu speichern und mit find den Anfang und das Ende von Binärdaten zu ermitteln.

0
Yang Zhang