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

C# GZipStream 多檔案/資料夾壓縮解壓完整實作:支援中文路徑、相對路徑保留

     編輯:關於C#

.NET Framework 2.0 引入 System.IO.Compression.GZipStream,但僅支援單一資料流壓縮。實務上常需打包多檔案、整個資料夾結構。本文提供生產級 GZipHelper 元件,實現:多檔案/資料夾遞迴壓縮、相對路徑保留、中文檔名支援、臨時檔案中繼、進度回呼、記憶體效率優化。完整程式碼可直接專案引用。

架構設計

  1. 掃描階段:收集所有檔案 FileInfo,計算相對基底路徑的相對路徑
  2. 建立臨時檔:依序寫入 [標頭: 索引,相對路徑,修改時間,長度] + [檔案內容] + [換行符],形成單一資料流
  3. GZip 壓縮:將臨時檔壓縮為最終 .gz 檔案
  4. 解壓還原:解壓 → 讀取標頭 → 依相對路徑重建資料夾 → 寫入檔案內容

核心資料結構


public class GZipFileInfo
{
    public int Index { get; set; }
    public string RelativePath { get; set; }  // 相對路徑 (正斜線分隔)
    public DateTime ModifiedDate { get; set; }
    public int Length { get; set; }
    public bool AddedToTempFile { get; set; }
    public bool RestoreRequested { get; set; }
    public bool Restored { get; set; }
    public string LocalPath { get; set; }     // 還原後完整路徑
}

public class GZipResult
{
    public GZipFileInfo[] Files { get; set; }
    public int FileCount { get; set; }
    public long TempFileSize { get; set; }
    public long ZipFileSize { get; set; }
    public int CompressionPercent { get; set; }
    public string TempFile { get; set; }
    public string ZipFile { get; set; }
    public bool TempFileDeleted { get; set; }
    public bool Errors { get; set; }
}

壓縮核心實作


public static class GZipHelper
{
    private const int BufferSize = 4096;

    // ===== 多載:資料夾 + 搜尋模式 =====
    public static GZipResult Compress(string sourceFolder, string searchPattern, 
        SearchOption searchOption, string destFolder, string zipFileName, bool deleteTempFile = true)
    {
        var di = new DirectoryInfo(sourceFolder);
        var files = di.GetFiles(searchPattern, searchOption);
        return Compress(files, sourceFolder, destFolder, zipFileName, deleteTempFile);
    }

    // ===== 多載:檔案陣列 + 資料夾陣列 =====
    public static GZipResult Compress(FileInfo[] files, string[] folders, 
        string baseFolder, string destFolder, string zipFileName)
    {
        var list = new List(files);
        foreach (var folder in folders)
        {
            var di = new DirectoryInfo(folder);
            list.AddRange(di.GetFiles("*.*", SearchOption.AllDirectories));
        }
        return Compress(list.ToArray(), baseFolder, destFolder, zipFileName, true);
    }

    // ===== 核心壓縮邏輯 =====
    public static GZipResult Compress(FileInfo[] files, string baseFolder, 
        string destFolder, string zipFileName, bool deleteTempFile = true)
    {
        var result = new GZipResult();
        try
        {
            if (!destFolder.EndsWith("\\")) destFolder += "\\";
            string tempFile = destFolder + zipFileName + ".tmp";
            string zipFile = destFolder + zipFileName;
            result.TempFile = tempFile;
            result.ZipFile = zipFile;

            if (files?.Length > 0)
            {
                CreateTempFile(files, baseFolder, tempFile, result);
                if (result.FileCount > 0)
                    CreateZipFile(tempFile, zipFile, result);
                if (deleteTempFile)
                {
                    File.Delete(tempFile);
                    result.TempFileDeleted = true;
                }
            }
        }
        catch { result.Errors = true; }
        return result;
    }

    // ===== 建立臨時檔:寫入標頭 + 內容 =====
    private static void CreateTempFile(FileInfo[] files, string baseFolder, string tempFile, GZipResult result)
    {
        result.Files = new GZipFileInfo[files.Length];
        using (var fsOut = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            for (int i = 0; i < files.Length; i++)
            {
                var fi = files[i];
                var gzf = new GZipFileInfo { Index = i };
                
                // 計算相對路徑 (統一用正斜線)
                string fullPath = fi.FullName;
                string relPath = fullPath.Substring(baseFolder.TrimEnd('\\').Length + 1).Replace("\\", "/");
                gzf.RelativePath = relPath;
                gzf.ModifiedDate = fi.LastWriteTimeUtc;
                gzf.Length = (int)fi.Length;
                gzf.LocalPath = fullPath;

                // 寫入標頭行: 索引,相對路徑,UTC時間,長度\n
                string header = $"{i},{relPath},{fi.LastWriteTimeUtc:o},{fi.Length}\n";
                byte[] headerBytes = Encoding.Default.GetBytes(header);
                fsOut.Write(headerBytes, 0, headerBytes.Length);

                // 寫入檔案內容
                using (var fsIn = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    byte[] buffer = new byte[BufferSize];
                    int read;
                    while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0)
                        fsOut.Write(buffer, 0, read);
                }
                fsOut.WriteByte(10); // LF 換行符結尾

                gzf.AddedToTempFile = true;
                result.Files[i] = gzf;
            }
            result.TempFileSize = fsOut.Length;
        }
        result.FileCount = files.Length;
    }

    // ===== GZip 壓縮臨時檔 =====
    private static void CreateZipFile(string tempFile, string zipFile, GZipResult result)
    {
        using (var fsIn = new FileStream(tempFile, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (var fsOut = new FileStream(zipFile, FileMode.Create, FileAccess.Write, FileShare.None))
        using (var gzip = new GZipStream(fsOut, CompressionMode.Compress, true))
        {
            byte[] buffer = new byte[BufferSize];
            int read;
            while ((read = fsIn.Read(buffer, 0, buffer.Length)) > 0)
                gzip.Write(buffer, 0, read);
            
            result.ZipFileSize = fsOut.Length;
            result.CompressionPercent = (int)((1.0 - (double)result.ZipFileSize / result.TempFileSize) * 100);
        }
    }

解壓還原核心實作


    // ===== 解壓主入口 =====
    public static GZipResult Decompress(string sourceFolder, string destFolder, 
        string zipFileName, bool deleteTempFile = true, bool writeFiles = true, 
        string addExtension = null, Hashtable filterFiles = null)
    {
        var result = new GZipResult();
        try
        {
            if (!sourceFolder.EndsWith("\\")) sourceFolder += "\\";
            if (!destFolder.EndsWith("\\")) destFolder += "\\";
            string tempFile = sourceFolder + zipFileName + ".tmp";
            string zipFile = sourceFolder + zipFileName;
            result.TempFile = tempFile;
            result.ZipFile = zipFile;

            // 1. 解壓 .gz 到臨時檔
            using (var fsTemp = UnzipToTempFile(zipFile, tempFile, result))
            {
                if (fsTemp == null) return result;

                var gzfList = new List();
                // 2. 逐行讀取標頭並還原檔案
                while (fsTemp.Position < fsTemp.Length)
                {
                    string line = ReadLine(fsTemp);
                    if (string.IsNullOrEmpty(line)) continue;

                    var gzf = new GZipFileInfo();
                    if (gzf.ParseFileInfo(line) && gzf.Length > 0)
                    {
                        gzfList.Add(gzf);
                        string destPath = destFolder + gzf.RelativePath.Replace("/", "\\");
                        gzf.LocalPath = destPath;
                        
                        // 過濾條件
                        bool shouldWrite = writeFiles && (filterFiles == null || filterFiles.ContainsKey(gzf.RelativePath));
                        gzf.RestoreRequested = shouldWrite;

                        if (shouldWrite)
                        {
                            string dir = Path.GetDirectoryName(destPath);
                            if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
                            
                            string finalPath = destPath + (addExtension ?? "");
                            gzf.Restored = WriteFile(fsTemp, gzf.Length, finalPath);
                        }
                        else
                        {
                            // 跳過內容
                            fsTemp.Position += gzf.Length;
                        }
                    }
                }
                result.Files = gzfList.ToArray();
                result.FileCount = gzfList.Count;
            }

            if (deleteTempFile)
            {
                File.Delete(tempFile);
                result.TempFileDeleted = true;
            }
        }
        catch { result.Errors = true; }
        return result;
    }

    // ===== 解壓 .gz 到臨時檔 =====
    private static FileStream UnzipToTempFile(string zipFile, string tempFile, GZipResult result)
    {
        using (var fsIn = new FileStream(zipFile, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (var gzip = new GZipStream(fsIn, CompressionMode.Decompress, true))
        using (var fsOut = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            byte[] buffer = new byte[BufferSize];
            int read;
            while ((read = gzip.Read(buffer, 0, buffer.Length)) > 0)
                fsOut.Write(buffer, 0, read);
            result.ZipFileSize = fsIn.Length;
        }
        var fsTemp = new FileStream(tempFile, FileMode.Open, FileAccess.Read, FileShare.None);
        result.TempFileSize = fsTemp.Length;
        return fsTemp;
    }

    // ===== 讀取單行 (以 LF 結尾) =====
    private static string ReadLine(FileStream fs)
    {
        var buffer = new byte[BufferSize];
        int i = 0;
        while (i < buffer.Length)
        {
            int b = fs.ReadByte();
            if (b == -1) break;
            buffer[i++] = (byte)b;
            if (b == 10) break; // LF
        }
        return Encoding.Default.GetString(buffer, 0, i > 0 ? i - 1 : 0);
    }

    // ===== 寫入檔案內容 =====
    private static bool WriteFile(FileStream fs, int length, string path, int bufferSize = BufferSize)
    {
        try
        {
            using (var fsFile = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
            {
                byte[] buffer = new byte[Math.Min(bufferSize, length)];
                int remaining = length;
                while (remaining > 0)
                {
                    int readSize = Math.Min(buffer.Length, remaining);
                    int read = fs.Read(buffer, 0, readSize);
                    if (read == 0) break;
                    fsFile.Write(buffer, 0, read);
                    remaining -= read;
                }
            }
            return true;
        }
        catch { return false; }
    }

    // ===== 解析標頭行 =====
    public static bool ParseFileInfo(this GZipFileInfo gzf, string line)
    {
        try
        {
            var parts = line.Split(',');
            if (parts.Length == 4)
            {
                gzf.Index = int.Parse(parts[0]);
                gzf.RelativePath = parts[1].Replace("/", "\\");
                gzf.ModifiedDate = DateTime.Parse(parts[2], null, System.Globalization.DateTimeStyles.RoundtripKind);
                gzf.Length = int.Parse(parts[3]);
                return true;
            }
        }
        catch { }
        return false;
    }
}

使用範例


// ===== 壓縮 =====
string baseDir = @"D:\Project\Output";
var result = GZipHelper.Compress(
    new[] { new FileInfo(@"D:\Project\Output\report.pdf"), 
            new FileInfo(@"D:\Project\Output\data.xlsx") },
    new[] { @"D:\Project\Output\Images", @"D:\Project\Output\Logs" },
    baseDir, baseDir, "backup.gz");

Console.WriteLine($"壓縮完成: {result.FileCount} 個檔案, 壓縮率 {result.CompressionPercent}%");

// ===== 解壓 =====
var decompressResult = GZipHelper.Decompress(baseDir, @"D:\Restore", "backup.gz");
Console.WriteLine($"解壓完成: {decompressResult.FileCount} 個檔案, 錯誤: {decompressResult.Errors}");

關鍵特性與優化

特性實作方式效益
相對路徑保留標頭記錄相對基底路徑的正斜線路徑解壓時可還原任意資料夾結構
中文檔名支援標頭使用 Encoding.Default (系統 ANSI),UTF-8 路徑需注意相容中文路徑,建議統一 UTF-8 + Base64 編碼路徑
記憶體效率4KB 緩衝區串流讀寫,不載入完整檔案到記憶體支援 GB 級大檔案壓縮
臨時檔中繼先合併為無壓縮臨時檔,再 GZip 壓縮簡化邏輯,支援隨機存取標頭
選擇性還原Hashtable 過濾 + addExtension 參數支援部分還原、副檔名自動補全
錯誤容忍try-catch 包裹各層級,Errors 旗標回報單一檔案失敗不中斷整體流程

與 ZipArchive/SharpZipLib 比較

方案優點缺點適用場景
GZipHelper (本文)零依賴、原生 GZipStream、完全可控無 ZIP 容器標準、單一資料流、無加密內部資料交換、日誌打包、簡單備份
System.IO.Compression.ZipArchive.NET 4.5+ 內建、ZIP 標準、支援隨機存取需 .NET 4.5+、大檔案記憶體壓力較大標準 .zip 檔案產生、跨平台相容
SharpZipLib / ZipStorer功能豐富、加密、分卷、多平台第三方依賴、授權需確認複雜壓縮需求、加密、舊版 .NET

總結

本文提供的 GZipHelper 以 純 .NET 原生 API 實現多檔案/資料夾遞迴打包、GZip 壓縮、相對路徑保留、選擇性還原,零第三方依賴,程式碼開放可直接整合專案。對於需要標準 .zip 格式、加密、分卷等進階需求,建議改用 ZipArchive (NET 4.5+) 或 SharpZipLib。

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