Загрузка больших файлов в Controller порциями с использованием HttpClient, IFormFile всегда пуст - PullRequest
0 голосов
/ 02 ноября 2018

Я пытаюсь создать класс .Net Standard "Client" для загрузки (иногда очень больших) файлов в контроллер. Я хочу сделать это, разбивая файл на куски и загружая их по одному. Предполагается, что другие приложения будут использовать это вместо непосредственного взаимодействия с Web Api.

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

Проблема, с которой я столкнулся, заключается в том, что параметр IEnumerable<IFormFile> files для моего контроллера всегда пуст при публикации из моего класса клиента

Контроллер

[Route("api/Upload")]
public ActionResult ChunkSave(IEnumerable<IFormFile> files, string metaData, Guid id)
{
    MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(metaData));
    var serializer = new DataContractJsonSerializer(typeof(ChunkMetaData));
    ChunkMetaData somemetaData = serializer.ReadObject(ms) as ChunkMetaData;

    // The Name of the Upload component is "files"
    if (files != null)
    {
        // If this is the first chunk, try to delete the file so that we don't accidently
        // and up appending new bytes to the old file.
        if (somemetaData.ChunkIndex == 0)
        {
            _io.DeleteFile(id, Path.GetFileName(somemetaData.FileName));
        }

        foreach (var file in files)
        {
            // Some browsers send file names with full path. This needs to be stripped.
             _io.AppendToFile(id, Path.GetFileName(somemetaData.FileName), file.OpenReadStream());
        }
    }

    FileResult fileBlob = new FileResult();
    fileBlob.uploaded = somemetaData.TotalChunks - 1 <= somemetaData.ChunkIndex;
    fileBlob.fileUid = somemetaData.UploadUid;
    return new JsonResult(fileBlob);
}

Клиент:

public class FileTransferClient
{
    HttpClient Client { get; set; } 

    public FileTransferClient(Uri apiUrl)
    {
        this.Client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true })
        {
            BaseAddress = apiUrl
        };
        this.Client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }

    public async Task<bool> UploadFile(Guid id, Stream file, string name, string contentType)
    {
        bool ret = true;
        int chunckSize = 2097152; //2MB
        int totalChunks = (int)(file.Length / chunckSize);
        if (file.Length % chunckSize != 0)
        {
            totalChunks++;
        }

        for (int i = 0; i < totalChunks; i++)
        {
            long position = (i * (long)chunckSize);
            int toRead = (int)Math.Min(file.Length - position + 1, chunckSize);
            byte[] buffer = new byte[toRead];
            await file.ReadAsync(buffer, 0, toRead);

            MultipartFormDataContent content = new MultipartFormDataContent();
            content.Add(new StringContent(id.ToString()), "id");
            var meta = JsonConvert.SerializeObject(new ChunkMetaData
            {
                UploadUid = id.ToString(),
                FileName = name,
                ChunkIndex = i,
                TotalChunks = totalChunks,
                TotalFileSize = file.Length,
                ContentType = contentType
            });
            content.Add(new StringContent(meta), "metaData");
            using (var ms = new MemoryStream(buffer))
            {
                content.Add(new StreamContent(ms),"files");
                var response = await Client.PostAsync("/api/Upload", content).ConfigureAwait(false);
                if (!response.IsSuccessStatusCode)
                {
                    ret = false;
                    break;
                }
            }
        }
        return ret;
    }
}

Ответы [ 2 ]

0 голосов
/ 05 ноября 2018

Проблема заключалась в том, что я использовал StreamContent вместо ByteArrayContent для представления своих файловых блоков. Вот что я закончил:

public async Task<FileData> UploadFileAsync(Guid id, string name, Stream file)
{
    Guid uid = Guid.NewGuid();
    int chunckSize = 2097152; //2MB
    int totalChunks = (int)(file.Length / chunckSize);
    if (file.Length % chunckSize != 0)
    {
        totalChunks++;
    }

    for (int i = 0; i < totalChunks; i++)
    {
        long position = (i * (long)chunckSize);
        int toRead = (int)Math.Min(file.Length - position + 1, chunckSize);
        byte[] buffer = new byte[toRead];
        await file.ReadAsync(buffer, 0, buffer.Length);

        using (MultipartFormDataContent form = new MultipartFormDataContent())
        {
            form.Add(new ByteArrayContent(buffer), "files", name);
            form.Add(new StringContent(id.ToString()), "id");
            var meta = JsonConvert.SerializeObject(new ChunkMetaData
            {
                UploadUid = id.ToString(),
                FileName = name,
                ChunkIndex = i,
                TotalChunks = totalChunks,
                TotalFileSize = file.Length,
                ContentType = "application/unknown"
            });
            form.Add(new StringContent(meta), "metaData");
            var response = await Client.PostAsync("/api/Upload", form).ConfigureAwait(false);
            return response.IsSuccessStatusCode;
        }
    }
    return true;
}
0 голосов
/ 02 ноября 2018

Ваш параметр пуст, потому что вы отправляете не массив файлов, а только один файл. Следовательно, привязка не выполняется, и вы получаете нулевое значение. Акт чанкинга (который вы даже не делаете) не равняется IEnumerable<IFormFile>; это все еще просто IFormFile.

Хотя вам нужно отправить как multipart/form-data, потому что вы отправляете и файл, и другие данные публикации, я думаю, вы не понимаете, что это на самом деле делает. Это просто означает, что тело запроса содержит несколько различных типов MIME, , а не - это означает, что он загружает файл в несколько частей, что, по-вашему, и делает.

Фактический процесс потоковой загрузки происходит на стороне сервера. Речь идет о том, как сервер выбирает обработку загружаемого файла, а не о том, как пользователь загружает его. Более конкретно, любой тип привязки модели, в частности к IFormFile, приведет к тому, что файл будет сначала помещен в буфер на диск, а затем передан в ваши действия. Другими словами, если вы принимаете IFormFile, вы уже проиграли битву. Он уже полностью передан с клиента на ваш сервер.

Документы ASP.NET Core показывают, как на самом деле выполнять потоковую загрузку, и неудивительно, что здесь задействовано немало кода, которого у вас нет в настоящее время. По сути, вам нужно полностью отключить привязку модели в действии и вручную проанализировать тело запроса, стараясь на самом деле разделить чтение из потока и не делать что-то, что заставит все это целиком зайти в память.

...