Чтение файла ввода из POST-данных multipart / form-data - PullRequest
42 голосов
/ 18 сентября 2011

Я помещаю файл в службу WCF REST через HTML-форму с enctype, установленным на multipart/form-data и одним компонентом: <input type="file" name="data">.Результирующий поток, читаемый сервером, содержит следующее:

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

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

Проблема в том, что я не уверен, как извлечь файлы байтов из потока.Мне нужно сделать это, чтобы записать файл на диск.

Ответы [ 9 ]

41 голосов
/ 11 февраля 2014

Извините, что присоединился к вечеринке поздно, но есть способ сделать это с публичным API Microsoft .

Вот что вам нужно:

  1. System.Net.Http.dll
    • Включено в .NET 4.5
    • Для .NET 4 получить его через NuGet
  2. System.Net.Http.Formatting.dll

Примечание Пакеты 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);
        }
    }
}

Что касается использования, допустим, у вас есть следующий метод REST WCF:

[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);
}
31 голосов
/ 18 сентября 2011

Вы можете взглянуть на следующую запись в блоге , которая иллюстрирует методику, которую можно использовать для анализа multipart/form-data на сервере с помощью Multipart Parser :

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.

25 голосов
/ 25 января 2013

У меня были некоторые проблемы с синтаксическим анализатором, которые основаны на синтаксическом анализе строк, особенно с большими файлами. Я обнаружил, что у него не хватает памяти и не удается проанализировать двоичные данные.

Чтобы справиться с этими проблемами, я открыл свою собственную попытку C # multipart / form-parser здесь

Особенности:

  • Хорошо обрабатывает очень большие файлы.(Данные передаются и передаются во время чтения)
  • Может обрабатывать несколько загрузок файлов и автоматически определяет, является ли раздел файлом или нет.
  • Возвращает файлы в виде потока, а не байта [] (подходит для больших файлов).
  • Полная документация для библиотеки, включая веб-сайт, созданный в стиле MSDN.
  • Полные модульные тесты.

Ограничения:

  • Не обрабатывает не множественные данные.
  • Код более сложен, чем

от Лоренцо. Просто используйте класс 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

16 голосов
/ 20 января 2012

Я с открытым исходным кодом C # Http анализатор здесь .

Это немного более гибко, чем другой, упомянутый в CodePlex, поскольку вы можете использовать его как для Multipart, так и для не 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);
    }
}
2 голосов
/ 24 июля 2013

Другой способ - использовать .Net-парсер для HttpRequest.Для этого вам нужно использовать немного отражения и простой класс для WorkerRequest.

Сначала создайте класс, производный от 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);
    }
}

Затем, где бы вы ни создавали поток сообщений, вы создаете экземпляр этого класса.Я делаю это в 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
                );

, а затем создаю HttpRequest, используя активатор и не публичный конструктор

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 у вас будут файлы изтвой поток

1 голос
/ 16 декабря 2013

Как насчет Regex?

Я написал это для текстового файла, но я считаю, что это может сработать для вас

(В случае, если ваш текстовый файл содержит строку, начинающуюся точно с "соответствующих" строк ниже - просто адаптируйте свой 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 голос
/ 25 декабря 2012

Я реализовал MultipartReader Пакет NuGet для ASP.NET 4 для чтения многочастных данных формы.Он основан на синтаксическом анализаторе форм Multipart , но поддерживает более одного файла.

1 голос
/ 15 февраля 2012

Парень, который решил это, опубликовал его как LGPL, и вы не можете изменять его. Я даже не нажал на него, когда увидел это. Вот моя версия. Это должно быть проверено. Вероятно, есть ошибки. Пожалуйста, разместите любые обновления. Нет гарантии. Вы можете изменить это все, что захотите, назвать его своим, распечатать на листе бумаги и использовать его для сбора отходов питомника, ... все равно.

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


    }
}
0 голосов
/ 16 сентября 2013

Я имел дело с WCF с загрузкой большого файла (несколько ГБ), где сохранение данных в памяти не вариант.Мое решение состоит в том, чтобы сохранить поток сообщений во временном файле и использовать поиск для определения начала и конца двоичных данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...