當使用者需一次下載多個檔案時,瀏覽器無法原生支援多檔案同時下載。標準解法是:伺服端將選取檔案複製到暫存資料夾 → 呼叫壓縮工具打包成單一 ZIP/RAR → 串流輸出給瀏覽器 → 清理暫存檔。本文提供完整實作方案。
protected void btnBatchDownload_Click(object sender, EventArgs e)
{
string sourceFolder = Server.MapPath("~/Uploads/Files/");
string tempFolderName = DateTime.Now.ToString("yyyyMMddHHmmssfff");
string tempFolder = Path.Combine(sourceFolder, tempFolderName);
try
{
// 1. 建立暫存資料夾
Directory.CreateDirectory(tempFolder);
// 2. 複製勾選的檔案(這裡示範全複製,實務依 CheckBoxList 篩選)
foreach (string file in Directory.GetFiles(sourceFolder))
{
string dest = Path.Combine(tempFolder, Path.GetFileName(file));
File.Copy(file, dest, true);
}
// 3. 壓縮打包
string rarPath = Path.Combine(tempFolder, tempFolderName + ".rar");
if (CreateRarArchive(tempFolder, rarPath))
{
// 4. 輸出下載
DownloadFile(rarPath);
}
else
{
ShowMessage("壓縮失敗");
}
}
catch (Exception ex)
{
LogError("批次下載錯誤", ex);
ShowMessage("處理發生錯誤:" + ex.Message);
}
finally
{
// 5. 清理暫存資料夾(非同步或延遲執行)
CleanupTempFolder(tempFolder);
}
}
/// <summary>使用 WinRAR 命令列建立壓縮檔</summary>
/// <param name="sourceDir">要壓縮的資料夾路徑</param>
/// <param name="rarFilePath">輸出的 .rar 完整路徑</param>
private bool CreateRarArchive(string sourceDir, string rarFilePath)
{
try
{
// 從登錄取得 WinRAR 安裝路徑
string winRarPath = GetWinRarPath();
if (string.IsNullOrEmpty(winRarPath) || !File.Exists(winRarPath))
{
// 備選:嘗試 PATH 環境變數中的 WinRAR
winRarPath = "WinRAR.exe";
}
// 參數說明:
// a = 加入檔案到壓縮檔
// -r = 遞迴子目錄
// -ep1 = 排除基底目錄路徑(只保留相對路徑)
// -ibck = 背景執行
string arguments = $"a -r -ep1 -ibck \"{rarFilePath}\" \"{sourceDir}\\*\"";
var psi = new ProcessStartInfo
{
FileName = winRarPath,
Arguments = arguments,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = sourceDir,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process proc = Process.Start(psi))
{
proc.WaitForExit(60000); // 最多等待 60 秒
return proc.ExitCode == 0 && File.Exists(rarFilePath);
}
}
catch (Exception ex)
{
LogError("建立 RAR 失敗", ex);
return false;
}
}
private string GetWinRarPath()
{
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey(@"WinRAR\Shell\Open\Command"))
{
if (key != null)
{
string cmd = key.GetValue("")?.ToString();
if (!string.IsNullOrEmpty(cmd))
{
// 格式通常為:"C:\Program Files\WinRAR\WinRAR.exe" "%1"
var match = System.Text.RegularExpressions.Regex.Match(cmd, @"\"([^""]+WinRAR\.exe)\"");
if (match.Success) return match.Groups[1].Value;
}
}
}
}
catch { }
return null;
}
private void DownloadFile(string filePath)
{
FileInfo fi = new FileInfo(filePath);
if (!fi.Exists) return;
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.ClearHeaders();
HttpContext.Current.Response.ClearContent();
// 關鍵標頭
HttpContext.Current.Response.AddHeader("Content-Disposition", $"attachment; filename=\"{HttpUtility.UrlEncode(fi.Name, System.Text.Encoding.UTF8)}\"");
HttpContext.Current.Response.AddHeader("Content-Length", fi.Length.ToString());
HttpContext.Current.Response.AddHeader("Content-Transfer-Encoding", "binary");
HttpContext.Current.Response.ContentType = "application/octet-stream";
HttpContext.Current.Response.ContentEncoding = System.Text.Encoding.GetEncoding("gb2312"); // 或 UTF-8
// 分塊傳輸避免大檔案記憶體暴增
const int bufferSize = 1024 * 1024; // 1MB
byte[] buffer = new byte[bufferSize];
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
if (!HttpContext.Current.Response.IsClientConnected) break;
HttpContext.Current.Response.OutputStream.Write(buffer, 0, bytesRead);
}
}
HttpContext.Current.Response.Flush();
HttpContext.Current.Response.End();
}
private void CleanupTempFolder(string folderPath)
{
try
{
if (Directory.Exists(folderPath))
{
// 移除唯讀屬性
foreach (string file in Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories))
{
File.SetAttributes(file, FileAttributes.Normal);
}
Directory.Delete(folderPath, true);
}
}
catch (Exception ex)
{
LogError("清理暫存資料夾失敗", ex);
// 可寫入排程稍後清理
}
}
.NET 4.5+ 可使用 `System.IO.Compression.ZipFile`,部署更簡單:
using System.IO.Compression;
private bool CreateZipArchive(string sourceDir, string zipFilePath)
{
try
{
if (File.Exists(zipFilePath)) File.Delete(zipFilePath);
// CreateFromDirectory 會包含資料夾本身,需調整
ZipFile.CreateFromDirectory(sourceDir, zipFilePath, CompressionLevel.Optimal, false);
return File.Exists(zipFilePath);
}
catch (Exception ex)
{
LogError("建立 ZIP 失敗", ex);
return false;
}
}
注意: `CreateFromDirectory` 會將資料夾名稱包含在壓縮檔內。若不想要最外層資料夾,需自行遍歷檔案建立 `ZipArchive`。
| 問題 | 解決方案 |
|---|---|
| 大檔案記憶體不足 | 使用 `Response.TransmitFile`(IIS 7.5+)或分塊串流輸出(如上例) |
| 壓縮耗時導致 Request Timeout | 非同步處理:建立壓縮檔 → 存放暫存區 → 回傳下載 Token → 前端輪詢/重導向下載 |
| 磁碟 IO 瓶頸 | 暫存資料夾放在 SSD/獨立磁碟;使用 RAM Disk 极速暫存 |
| WinRAR 授權問題 | 改用開源 7-Zip (`7za.exe`) 或 .NET 內建 ZIP / SharpZipLib |
| 並發衝突 | 暫存資料夾命名加入 GUID:`Guid.NewGuid().ToString("N")` |
<asp:GridView ID="gvFiles" runat="server" AutoGenerateColumns="False" DataKeyNames="FileId">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:CheckBox ID="chkSelect" runat="server" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="FileName" HeaderText="檔案名稱" />
<asp:BoundField DataField="FileSize" HeaderText="大小" DataFormatString="{0:N0} KB" />
</Columns>
</asp:GridView>
<asp:Button ID="btnDownload" runat="server" Text="批次下載" OnClick="btnBatchDownload_Click" />
protected void btnBatchDownload_Click(object sender, EventArgs e)
{
var selectedFiles = new List<string>();
foreach (GridViewRow row in gvFiles.Rows)
{
var chk = row.FindControl("chkSelect") as CheckBox;
if (chk?.Checked == true)
{
string fileName = gvFiles.DataKeys[row.RowIndex].Value.ToString();
selectedFiles.Add(fileName);
}
}
if (selectedFiles.Count == 0) { ShowMessage("請至少選擇一個檔案"); return; }
// 呼叫核心邏輯,傳入 selectedFiles 進行複製壓縮
ProcessBatchDownload(selectedFiles);
}
批次下載的核心在於「伺服端聚合 → 壓縮 → 串流輸出 → 清理」。建議優先使用 .NET 內建 `ZipFile` 避免外部相依;若需 RAR 格式或更高壓縮率,再整合 7-Zip(開源、命令列相容)。務必處理大檔案串流、暫存清理、錯誤記錄與併發控制,確保生產環境穩定。