[ASP.NET Web API 2] 檔案上傳和下載

本以為這是一個很簡單的題目,但由於我是用 OWin,所以只要依賴 HttpContext.Current 就無法使用,花了一些時間整理出不透過 HttpContext.Current 上傳檔案的用法

開發環境

  • VS 2017
  • .NET 4.7.2

前置作業

用戶端是用單元測試專案建立,不知道如何使用單元測試專案測 Web API 的人請參考下篇步驟

[Web API] 使用 OWIN 進行單元測試

 

本文連結

上傳

上傳-伺服器端

這裡提供了兩種方式 MultipartMemoryStreamProvider、MultipartFormDataStreamProvider

MultipartMemoryStreamProvider

它會將獨到的檔案放到記憶體,這個例子是收到檔案後寫入 App_Data 資料夾並回傳檔案資訊

  • IsMimeMultipartContent:不是 MimeMultipartContent,就拋例外
  • Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data"):取得 App_Data 的實體路徑。
  • Request.Content.ReadAsMultipartAsync(provider):讀取檔案至 MultipartMemoryStreamProvider,資料存放在 Contents 屬性
[HttpPost]
[Route("upload")]
public async Task<IHttpActionResult> Upload()
{
    if (!this.Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
 
    var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");
    var exists = Directory.Exists(root);
    if (!exists)
    {
        Directory.CreateDirectory("App_Data");
    }
 
    try
    {
        var provider = new MultipartMemoryStreamProvider();
        await this.Request.Content.ReadAsMultipartAsync(provider);
 
        var uploadResponse = new UploadResponse();
        foreach (var content in provider.Contents)
        {
            var fileName = content.Headers.ContentDisposition.FileName.Trim('\"');
            var fileBytes = await content.ReadAsByteArrayAsync();
 
            var outputPath = Path.Combine(root, fileName);
            using (var output = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
            {
                await output.WriteAsync(fileBytes, 0, fileBytes.Length);
            }
 
            uploadResponse.Names.Add(fileName);
            uploadResponse.FileNames.Add(outputPath);
            uploadResponse.ContentTypes.Add(content.Headers.ContentType.MediaType);
        }
 
        return this.Ok(uploadResponse);
    }
    catch (Exception e)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent(e.Message)
        });
    }
}

 

MultipartFormDataStreamProvider

這跟上一個例子很像,差別在於它會將讀到的檔案放置 App_Data 並給予預設檔名

[HttpPost]
[Route("UploadFormData")]
public async Task<IHttpActionResult> UploadFormData()
{
    if (!this.Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
 
    var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");
    var exists = Directory.Exists(root);
    if (!exists)
    {
        Directory.CreateDirectory("App_Data");
    }
 
    var provider = new MultipartFormDataStreamProvider(root);
 
    try
    {
        await this.Request.Content.ReadAsMultipartAsync(provider);
        var uploadResponse = new UploadResponse();
        uploadResponse.Description = provider.FormData["description"];
 
        foreach (var content in provider.FileData)
        {
            var fileName = content.Headers.ContentDisposition.FileName.Trim('\"');
            uploadResponse.Names.Add(fileName);
            uploadResponse.FileNames.Add(content.LocalFileName);
            uploadResponse.ContentTypes.Add(content.Headers.ContentType.MediaType);
        }
 
        return this.Ok(uploadResponse);
    }
    catch (Exception e)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent(e.Message)
        });
    }
}

 

上傳-用戶端

  • ByteArrayContent:為檔案建立 ByteArrayContent,包含了 MimeType(ContentType)、FileName。
  • MimeMapping.GetMimeMapping:取得副檔名的 MimeType,這個靜態方法在 System.Web.dll 組件當中。
  • MultipartFormDataContent:傳遞多個 ByteArrayContent。它的 ContentType 是 multipart/form-data
[TestMethod]
public void Updload_TEST()
{
    var url = "/api/file/upload";
    var fileNames = new[] {"例外1.txt""例外2.txt"};
    using (var content = new MultipartFormDataContent())
    {
        foreach (var fileName in fileNames)
        {
            var fileMimeType = MimeMapping.GetMimeMapping(fileName);
            var fileBytes = File.ReadAllBytes(fileName);
            var fileContent = new ByteArrayContent(fileBytes);
            fileContent.Headers.ContentType = new MediaTypeHeaderValue(fileMimeType);
            content.Add(fileContent, fileName, fileName);
        }
 
        var response = s_client.PostAsync(url, content).Result;
        response.EnsureSuccessStatusCode();
        var result = response.Content.ReadAsAsync<UploadResponse>().Result;
        Assert.IsNotNull(result);
    }
}

 

下載

下載-伺服器端

關鍵在於回傳的 Header,告訴用戶現在是附檔下載

  • content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
  • content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
[HttpGet]
[Route("download")]
public async Task<IHttpActionResult> Download(string fileName)
{
    var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");
    var exists = Directory.Exists(root);
    if (!exists)
    {
        Directory.CreateDirectory("App_Data");
    }
 
    var filePath = Path.Combine(root, fileName);
    try
    {
        if (!File.Exists(filePath))
        {
            return this.NotFound();
        }
 
        var fileStream = new FileStream(filePath, FileMode.Open);
        var content = new StreamContent(fileStream);
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
 
        //content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(fileName));
        content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = fileName
        };
        return new ResponseMessageResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = content
        });
    }
    catch (Exception e)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent(e.Message)
        });
    }
}

 

下載-用戶端

調用 API 後拿 Bytes

[TestMethod]
public void Download_TEST()
{
    var fileName = "例外1.txt";
    var url = $"api/file/download?fileName={fileName}";
 
    var response = s_client.GetAsync(url).Result;
    response.EnsureSuccessStatusCode();
    var fileBytes = response.Content.ReadAsByteArrayAsync().Result;
    Assert.IsTrue(fileBytes.Length > 0);
}

 

完整範例

https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/UploadAndDownloadFiles

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo