golang封装tar打包解包
在golang项目中,需要对文件夹进行tar.gz打包然后分发。搜了下github,没有找到现成可用的库,只好自己进行封装。这里想到了2个实现方案:
1、使用官方的archive/tar库,自行实现压缩打包和解包的过程;
2、通过os/exec调用shell命令,直接调用系统的tar命令进行打包;
这里先介绍下方案一的实现,有需要的老铁可以参考。方案二在另外篇幅说明
方案一:使用archive/tar库封装
这个方案实现起来也不难,大体思路是打包时遍历目录的所有文件,通过tar.Writer写入到tar包,在写入的过程中处理下header的信息。解包则通过tar.Reader读取tar包的信息,根据header.Name创建文件然后将内容拷贝进去。
archive/tar库官方文档: https://pkg.go.dev/archive/tar
声明一个TgzPacker的结构
type TgzPacker struct {}func NewTgzPacker() *TgzPacker { return &TgzPacker{ }}// 打包时如果目标的tar文件已经存在,则删除掉func (tp *TgzPacker) removeTargetFile(fileName string) (err error) { // 判断是否存在同名目标文件 if _, err := os.Stat(fileName); os.IsNotExist(err) { return nil } return os.Remove(fileName)}// 判断目录是否存在,在解压的逻辑会shi'yongfunc (tp *TgzPacker) dirExists(dir string) bool { info, err := os.Stat(dir) return (err == nil || os.IsExist(err)) && info.IsDir()}
压缩打包
首先看打包的逻辑,先创建一个文件写入句柄,因为需要使用gzip压缩能力,所以通过gzip.NewWriter包装一层,最后通过tar.NewWriter创建tar的写入句柄,通过目录遍历,将文件写入即可
// Pack 压缩,这里的sourceFullPath可能是单个文件,也可能是个目录func (tp *TgzPacker) Pack(sourceFullPath string, tarFileName string) (err error) { sourceInfo, err := os.Stat(sourceFullPath) // 校验源目录是否存在 if err != nil { return err } // 删除目标tar文件 if err = tp.removeTargetFile(tarFileName); err != nil { return err } // 创建写入文件句柄 file, err := os.Create(tarFileName) if err != nil { return err } defer func() { // 主程序没有err,但是关闭句柄报错,则将关闭句柄的报错返回 if err2 := file.Close(); err2 != nil && err == nil { err = err2 } }() // 创建gzip的写入句柄,对file的包装 gWriter := gzip.NewWriter(file) defer func() { // 主程序没有err,但是关闭句柄报错,则将关闭句柄的报错返回 if err2 := gWriter.Close(); err2 != nil && err == nil { err = err2 } }() // 创建tar的写入句柄,对gzip的包装 tarWriter := tar.NewWriter(gWriter) defer func() { // 主程序没有err,但是关闭句柄报错,则将关闭句柄的报错返回 if err2 := tarWriter.Close(); err2 != nil && err == nil { err = err2 } }() // 开始压缩 if sourceInfo.IsDir() { return tp.tarFolder(sourceFullPath, filepath.Base(sourceFullPath), tarWriter) } return tp.tarFile(sourceFullPath, tarWriter)}
单文件打包
单个文件的打包比较简单,直接读取源文件,写入tarWriter即可
// 对单个文件进行打包func (tp *TgzPacker) tarFile(sourceFullFile string, writer *tar.Writer) error { info, err := os.Stat(sourceFullFile) if err != nil { return err } // 创建头信息 header, err := tar.FileInfoHeader(info, "") if err != nil { return err } // 头信息写入 err = writer.WriteHeader(header) if err != nil { return err } // 读取源文件,将内容拷贝到tar.Writer中 fr, err := os.Open(sourceFullFile) if err != nil { return err } defer func() { // 如果主程序的err为空nil,而文件句柄关闭err,则将关闭句柄的err返回 if err2 := fr.Close(); err2 != nil && err == nil { err = err2 } }() if _, err = io.Copy(writer, fr); err != nil { return err } return nil}
文件夹打包
文件夹的打包逻辑也很简单,直接遍历文件夹下的所有文件,不过跟单文件打包有2个需要主要的地方:
1、header需要对Name进行处理,需要将name整理为相对根目录的带路径的文件名
2、待打包的根目录,在处理header的Name时,不需要带路径。这样解压时可以直接在将根目录解压到工作目录下
// sourceFullPath为待打包目录,baseName为待打包目录的根目录名称func (tp *TgzPacker) tarFolder(sourceFullPath string, baseName string, writer *tar.Writer) error { // 保留最开始的原始目录,用于目录遍历过程中将文件由绝对路径改为相对路径 baseFullPath := sourceFullPath return filepath.Walk(sourceFullPath, func(fileName string, info fs.FileInfo, err error) error { if err != nil { return err } // 创建头信息 header, err := tar.FileInfoHeader(info, "") if err != nil { return err } // 修改header的name,这里需要按照相对路径来 // 说明这里是根目录,直接将目录名写入header即可 if fileName == baseFullPath { header.Name = baseName } else { // 非根目录,需要对路径做处理:去掉绝对路径的前半部分,然后构造基于根目录的相对路径 header.Name = filepath.Join(baseName, strings.TrimPrefix(fileName, baseFullPath)) } if err = writer.WriteHeader(header); err != nil { return err } // linux文件有很多类型,这里仅处理普通文件,如业务需要处理其他类型的文件,这里添加相应的处理逻辑即可 if !info.Mode().IsRegular() { return nil } // 普通文件,则创建读句柄,将内容拷贝到tarWriter中 fr, err := os.Open(fileName) if err != nil { return err } defer fr.Close() if _, err := io.Copy(writer, fr); err != nil { return err } return nil })}
解包
解包的总体逻辑基本和压缩的逻辑反过来即可,即遍历tar包内的header,通过header.Name创建对应的文件,再将文件内容写入
// tarFileName为待解压的tar包,dstDir为解压的目标目录func (tp *TgzPacker) UnPack(tarFileName string, dstDir string) (err error) { // 打开tar文件 fr, err := os.Open(tarFileName) if err != nil { return err } defer func() { if err2 := fr.Close(); err2 != nil && err == nil { err = err2 } }() // 使用gzip解压 gr, err := gzip.NewReader(fr) if err != nil { return err } defer func() { if err2 := gr.Close(); err2 != nil && err == nil { err = err2 } }() // 创建tar reader tarReader := tar.NewReader(gr) // 循环读取 for { header, err := tarReader.Next() switch { // 读取结束 case err == io.EOF: return nil case err != nil: return err case header == nil: continue } // 因为指定了解压的目录,所以文件名加上路径 targetFullPath := filepath.Join(dstDir, header.Name) // 根据文件类型做处理,这里只处理目录和普通文件,如果需要处理其他类型文件,添加case即可 switch header.Typeflag { case tar.TypeDir: // 是目录,不存在则创建 if exists := tp.dirExists(targetFullPath); !exists { if err = os.MkdirAll(targetFullPath, 0755); err != nil { return err } } case tar.TypeReg: // 是普通文件,创建并将内容写入 file, err := os.OpenFile(targetFullPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { return err } _, err = io.Copy(file, tarReader) // 循环内不能用defer,先关闭文件句柄 if err2 := file.Close(); err2 != nil { return err2 } // 这里再对文件copy的结果做判断 if err != nil { return err } } }}