Go 和许多其他编程语言一样,标准库支持 zip 文件的压缩和解压。
在本文中,我们将介绍如何在 Go 中利用标准库包 archive/zip 完成 zip 文件的创建和提取。
1.压缩
第一步:创建一个 zip 基础文件。
zip 文件也是一个文件,我们要做的第一件事是创建为一个简单的文件作为 zip 文件,就像在 Go 中处理任何其他文件一样。使用 os package 的 os.Create() 函数创建一个文件对象。
func Create(name string) (*File, error)
第二步:初始化 zip.Writer。
使用 archive/zip 包中的 zip.NewWriter 函数初始化一个 zip.Writer,用于将数据(文件和目录)写入 zip 文件。
func NewWriter(w io.Writer) *Writer
第三步:使用 zip.Writer.Create 创建一个 io.Writer。
一旦创建完 zip writer 后,便可以使用 zip.Writer.Create 向 zip 文件中添加一个文件或目录。它返回一个待压缩的文件内容应写入的 Writer,文件内容将使用 Deflate 方法进行压缩。
func (w *Writer) Create(name string) (io.Writer, error)
第四步:使用 io.Copy 或 io.Writer.Write 写数据到到 zip 文件。
zip.Writer.Create 函数返回一个 io.Writer ,因此任何文件内容都可以通过流式写入该 writer。使用 io.Copy 或调用 writer 的 Write 方法。
func Copy(dst Writer, src Reader) (written int64, err error)
第五步:使用 zip.Writer.Close 关闭 zip 文件。
将所有文件和目录写入 zip 文件后,可以通过 zip.Writer.Close 方法关闭 zip writer,将所有数据写入指向基础 zip 文件的数据流。注意,它不会关闭指向基础 zip 文件的 Writer。
func (w *Writer) Close() error
下面给出示例代码:
package main
import (
"archive/zip"
"fmt"
"io"
"os"
)
func main() {
fmt.Println("creating zip archive...")
archive, err := os.Create("archive.zip")
if err != nil {
panic(err)
}
defer archive.Close()
zipWriter := zip.NewWriter(archive)
fmt.Println("opening first file...")
f1, err := os.Open("test.csv")
if err != nil {
panic(err)
}
defer f1.Close()
fmt.Println("writing first file to archive...")
w1, err := zipWriter.Create("csv/test.csv")
if err != nil {
panic(err)
}
if _, err := io.Copy(w1, f1); err != nil {
panic(err)
}
fmt.Println("opening second file")
f2, err := os.Open("test.txt")
if err != nil {
panic(err)
}
defer f2.Close()
fmt.Println("writing second file to archive...")
w2, err := zipWriter.Create("txt/test.txt")
if err != nil {
panic(err)
}
if _, err := io.Copy(w2, f2); err != nil {
panic(err)
}
fmt.Println("closing zip archive...")
zipWriter.Close()
}
运行输出:
creating zip archive...
opening first file...
writing first file to archive...
opening second file
writing second file to archive...
closing zip archive...
最终的 zip 存档包含预期的文件。
unzip -l archive.zip
Archive: archive.zip
Length Date Time Name
--------- ---------- ----- ----
57 00-00-1980 00:00 csv/test.csv
12 00-00-1980 00:00 txt/test.txt
--------- -------
69 2 files
2.解压缩
利用标准库 archive/zip 包可以创建 zip 文件,同样地也可以完成对 zip 文件的解压缩。下面让我们看看如何在 Go 中解压 zip 文件。
第一步:使用 zip.OpenReader 打开 zip 文件。
要想解压 zip 文件我们可能需要做的第一件事是打开它。我们可以使用 archive/zip 包提供的函数 zip.OpenReader 来打开 zip 文件,以 io.ReadCloser 的形式返回一个 zip.Reader 的实例。
func OpenReader(name string) (*ReadCloser, error)
第二步:循环访问 zip 中的文件。
zip.OpenReader 返回一个 zip.Reader 实例,其包含一个 zip.File 切片。
type Reader struct {
File []*File
Comment string
// contains filtered or unexported fields
}
第三步:使用 zip.File.Open 方法读取 zip 中文件的内容。
func (f *File) Open() (io.ReadCloser, error)
第四步:使用 io.Copy 或 io.Writer.Write 保存解压后的文件内容。
func Copy(dst Writer, src Reader) (written int64, err error)
第五步:使用 zip.Reader.Close 关闭 zip 文件。
读取完 zip 存档中所有文件的内容并保存到指定位置后,要用接口 ReadCloser 中的方法 Close 关闭文件句柄。
type ReadCloser interface {
Reader
Closer
}
// Closer is the interface that wraps the basic Close method.
// The behavior of Close after the first call is undefined.
// Specific implementations may document their own behavior.
type Closer interface {
Close() error
}
下面给出示例,我们尝试解压缩包含 2 个文件的 zip 文件。有一个 txt 文件和一个 csv 文件,它们将被解压缩到选定的输出目录中。
package main
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func main() {
dst := "output"
archive, err := zip.OpenReader("archive.zip")
if err != nil {
panic(err)
}
defer archive.Close()
for _, f := range archive.File {
filePath := filepath.Join(dst, f.Name)
fmt.Println("unzipping file ", filePath)
if !strings.HasPrefix(filePath, filepath.Clean(dst)+string(os.PathSeparator)) {
fmt.Println("invalid file path")
return
}
if f.FileInfo().IsDir() {
fmt.Println("creating directory...")
os.MkdirAll(filePath, os.ModePerm)
continue
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
panic(err)
}
dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
panic(err)
}
fileInArchive, err := f.Open()
if err != nil {
panic(err)
}
if _, err := io.Copy(dstFile, fileInArchive); err != nil {
panic(err)
}
dstFile.Close()
fileInArchive.Close()
}
}
运行输出:
unzipping file output/csv/test.csv
unzipping file output/txt/test.txt
3.进一步封装
上面详细讲解了压缩与解压缩的操作,并给出了相应的示例。
为了更好地复用上面的代码,下面做了进一步的封装,实现两个压缩与解压缩的函数。
3.1 压缩
import (
"archive/zip"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
// Zip compresses the specified files or dirs to zip archive.
// If a path is a dir don't need to specify the trailing path separator.
// For example calling Zip("archive.zip", "dir", "csv/baz.csv") will get archive.zip and the content of which is
// baz.csv
// dir
// ├── bar.txt
// └── foo.txt
// Note that if a file is a symbolic link it will be skipped.
func Zip(zipPath string, paths ...string) error {
// Create zip file and it's parent dir.
if err := os.MkdirAll(filepath.Dir(zipPath), os.ModePerm); err != nil {
return err
}
archive, err := os.Create(zipPath)
if err != nil {
return err
}
defer archive.Close()
// New zip writer.
zipWriter := zip.NewWriter(archive)
defer zipWriter.Close()
// Traverse the file or directory.
for _, rootPath := range paths {
// Remove the trailing path separator if path is a directory.
rootPath = strings.TrimSuffix(rootPath, string(os.PathSeparator))
// Visit all the files or directories in the tree.
err = filepath.Walk(rootPath, walkFunc(rootPath, zipWriter))
if err != nil {
return err
}
}
return nil
}
func walkFunc(rootPath string, zipWriter *zip.Writer) filepath.WalkFunc {
return func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// If a file is a symbolic link it will be skipped.
if info.Mode()&os.ModeSymlink != 0 {
return nil
}
// Create a local file header.
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// Set compression method.
header.Method = zip.Deflate
// Set relative path of a file as the header name.
header.Name, err = filepath.Rel(filepath.Dir(rootPath), path)
if err != nil {
return err
}
if info.IsDir() {
header.Name += string(os.PathSeparator)
}
// Create writer for the file header and save content of the file.
headerWriter, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(headerWriter, f)
return err
}
}
如将一个目录和一个文件添加到当前目录指定名称的 zip 文件。
func main() {
err := Zip("archive.zip", "dir", "baz.csv")
fmt.Println("err is", err)
}
我们可以查看一下生成的 zip 文件的内容。
unzip -l archive.zip
Archive: archive.zip
Length Date Time Name
--------- ---------- ----- ----
0 01-15-2022 20:17 dir/
0 01-15-2022 20:17 dir/bar.txt
0 01-15-2022 20:17 dir/foo.txt
0 01-15-2022 20:30 baz.csv
--------- -------
0 4 files
3.2 解压
// Unzip decompresses a zip file to specified directory.
// Note that the destination directory don't need to specify the trailing path separator.
// If the destination directory doesn't exist, it will be created automatically.
func Unzip(zipath, dir string) error {
// Open zip file.
reader, err := zip.OpenReader(zipath)
if err != nil {
return err
}
defer reader.Close()
for _, file := range reader.File {
if err := unzipFile(file, dir); err != nil {
return err
}
}
return nil
}
func unzipFile(file *zip.File, dir string) error {
// Prevent path traversal vulnerability.
// Such as if the file name is "../../../path/to/file.txt" which will be cleaned to "path/to/file.txt".
name := strings.TrimPrefix(filepath.Join(string(filepath.Separator), file.Name), string(filepath.Separator))
filePath := path.Join(dir, name)
// Create the directory of file.
if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
return err
}
return nil
}
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return err
}
// Open the file.
r, err := file.Open()
if err != nil {
return err
}
defer r.Close()
// Create the file.
w, err := os.Create(filePath)
if err != nil {
return err
}
defer w.Close()
// Save the decompressed file content.
_, err = io.Copy(w, r)
return err
}
比如将上面创建的 archive.zip 文件解压到当前目录。
func main() {
err := Unzip("archive.zip", ".")
fmt.Println("err is", err)
}
4.dablelv/cyan
本文实现的压缩与解压缩函数已放置开源库 dablelv/cyan,可 import 直接使用。
package main
import (
"fmt"
"github.com/dablelv/cyan/zip"
)
func main() {
// 注意,该函数不支持符号链接,如果是符号链接将被忽略。
_ = zip.Zip("archive.zip", "dir", "baz.csv")
_ = zip.Unzip("archive.zip", ".")
// 该函数支持符号链接。
_ = zip.ZipFollowSymlink("archive.zip", "dir", "baz.csv")
}
欢迎大家 Star & PR。
参考文献
Golang Zip File Example
Golang Unzip File Example
Zip a file in Go
Unzip a file in Go