本以為這是一個很簡單的題目,但由於我是用 OWin,所以只要依賴 HttpContext.Current 就無法使用,花了一些時間整理出不透過 HttpContext.Current 上傳檔案的用法
開發環境
- VS 2017
- .NET 4.7.2
前置作業
用戶端是用單元測試專案建立,不知道如何使用單元測試專案測 Web API 的人請參考下篇步驟
本文連結
上傳
上傳-伺服器端
這裡提供了兩種方式 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