Golang中处理压缩包需防范路径穿越漏洞,解压时应校验文件路径是否在目标目录内,避免恶意文件写入。

Golang在文件压缩与解压方面,其标准库提供了相当成熟且高效的解决方案,特别是
archive/zip
archive/tar
compress/gzip
package main
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// CompressToZip 将指定路径的文件或目录压缩成ZIP文件
// sourcePath 可以是文件或目录
// destZipFile 是目标ZIP文件的路径
func CompressToZip(sourcePath, destZipFile string) error {
zipFile, err := os.Create(destZipFile)
if err != nil {
return fmt.Errorf("创建ZIP文件失败: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
info, err := os.Stat(sourcePath)
if err != nil {
return fmt.Errorf("获取源路径信息失败: %w", err)
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(sourcePath)
}
err = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 构建在ZIP文件中的相对路径
// 如果源是目录,相对路径需要包含目录名
// 如果源是文件,相对路径就是文件名本身
headerPath := strings.TrimPrefix(path, sourcePath)
if info.IsDir() {
if baseDir != "" { // 源是目录,需要加上目录名
headerPath = filepath.Join(baseDir, headerPath)
}
if headerPath != "" { // 确保目录名后面有斜杠,表示是目录
headerPath += "/"
}
} else if baseDir != "" { // 源是目录下的文件
headerPath = filepath.Join(baseDir, headerPath)
} else { // 源是单个文件
headerPath = filepath.Base(sourcePath)
}
// 移除开头的斜杠或点斜杠
headerPath = strings.TrimPrefix(headerPath, string(filepath.Separator))
headerPath = strings.TrimPrefix(headerPath, ".")
if headerPath == "" && info.IsDir() { // 避免根目录自身被添加为 ""
return nil
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("创建文件头失败: %w", err)
}
header.Name = headerPath // 使用我们构建的相对路径
header.Method = zip.Deflate
if info.IsDir() {
header.Method = 0 // 目录不需要压缩方法
header.SetMode(info.Mode()) // 保留目录权限
_, err = zipWriter.CreateHeader(header)
if err != nil {
return fmt.Errorf("创建目录头失败: %w", err)
}
return nil
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return fmt.Errorf("创建文件写入器失败: %w", err)
}
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return fmt.Errorf("写入文件内容失败: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("遍历文件时发生错误: %w", err)
}
return nil
}
// DecompressZip 将ZIP文件解压到指定目录
func DecompressZip(zipFile, destDir string) error {
reader, err := zip.OpenReader(zipFile)
if err != nil {
return fmt.Errorf("打开ZIP文件失败: %w", err)
}
defer reader.Close()
for _, file := range reader.File {
// 避免路径穿越攻击
filePath := filepath.Join(destDir, file.Name)
if !strings.HasPrefix(filePath, filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("非法文件路径: %s", file.Name)
}
if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
continue
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { // 确保父目录存在
return fmt.Errorf("创建父目录失败: %w", err)
}
outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return fmt.Errorf("创建输出文件失败: %w", err)
}
rc, err := file.Open()
if err != nil {
outFile.Close()
return fmt.Errorf("打开ZIP文件内部文件失败: %w", err)
}
_, err = io.Copy(outFile, rc)
rc.Close()
outFile.Close()
if err != nil {
return fmt.Errorf("写入文件内容失败: %w", err)
}
}
return nil
}
// CompressToTarGz 将指定路径的文件或目录压缩成TAR.GZ文件
// sourcePath 可以是文件或目录
// destTarGzFile 是目标TAR.GZ文件的路径
func CompressToTarGz(sourcePath, destTarGzFile string) error {
tarGzFile, err := os.Create(destTarGzFile)
if err != nil {
return fmt.Errorf("创建TAR.GZ文件失败: %w", err)
}
defer tarGzFile.Close()
gzipWriter := gzip.NewWriter(tarGzFile)
defer gzipWriter.Close()
tarWriter := tar.NewWriter(gzipWriter)
defer tarWriter.Close()
info, err := os.Stat(sourcePath)
if err != nil {
return fmt.Errorf("获取源路径信息失败: %w", err)
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(sourcePath)
}
err = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 构建在TAR文件中的相对路径
headerPath := strings.TrimPrefix(path, sourcePath)
if baseDir != "" { // 如果源是目录,相对路径需要包含目录名
headerPath = filepath.Join(baseDir, headerPath)
}
// 移除开头的斜杠或点斜杠
headerPath = strings.TrimPrefix(headerPath, string(filepath.Separator))
headerPath = strings.TrimPrefix(headerPath, ".")
if headerPath == "" && info.IsDir() { // 避免根目录自身被添加为 ""
return nil
}
header, err := tar.FileInfoHeader(info, "") // linkname为空
if err != nil {
return fmt.Errorf("创建文件头失败: %w", err)
}
header.Name = headerPath // 使用我们构建的相对路径
if err := tarWriter.WriteHeader(header); err != nil {
return fmt.Errorf("写入TAR文件头失败: %w", err)
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
if err != nil {
return fmt.Errorf("写入文件内容失败: %w", err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("遍历文件时发生错误: %w", err)
}
return nil
}
// DecompressTarGz 将TAR.GZ文件解压到指定目录
func DecompressTarGz(tarGzFile, destDir string) error {
file, err := os.Open(tarGzFile)
if err != nil {
return fmt.Errorf("打开TAR.GZ文件失败: %w", err)
}
defer file.Close()
gzipReader, err := gzip.NewReader(file)
if err != nil {
return fmt.Errorf("创建GZIP读取器失败: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return fmt.Errorf("读取TAR文件头失败: %w", err)
}
// 避免路径穿越攻击
filePath := filepath.Join(destDir, header.Name)
if !strings.HasPrefix(filePath, filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("非法文件路径: %s", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(filePath, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("创建目录失败: %w", err)
}
case tar.TypeReg:
if err := os.MkdirAll(filepath.Dir(filePath), os.FileMode(header.Mode)); err != nil { // 确保父目录存在
return fmt.Errorf("创建父目录失败: %w", err)
}
outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return fmt.Errorf("创建输出文件失败: %w", err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return fmt.Errorf("写入文件内容失败: %w", err)
}
outFile.Close()
default:
// 忽略其他类型,例如符号链接、设备文件等,或者根据需求进行处理
fmt.Printf("忽略文件类型: %s, 名称: %s\n", string(header.Typeflag), header.Name)
}
}
return nil
}
func main() {
// 示例用法
// 创建一些测试文件和目录
os.MkdirAll("test_source/subdir", 0755)
os.WriteFile("test_source/file1.txt", []byte("Hello from file1"), 0644)
os.WriteFile("test_source/subdir/file2.txt", []byte("Hello from file2 in subdir"), 0644)
fmt.Println("--- ZIP 压缩与解压 ---")
zipFile := "archive.zip"
zipDestDir := "unzipped_zip"
fmt.Printf("压缩 'test_source' 到 '%s'\n", zipFile)
if err := CompressToZip("test_source", zipFile); err != nil {
fmt.Printf("ZIP压缩失败: %v\n", err)
} else {
fmt.Printf("ZIP压缩成功: %s\n", zipFile)
fmt.Printf("解压 '%s' 到 '%s'\n", zipFile, zipDestDir)
if err := DecompressZip(zipFile, zipDestDir); err != nil {
fmt.Printf("ZIP解压失败: %v\n", err)
} else {
fmt.Printf("ZIP解压成功到: %s\n", zipDestDir)
}
}
fmt.Println("\n--- TAR.GZ 压缩与解压 ---")
tarGzFile := "archive.tar.gz"
tarGzDestDir := "unzipped_targz"
fmt.Printf("压缩 'test_source' 到 '%s'\n", tarGzFile)
if err := CompressToTarGz("test_source", tarGzFile); err != nil {
fmt.Printf("TAR.GZ压缩失败: %v\n", err)
} else {
fmt.Printf("TAR.GZ压缩成功: %s\n", tarGzFile)
fmt.Printf("解压 '%s' 到 '%s'\n", tarGzFile, tarGzDestDir)
if err := DecompressTarGz(tarGzFile, tarGzDestDir); err != nil {
fmt.Printf("TAR.GZ解压失败: %v\n", err)
} else {
fmt.Printf("TAR.GZ解压成功到: %s\n", tarGzDestDir)
}
}
// 清理测试文件
os.RemoveAll("test_source")
os.RemoveAll(zipDestDir)
os.RemoveAll(tarGzDestDir)
os.Remove(zipFile)
os.Remove(tarGzFile)
}
处理大文件或大量小文件时,性能确实是个绕不开的话题。我个人在实践中发现,很多时候瓶颈并不在CPU的压缩/解压算法本身,而是在文件I/O上。
首先,流式处理是王道。无论是
zip
tar
io.Copy
其次,缓冲区大小的影响不可忽视。标准库内部通常会使用默认的缓冲区,但在某些特定场景下,比如网络传输或特定的磁盘特性,调整
bufio.Reader
bufio.Writer
立即学习“go语言免费学习笔记(深入)”;
再者,并发是把双刃剑。对于压缩,如果你有多个独立的目录或文件需要压缩,可以考虑为每个压缩任务启动一个goroutine。但要注意,如果它们最终都要写入同一个压缩文件,那么写入操作仍然需要同步,比如通过互斥锁或者通道来协调。解压时,如果压缩包内的文件是独立的,同样可以考虑并发解压,但前提是目标磁盘I/O能够跟上,否则反而可能因为I/O竞争而导致性能下降。我的经验是,除非文件数量极其庞大且独立性强,否则并发带来的管理开销可能抵消掉性能增益。
最后,错误处理和资源释放。这看起来和性能无关,但一个健壮的错误处理机制能防止资源泄露(比如文件句柄未关闭),而这些泄露在大规模操作时会累积,最终导致系统资源耗尽,从而间接影响性能甚至导致程序崩溃。
defer
选择
zip
tar.gz
Zip (.zip
tar
gzip
Tar.gz (.tar.gz
.tgz
tar
gzip
tar
gzip
gzip
tar.gz
tar.gz
我的选择偏好:
zip
tar.gz
处理压缩包时,文件
以上就是Golang压缩解压文件 zip/tar标准库实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号