最近要实现一个zip打包的功能,前端下载文件夹的时候,没办法一次性创建多个文件,所以需要打包后下载。压缩不压缩的倒是无所谓的。
因为zip应该就是最通用的压缩格式了,是开源的算法,并且win自带zip的解压。。。
查了一下以后发现,golang无论是标准库还是开源的一些库都好难用啊。
开源的github上这个库star很多 https://github.com/mholt/archiver
但是看起来并不好用,官方给的例子不符合我的需求,我下载的时候希望能忽略一些文件夹,比如node_modules,
官方给的例子用的这个FilesFromDisk API,只能改变映射的名字,不能忽略文件夹。
标准库里面的文档也很糟糕,看了例子也不会用。例子里面没有对文件夹怎么处理的步骤
最后在网上找到现成的代码了,然后我改了一下符合我的需求。
type ZipOpt struct {
SrcFile string
DestZip string
IgnorePatternList []string
DestWriter io.Writer //使用ZipBytes要传
}
func Zip(opt ZipOpt) error {
zipfile, err := os.Create(opt.DestZip)
if err != nil {
return err
}
defer zipfile.Close()
// 压缩后的路径映射
baseDir := path.Dir(opt.SrcFile)
archive := zip.NewWriter(zipfile)
defer archive.Close()
return ExploreDir(ExploreDirOption{
RootPath: opt.SrcFile,
IgnorePatternList: opt.IgnorePatternList,
},
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// 移除根路径之前的路径,这样映射后是正常的目录结构
header.Name = strings.TrimLeft(path, baseDir)
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return nil
})
}
写了一个递归遍历路径的方法
type ExploreDirOption struct {
RootPath string `yaml:"rootPath"` // 目标根目录
IgnorePatternList []string `yaml:"ignorePattern"` // 忽略路径,采用shell文件名模式匹配,在windows下\\会被当作路径分隔符
}
func ExploreDir(option ExploreDirOption, fn fs.WalkDirFunc) error {
filelist, err := os.ReadDir(option.RootPath)
if err != nil {
return err
}
for _, ff := range filelist {
currentpath := path.Join(option.RootPath, ff.Name())
if len(option.IgnorePatternList) > 0 {
matched, err := MatchReList(option.IgnorePatternList, currentpath)
if err != nil {
return err
}
// 匹配到的项目跳过
if matched {
continue
}
}
err = fn(currentpath, ff, nil)
if err != nil {
return err
}
if ff.IsDir() {
ExploreDir(ExploreDirOption{
RootPath: currentpath,
IgnorePatternList: option.IgnorePatternList,
}, func(path string, d fs.DirEntry, err error) error {
return fn(path, d, err)
})
}
}
return nil
}
然后因为我是用gin框架,生成临时文件太麻烦了,干脆压缩的内容直接写入响应体,所以改了一下
func ZipBytes(opt ZipOpt) error {
// 压缩后的路径映射
baseDir := path.Dir(opt.SrcFile)
if opt.DestWriter == nil {
return errors.New("DestWriter should not be nil")
}
archive := zip.NewWriter(opt.DestWriter)
defer archive.Close()
return ExploreDir(ExploreDirOption{
RootPath: opt.SrcFile,
IgnorePatternList: opt.IgnorePatternList,
},
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = strings.TrimLeft(path, baseDir)
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return nil
})
}
下面是gin里面的代码,匹配用的正则,golang filepath 库里面的match方法功能太简陋了,还不如用正则。
// 打包时忽略的文件
ignorePatternList := []string{"[\\/]node_modules[\\/]|[\\/]node_modules$"}
if req.IsFolder {
err := f.checkDirBeforeCompress(req.Path, DownloadDirCheckOption{
maxCount: 1000,
maxSize: 4 << 30, //4gb
ignorePatternList: ignorePatternList,
})
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip", url.QueryEscape(req.Name)))
err = util.ZipBytes(util.ZipOpt{
SrcFile: req.Path,
IgnorePatternList: ignorePatternList,
DestWriter: c.Writer,
})
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
}
还有设置压缩,官网也是给了一个例子
package main
import (
"archive/zip"
"bytes"
"compress/flate"
"io"
)
func main() {
// Override the default Deflate compressor with a higher compression level.
// Create a buffer to write our archive to.
buf := new(bytes.Buffer)
// Create a new zip archive.
w := zip.NewWriter(buf)
// Register a custom Deflate compressor.
w.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, flate.BestCompression)
})
// Proceed to add files to w.
}
最后我觉得如果真的有压缩文件的需求,还是直接用7z命令行吧。我这边只是为了凑合用,7z命令行也有问题就是要额外打包,会占用一定体积。
网友评论