首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 批次下載檔案:伺服端壓縮打包輸出完整方案

     編輯:ASP.NET 基礎

前言

當使用者需一次下載多個檔案時,瀏覽器無法原生支援多檔案同時下載。標準解法是:伺服端將選取檔案複製到暫存資料夾 → 呼叫壓縮工具打包成單一 ZIP/RAR → 串流輸出給瀏覽器 → 清理暫存檔。本文提供完整實作方案。

一、整體流程架構

  1. 前端:使用者勾選檔案 → 送出下載請求
  2. 後端:建立唯一暫存資料夾 → 複製檔案 → 壓縮打包 → `Response` 輸出 → 刪除暫存資料夾
  3. 關鍵點:使用 `WinRAR` 命令列模式壓縮,支援大檔案、資料夾結構保留

二、核心程式碼實作

1. 主下載邏輯

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);
    }
}

2. 呼叫 WinRAR 建立壓縮檔

/// <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;
}

3. 檔案串流輸出下載

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();
}

4. 清理暫存資料夾

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 內建 ZIP(不需 WinRAR)

.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(開源、命令列相容)。務必處理大檔案串流、暫存清理、錯誤記錄與併發控制,確保生產環境穩定。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved