前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang封装tar打包解包

golang封装tar打包解包

原创
作者头像
willsiom
发布2021-10-15 15:08:20
2.8K0
发布2021-10-15 15:08:20
举报
文章被收录于专栏:沈钦华的专栏

在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的结构

代码语言:javascript
复制
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'yong
func (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的写入句柄,通过目录遍历,将文件写入即可

代码语言:javascript
复制
// 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即可

代码语言:javascript
复制
// 对单个文件进行打包
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时,不需要带路径。这样解压时可以直接在将根目录解压到工作目录下

代码语言:javascript
复制
// 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创建对应的文件,再将文件内容写入

代码语言:javascript
复制
// 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
         }
      }
   }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 方案一:使用archive/tar库封装
  • 压缩打包
    • 单文件打包
      • 文件夹打包
      • 解包
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档