Web API 2 引入 MultipartFormDataStreamProvider 專為 `multipart/form-data` 串流式儲存設計,**不將整個請求載入記憶體**,適合大檔案上傳。
public class UploadController : ApiController
{
// POST api/upload
[HttpPost]
[Route("api/upload/image")]
public async Task<IHttpActionResult> UploadImage()
{
// 1. 驗證 Content-Type
if (!Request.Content.IsMimeMultipartContent("form-data"))
return BadRequest("僅支援 multipart/form-data");
// 2. 設定暫存目錄(實體儲存路徑)
string tempRoot = HttpContext.Current.Server.MapPath("~/App_Data/TempUploads");
Directory.CreateDirectory(tempRoot);
var provider = new MultipartFormDataStreamProvider(tempRoot);
try
{
// 3. 非同步讀取並儲存至暫存檔
await Request.Content.ReadAsMultipartAsync(provider);
// 4. 取得第一個檔案(單檔上傳場景)
var fileData = provider.FileData.FirstOrDefault();
if (fileData == null) return BadRequest("未收到檔案");
// 5. 解析原始檔名(去除雙引號)
string originalName = fileData.Headers.ContentDisposition.FileName.Trim('"');
string ext = Path.GetExtension(originalName).ToLowerInvariant();
// 6. 業務驗證
var validation = ValidateFile(fileData, ext);
if (!validation.IsValid) return BadRequest(validation.ErrorMessage);
// 7. 產生永久儲存路徑
string permanentDir = HttpContext.Current.Server.MapPath("~/Uploads/Images");
Directory.CreateDirectory(permanentDir);
string finalName = $"{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}{ext}";
string finalPath = Path.Combine(permanentDir, finalName);
// 8. 移動暫存檔 → 永久位置(同磁碟區瞬間完成)
File.Move(fileData.LocalFileName, finalPath);
// 9. 回傳結果
return Ok(new
{
success = true,
fileName = finalName,
originalName,
url = $"/Uploads/Images/{finalName}",
size = new FileInfo(finalPath).Length
});
}
catch (Exception ex)
{
// 清理暫存檔
foreach (var f in provider.FileData)
if (File.Exists(f.LocalFileName)) File.Delete(f.LocalFileName);
// 記錄錯誤(略)
return InternalServerError(ex);
}
}
private (bool IsValid, string ErrorMessage) ValidateFile(MultipartFileData file, string ext)
{
// 副檔名白名單
var allowed = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
if (!allowed.Contains(ext)) return (false, "不支援的檔案格式");
// 檔案大小(這裡已寫入磁碟,讀取長度)
long size = new FileInfo(file.LocalFileName).Length;
if (size == 0) return (false, "空檔案");
if (size > 10 * 1024 * 1024) return (false, "檔案不可超過 10MB");
// 進階:Magic Number 驗證真實格式
if (!IsValidImageHeader(file.LocalFileName)) return (false, "檔案內容非有效圖片");
return (true, null);
}
private bool IsValidImageHeader(string path)
{
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] header = new byte[8];
fs.Read(header, 0, header.Length);
// JPEG: FF D8 FF
if (header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF) return true;
// PNG: 89 50 4E 47 0D 0A 1A 0A
if (header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47) return true;
// GIF: 47 49 46 38
if (header[0] == 0x47 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x38) return true;
// BMP: 42 4D
if (header[0] == 0x42 && header[1] == 0x4D) return true;
// WebP: 52 49 46 46 ... 57 45 42 50
if (header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x46) return true;
}
return false;
}
}
<system.web>
<!-- 最大請求 100MB,執行逾時 10 分鐘 -->
<httpRuntime maxRequestLength="102400" executionTimeout="600" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<!-- 最大內容 100MB (bytes) -->
<requestLimits maxAllowedContentLength="104857600" />
</requestFiltering>
</security>
</system.webServer>
// HTML5 FormData + fetch
const form = new FormData();
form.append('file', fileInput.files[0]); // key 需與後端 FileData 對應
fetch('/api/upload/image', { method: 'POST', body: form })
.then(r => r.json())
.then(data => console.log('上傳成功', data.url))
.catch(err => console.error('上傳失敗', err));
// 多檔案
var files = provider.FileData; // List
// 表單欄位
var fields = provider.FormData; // NameValueCollection
string userId = fields["userId"];
// 串流直上 Azure Blob / AWS S3(不落地本地磁碟)
public class CloudStreamProvider : MultipartFileStreamProvider
{
private readonly CloudBlobContainer _container;
public CloudStreamProvider(CloudBlobContainer container) : base(Path.GetTempPath())
{ _container = container; }
public override Stream GetStream(HttpContentHeaders headers, FormDataCollection form)
{
string blobName = $"{Guid.NewGuid():N}{Path.GetExtension(headers.ContentDisposition.FileName.Trim('"'))}";
var blob = _container.GetBlockBlobReference(blobName);
return blob.OpenWrite(); // 直接寫入 Blob 串流
}
}
// 使用
var cloudProvider = new CloudStreamProvider(container);
await Request.Content.ReadAsMultipartAsync(cloudProvider);
| 舊版 | Web API | 差異 |
|---|---|---|
Request.Files | provider.FileData | 串流 vs 記憶體 |
file.InputStream | File.OpenRead(fileData.LocalFileName) | 暫存檔路徑 |
file.SaveAs(path) | File.Move(temp, final) | 瞬間移動 |
file.ContentLength | new FileInfo(temp).Length | 磁碟大小 |
`MultipartFormDataStreamProvider` 是 Web API 處理大檔案上傳的標準組件,關鍵在於**串流落地暫存目錄 → 業務驗證 → 原子性移動至永久目錄**。配合 `maxRequestLength`/`maxAllowedContentLength`、Magic Number 驗證、雲存串流提供者,可構建生產級上傳服務。新專案建議直接使用 ASP.NET Core 的 `IFormFile` + `FileBufferingReadStream`,架構更簡潔。