WebClient.OpenWriteTaskAsync 以 25MB 為單位分塊上傳,完成一塊才發送下一塊 (避免記憶體暴增)。public static class LargeFileUploader
{
private const int ChunkSize = 25 * 1024 * 1024; // 25MB
///
/// 進入點:自動判斷檔案大小決定是否分塊
///
public static async Task<bool> UploadAsync(string url, Stream fileStream)
{
if (fileStream.Length > ChunkSize)
url += "&t=1"; // 告知伺服器採用分塊模式
try
{
await UploadChunkedAsync(url, fileStream);
return true;
}
catch (Exception ex)
{
// 記錄錯誤
return false;
}
}
private static async Task UploadChunkedAsync(string url, Stream sourceStream)
{
var wc = new WebClient();
byte[] buffer = new byte[ChunkSize];
// 遞迴上傳每一塊
Action uploadNext = null;
uploadNext = async () =>
{
int read = sourceStream.Read(buffer, 0, buffer.Length);
if (read == 0)
{
sourceStream.Close();
return;
}
// 這一塊寫入完成後再遞迴下一塊
wc.WriteStreamClosed += (s, e) => uploadNext();
using (var serverStream = await wc.OpenWriteTaskAsync(new Uri(url, UriKind.Absolute), "POST"))
{
serverStream.Write(buffer, 0, read);
// 關閉 serverStream 會觸發 WriteStreamClosed
}
};
uploadNext();
}
}
public class UploadHandler : IHttpHandler, IRequiresSessionState
{
public bool IsReusable => false;
public void ProcessRequest(HttpContext context)
{
try
{
// 1. 解析上傳元資料 (Base64 + JSON)
string metaB64 = context.Request.QueryString["data"];
string metaJson = Base64Decode(metaB64, "YourSecretKey"); // 自行實作解密
var meta = JsonConvert.DeserializeObject<FileMeta>(metaJson);
bool isChunked = context.Request.QueryString["t"] == "1";
// 2. 確保目錄存在
string dir = Path.Combine(Config.UploadRoot, meta.Dir);
Directory.CreateDirectory(dir);
string filePath = Path.Combine(dir, meta.FileName);
// 3. 非分塊模式先刪除舊檔
if (!isChunked && File.Exists(filePath)) File.Delete(filePath);
// 4. 追加寫入流
using (var fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None))
{
const int BufSize = 4096;
byte[] buffer = new byte[BufSize];
int read;
var input = context.Request.InputStream;
while ((read = input.Read(buffer, 0, BufSize)) > 0)
{
fs.Write(buffer, 0, read);
}
}
// 5. 產生縮圖 (可選)
if (meta.NeedThumbnail) GenerateThumbnail(filePath, meta);
// 6. 清理舊檔 (替換上傳情境)
CleanupOldFiles(meta);
context.Response.Write("OK");
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
context.Response.Write(ex.Message);
}
}
// 輔助方法略...
}
| 議題 | 處理方式 |
|---|---|
| 記憶體控制 | 固定 25MB 緩衝區,讀一塊傳一塊,不將整檔載入記憶體。 |
| 順序保證 | `WriteStreamClosed` 事件串聯下一塊上傳,確保伺服端依序寫入,避免競態覆蓋。 |
| 斷點續傳 | 伺服端可對比檔案大小與元資料 `FileSize`,決定是否略過已上傳分塊 (需客戶端支援 Range/Resume)。 |
| 錯誤重試 | 客戶端捕捉例外後可指數退避重試特定分塊。 |
| 安全性 | 元資料加密傳遞、伺服端驗證檔案類型 (Magic Number)、限制副檔名、掃毒。 |
Silverlight 已於 2021 年停止支援。新專案建議採用:
Silverlight 時代的 `WebClient` 分塊上傳展現了「串流分塊、事件串聯、伺服端追加寫入」的核心模式,這些原理在現代 HTML5/Blazor 上傳元件中依然適用,理解底層機制有助於選型與除錯。