archive/zip 实现压缩及解压

作者: BroQiang

发布于 2018-06-29 | 最后更新 2018-06-29


看标准库文档,就会发现, archive/zip 和 archive/tar 看起来方法名什么的都很像。使用起来也差不多,如果是按照我的文档顺序看到的这篇文档,上一篇 tar 中已经都介绍过了,这里就不再做过多的说明。

压缩

和 tar 的过程很像,只有些小的差别,详情见示例代码。

示例代码( zip.go ):

package main

import (
	"archive/zip"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
)

func main() {
	// 源档案(准备压缩的文件或目录)
	var src = "log"
	// 目标文件,压缩后的文件
	var dst = "log.zip"

	if err := Zip(dst, src); err != nil {
		log.Fatalln(err)
	}
}

func Zip(dst, src string) (err error) {
	// 创建准备写入的文件
	fw, err := os.Create(dst)
	defer fw.Close()
	if err != nil {
		return err
	}

	// 通过 fw 来创建 zip.Write
	zw := zip.NewWriter(fw)
	defer func() {
		// 检测一下是否成功关闭
		if err := zw.Close(); err != nil {
			log.Fatalln(err)
		}
	}()

	// 下面来将文件写入 zw ,因为有可能会有很多个目录及文件,所以递归处理
	return filepath.Walk(src, func(path string, fi os.FileInfo, errBack error) (err error) {
		if errBack != nil {
			return errBack
		}

		// 通过文件信息,创建 zip 的文件信息
		fh, err := zip.FileInfoHeader(fi)
		if err != nil {
			return
		}

		// 替换文件信息中的文件名
		fh.Name = strings.TrimPrefix(path, string(filepath.Separator))

		// 这步开始没有加,会发现解压的时候说它不是个目录
		if fi.IsDir() {
			fh.Name += "/"
		}

		// 写入文件信息,并返回一个 Write 结构
		w, err := zw.CreateHeader(fh)
		if err != nil {
			return
		}

		// 检测,如果不是标准文件就只写入头信息,不写入文件数据到 w
		// 如目录,也没有数据需要写
		if !fh.Mode().IsRegular() {
			return nil
		}

		// 打开要压缩的文件
		fr, err := os.Open(path)
		defer fr.Close()
		if err != nil {
			return
		}

		// 将打开的文件 Copy 到 w
		n, err := io.Copy(w, fr)
		if err != nil {
			return
		}
		// 输出压缩的内容
		fmt.Printf("成功压缩文件: %s, 共写入了 %d 个字符的数据\n", path, n)

		return nil
	})
}

解压缩

直接看代码( unzip.go )吧:

package main

import (
	"archive/zip"
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
)

func main() {
	// 压缩包
	var src = "log.zip"
	// 解压后保存的位置,为空表示当前目录
	var dst = ""

	if err := UnZip(dst, src); err != nil {
		log.Fatalln(err)
	}
}

func UnZip(dst, src string) (err error) {
	// 打开压缩文件,这个 zip 包有个方便的 ReadCloser 类型
	// 这个里面有个方便的 OpenReader 函数,可以比 tar 的时候省去一个打开文件的步骤
	zr, err := zip.OpenReader(src)
	defer zr.Close()
	if err != nil {
		return
	}

	// 如果解压后不是放在当前目录就按照保存目录去创建目录
	if dst != "" {
		if err := os.MkdirAll(dst, 0755); err != nil {
			return err
		}
	}

	// 遍历 zr ,将文件写入到磁盘
	for _, file := range zr.File {
		path := filepath.Join(dst, file.Name)

		// 如果是目录,就创建目录
		if file.FileInfo().IsDir() {
			if err := os.MkdirAll(path, file.Mode()); err != nil {
				return err
			}
			// 因为是目录,跳过当前循环,因为后面都是文件的处理
			continue
		}

		// 获取到 Reader
		fr, err := file.Open()
		if err != nil {
			return err
		}

		// 创建要写出的文件对应的 Write
		fw, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, file.Mode())
		if err != nil {
			return err
		}

		n, err := io.Copy(fw, fr)
		if err != nil {
			return err
		}

		// 将解压的结果输出
		fmt.Printf("成功解压 %s ,共写入了 %d 个字符的数据\n", path, n)

		// 因为是在循环中,无法使用 defer ,直接放在最后
		// 不过这样也有问题,当出现 err 的时候就不会执行这个了,
		// 可以把它单独放在一个函数中,这里是个实验,就这样了
		fw.Close()
		fr.Close()
	}
	return nil
}