Go语言处理zip压缩还是比较方便的,可以直接使用Go标准库archive/zip。下面记录下具体使用方法,以及注意事项。

Go 处理zip解压

一般zip文件是来自于磁盘或者网络,不管处理磁盘还是网络中的zip文件首先都是读取文件数据

// 读取磁盘文件
func getFromDisk(filePath string) ([]byte, error)  {
	return os.ReadFile(filePath)
}

// 读取网络文件
func getZipFromNet(zipURL string) ([]byte, error) {
	rsp, err := http.Get(zipURL)
	if err != nil {
		return nil, err
	}
	rspBody, err := io.ReadAll(rsp.Body)
	if err != nil {
		return nil, err
	}
	defer rsp.Body.Close()
	return rspBody, nil
}

拿到数据后在使用标准库解压就好,需要注意的是要记得校验目录是否存在,如果不存在要先创建不然下面Open打开文件的时候会报错。

// @zipData 压缩数据
// @destDir 要解压的文件夹 
func unzip(zipData []byte, destDir string) error {
	zipReader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
	if err != nil {
		return err
	}
	for _, f := range zipReader.File {
		err := writeUnzipFile(f, destDir)
		if err != nil {
			fmt.Println(err)
			return err
		}
	}
	return nil
}

// isFileExist 文件或目录是否存在
func isFileExist(filePath string) bool {
	_, err := os.Stat(filePath)
	if err != nil {
		if os.IsExist(err) {
			return true
		}
		return false
	}
	return true
}

func writeUnzipFile(f *zip.File, destDir string) error {
	fName := f.Name
	destPath := filepath.Join(destDir, fName)
	// 判断文件夹是否存在,主要是处理zip包含多层文件目录的情况
	if f.FileInfo().IsDir() && !isFileExist(destPath) {
		err := os.MkdirAll(destPath, os.ModePerm)
		return err
	}
	// 创建要写入的文件
	fw, err := os.Open(destPath)
	if err != nil {
		return err
	}
	defer fw.Close()
	fr, err := f.Open()
	if err != nil {
		return err
	}
	defer fr.Close()
	_, err = io.Copy(fw, fr)
	return err
}

压缩文件

压缩文件就要用到zip writer了,这里用到filepath.Walk遍历目录下所有文件读取并写入到zip writer里,需要注意的是filepath.Walk方法会遍历目录自身,处理的时候要跳过。

// @toZipFilePath 要压缩的文件所在目录 绝对路径
// @destDir 生成压缩文件的目录
// @fileName 生成的压缩文件名称 如xxx.zip
func zipData(toZipFilePath string, destDir string, fileName string) error {
	if !isFileExist(destDir) {
		err := os.MkdirAll(destDir, os.ModePerm)
		return err
	}
	// 创建新的压缩文件
	archive, err := os.Create(destDir + "/" + fileName)
	if err != nil {
		return err

	}
	zipWriter := zip.NewWriter(archive)
	defer zipWriter.Close()
	err = filepath.Walk(toZipFilePath, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}
		fmt.Println("walk", path)
		// 跳过目录自身
		if path == toZipFilePath {
			return nil
		}
		// 获取zip包中的相对路径 比如要压缩的目录是/tmp/tozip
		// 要压缩的文件是/tmp/tozip/tozip.file
		// 则得到的zipPath = tozip.file
		// 保证压缩后文件目录结构和之前是一样的
		// 如果需要使用新的目录,可以根据需要自定义
		zipPath := path[len(toZipFilePath)+1:]
		if info.IsDir() {
			zipPath += "/"
		}
		w, err := zipWriter.Create(zipPath)
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		fr, err := os.Open(path)
		defer fr.Close()
		if err != nil {
			return err
		}
		_, err = io.Copy(w, fr)
		if err != nil {
			return err
		}
		return nil
	})
	// 在这里读取新的zip文件可能会出问题
	// 除非吧上面的defer zipWriter.Close()去掉,然后在这里先执行zipWriter.Close()在读取
	return err
}

处理压缩的时候在zipWriter close之前读取文件读到的数据可能会有问题,因为缓冲区的数据不一定写入完成。在这里被坑过一次,想要读取压缩后的文件重新上传到文件服务结果缓冲区没有flush导致读取的数据有问题。

在内存中将zip文件解压修改后重新压缩

直接将zip reader中解压后的文件重新写入zip writer也是可以的,如果需要修改zip包中的某些数据并重新压缩可以直接在内存中完成,不用先解压至磁盘,再从磁盘读取数据压缩。需要注意的是获取压缩数据前zipWriter要先close保证缓冲区数据都已写入。

func reZip(zipData []byte) ([]byte, error) {
	zipReader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	buf := []byte{}
	writer := bytes.NewBuffer(buf)
	zipWriter := zip.NewWriter(writer)
	for _, f := range zipReader.File {
		w, err := zipWriter.Create(f.Name)
		if err != nil {
			fmt.Println(err)
			return nil, err
		}
		fr, err := f.Open()
		if err != nil {
			fmt.Println(err)
			return nil, err
		}
		_, err = io.Copy(w, fr)
		if err != nil {
			fmt.Println(err)
			return nil, err
		}
	}
	zipWriter.Close()
	return writer.Bytes(), nil
}