使用 HttpClient 将大文件分块上传到 Controller,IFormFile 始终为空

Uploading large files to Controller in chunks using HttpClient, IFormFile always empty

我正在尝试创建一个 .Net 标准 "Client" class 用于将(有时非常大的)文件上传到控制器。我想通过将文件分成块并一次上传一个来做到这一点。目的是让其他应用程序使用它而不是直接与 Web 通信 Api。

我已经让 Controller 工作了。我已经验证它可以使用支持块保存的 Kendo-ui 控件工作。

我遇到的问题是,从我的客户端 class

发布时,我的控制器的 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;
    }
}

您的参数为空,因为您发送的不是一组文件,而是一个文件。因此,绑定失败,您得到一个空值。分块的行为(你实际上并没有这样做)并不等同于 IEnumerable<IFormFile>;它仍然只是一个 IFormFile.

虽然您需要作为 multipart/form-data 发送,因为您要发送文件上传和其他一些 post 数据,但我认为您误解了这实际上是做什么的。它只是意味着请求正文包含多种不同的 mime 类型,它 而不是 意味着它将文件分成多个部分上传,这似乎是你所认为的。

流式传输上传的实际行为发生在服务器端。这是关于服务器如何选择处理正在上传的文件,而不是关于用户如何上传它。更具体地说,任何类型的模型绑定,特别是 IFormFile 都会导致文件首先假脱机到磁盘,然后传递到您的操作中。换句话说,如果您接受 IFormFile,您就已经输了。它已经完全从客户端传输到您的服务器。

ASP.NET Core docs 向您展示了如何实际上 流式传输上传,不出所料,其中涉及相当多的代码,none 您目前拥有.您基本上必须完全关闭操作上的模型绑定并自己手动解析请求主体,注意实际分块读取流,而不是做一些会立即将整个内容强制放入内存的操作。

问题是我使用 StreamContent 而不是 ByteArrayContent 来表示我的文件块。这是我最终得到的结果:

public async Task<Bool> UploadFileAsync(Guid id, string name, Stream file)
{
    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, 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;
}