Чтение ввода файла из POST файла с несколькими данными/формами данных

Я отправляю файл в службу 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--

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

Ответ 1

Вы можете взглянуть на после сообщения в блоге, который иллюстрирует технику, которая может использоваться для синтаксического анализа 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);
    }
}

Другая возможность - включить совместимость aspnet и использовать HttpContext.Current.Request, но это не очень удобный способ WCFish.

Ответ 2

Прошу прощения за участие в вечеринке поздно, но есть способ сделать это с помощью общедоступного API Microsoft.

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

Я открываю открытый синтаксический анализатор С# Http здесь.

Это немного более гибко, чем упомянутый выше, который относится к 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);
    }
}

Ответ 5

Парень, который решил это, опубликовал его как 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();
        }


    }
}

Ответ 7

Другим способом было бы использовать .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 у вас будут файлы из вашего потока.

Ответ 8

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

Ответ 9

Как насчет некоторого 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;
    }