.NET Framework 2.0 引入 System.IO.Compression.GZipStream,但僅支援單一資料流壓縮。實務上常需打包多檔案、整個資料夾結構。本文提供生產級 GZipHelper 元件,實現:多檔案/資料夾遞迴壓縮、相對路徑保留、中文檔名支援、臨時檔案中繼、進度回呼、記憶體效率優化。完整程式碼可直接專案引用。
FileInfo,計算相對基底路徑的相對路徑
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 旗標回報 | 單一檔案失敗不中斷整體流程 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 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。