web-dev-qa-db-fra.com

Lecture d'un fichier saisi à partir d'une donnée multipart/form POST

Je poste un fichier sur un service WCF REST via un formulaire HTML, avec enctype défini sur multipart/form-data et un seul composant: <input type="file" name="data">. Le flux résultant en cours de lecture par le serveur contient les éléments suivants:

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

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

Le problème est que je ne sais pas comment extraire les octets du fichier à partir du flux. Je dois le faire pour écrire le fichier sur le disque.

38
rafale

Vous pouvez consulter l'article suivant sur le blog , qui illustre une technique permettant d'analyser multipart/form-data sur le serveur à l'aide de l'analyseur Multipart :

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

Une autre possibilité est d'activer la compatibilité aspnet et d'utiliser HttpContext.Current.Request, mais ce n'est pas une méthode très WCFish.

29
Darin Dimitrov

Désolé de vous être joint à la soirée en retard, mais il existe un moyen de le faire avec API publique Microsoft.

Voici ce dont vous avez besoin:

  1. System.Net.Http.dll
    • Inclus dans .NET 4.5
    • Pour .NET 4, obtenez-le via NuGet
  2. System.Net.Http.Formatting.dll

Note Les paquets Nuget sont livrés avec plus d'assemblages, mais au moment de l'écriture, vous n'avez besoin que de ce qui précède. 

Une fois que vous avez référencé les assemblys, le code peut ressembler à ceci (en utilisant .NET 4.5 pour plus de commodité):

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

En ce qui concerne l'utilisation, supposons que vous utilisiez la méthode WCF REST suivante:

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

Vous pouvez le mettre en œuvre comme si

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

J'ai eu quelques problèmes avec l'analyseur basé sur l'analyse des chaînes, en particulier avec les gros fichiers. 

Pour faire face à ces problèmes, j'ai ouvert la source de ma propre tentative d'un analyseur C # multipart/form-data ici

Caractéristiques:

  • Gère bien les très gros fichiers. (Les données sont diffusées et extraites en cours de lecture)
  • Peut gérer plusieurs téléchargements de fichiers et détecte automatiquement si une section est un fichier ou non.
  • Renvoie les fichiers sous forme de flux et non sous forme d'octet [] (convient aux fichiers volumineux).
  • Documentation complète pour la bibliothèque, y compris un site Web généré à la manière de MSDN.
  • Tests unitaires complets.

Restrictions

  • Ne gère pas les données non multiparties. 
  • Le code est plus compliqué que celui de Lorenzo

Utilisez simplement la classe MultipartFormDataParser comme ceci:

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

Dans le contexte d'un service WCF, vous pouvez l'utiliser comme ceci:

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

    ...
}

Ou comme ceci (légèrement plus lent mais plus favorable au code):

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

La documentation est également disponible. Lorsque vous clonez le référentiel, accédez simplement à HttpMultipartParserDocumentation/Help/index.html.

23
Jake Woods

J'ai ouvert-source un analyseur de formulaire C # Http ici .

Ceci est légèrement plus flexible que l’autre mentionné qui est sur CodePlex, puisque vous pouvez l’utiliser pour form-data multipart et non multipart, et vous donne également d’autres paramètres de formulaire formatés dans un objet Dictionary.

Ceci peut être utilisé comme suit:

non-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
Lorenzo Polidori

Une autre solution consiste à utiliser un analyseur .Net pour HttpRequest. Pour ce faire, vous devez utiliser un peu de réflexion et une classe simple pour WorkerRequest. 

Commencez par créer une classe dérivée de HttpWorkerRequest (pour simplifier, vous pouvez utiliser 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);
    }
}

Ensuite, où que vous soyez, vous créez un flux de messages et une instance de cette classe. Je le fais comme ça dans 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
                );

Et créez ensuite HttpRequest à l'aide d'un activateur et d'un constructeur non public

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

Après cela, dans r.Files, vous aurez des fichiers de votre flux. 

2
Lukasz Salamon

Que diriez-vous de Regex?

J'ai écrit ceci pour un texte dans un fichier, mais je crois que cela pourrait fonctionner pour vous

(Si votre fichier texte contient des lignes commençant exactement par celles "correspondantes" ci-dessous, adaptez simplement votre regex)

    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

J'ai implémenté MultipartReader NuGet pour ASP.NET 4 pour la lecture de données de formulaire en plusieurs parties. Il est basé sur Multipart Form Data Parser , mais il prend en charge plusieurs fichiers.

1
Václav Dajbych

Le gars qui a résolu ce problème l'a affiché comme LGPL et vous n'êtes pas autorisé à le modifier. Je n'ai même pas cliqué dessus quand j'ai vu que… .. Voici ma version. Cela doit être testé. Il y a probablement des bugs. S'il vous plaît poster des mises à jour. Aucune garantie. Vous pouvez modifier cela à votre guise, l'appeler comme vous-même, l'imprimer sur un morceau de papier et l'utiliser comme débris de chenil,.

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

J'ai traité WCF avec le téléchargement de fichiers volumineux (plusieurs fichiers), où stocker des données en mémoire n'est pas une option. Ma solution consiste à stocker le flux de messages dans un fichier temporaire et à utiliser find pour connaître le début et la fin des données binaires.

0
Yang Zhang