golang封装tar打包解包

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         }      }   }}