golang标准库archive/tar实现打包压缩及解压
文章目录
- 前言
- 一、单个文件操作
- 1.单个文件打包示例
- 2.单个文件解包示例
- 二、目录示例
- 1.打包压缩
- 2.解包
- 补充
前言
这个包就是将文件进行打包和解包,通俗理解就是Linux 下的 tar 命令。 主要是通过 tar.Reader 读取 tar 包,通过 tar.Writer 写入 tar包,在写入的过程中再设置一下头,详细的过程以示例的方式进行展示,可以查看代码里面的注释。
标准库 tar 中文文档https://studygolang.com/static/pkgdoc/pkg/archive_tar.html
一、单个文件操作
1.单个文件打包示例
package main
import (
"os"
"log"
"archive/tar"
"fmt"
"io"
)
func main() {
// 准备打包的源文件
var srcFile = "sshd"
// 打包后的文件
var desFile = fmt.Sprintf("%s.tar",srcFile)
// 需要注意文件的打开即关闭的顺序,因为 defer 是后入先出,所以关闭顺序很重要
// 第一次写这个示例的时候就没注意,导致写完的 tar 包不完整
// ###### 第 1 步,先准备好一个 tar.Writer 结构,然后再向里面写入内容。 ######
// 创建一个文件,用来保存打包后的 passwd.tar 文件
fw, err := os.Create(desFile)
ErrPrintln(err)
defer fw.Close()
// 通过 fw 创建一个 tar.Writer
tw := tar.NewWriter(fw)
// 这里不要忘记关闭,如果不能成功关闭会造成 tar 包不完整
// 所以这里在关闭的同时进行判断,可以清楚的知道是否成功关闭
defer func() {
if err := tw.Close(); err != nil {
ErrPrintln(err)
}
}()
// ###### 第 2 步,处理文件信息,也就是 tar.Header 相关的 ######
// tar 包共有两部分内容:文件信息和文件数据
// 通过 Stat 获取 FileInfo,然后通过 FileInfoHeader 得到 hdr tar.*Header
fi, err := os.Stat(srcFile)
ErrPrintln(err)
hdr, err := tar.FileInfoHeader(fi, "")
// 将 tar 的文件信息 hdr 写入到 tw
err = tw.WriteHeader(hdr)
ErrPrintln(err)
// 将文件数据写入
// 打开准备写入的文件
fr, err := os.Open(srcFile)
ErrPrintln(err)
defer fr.Close()
written, err := io.Copy(tw, fr)
ErrPrintln(err)
log.Printf("共写入了 %d 个字符的数据\n",written)
}
// 定义一个用来打印的函数,少写点代码,因为要处理很多次的 err
// 后面其他示例还会继续使用这个函数,就不单独再写,望看到此函数了解
func ErrPrintln(err error) {
if err != nil {
log.Println(err)
os.Exit(1)
}
}
2.单个文件解包示例
package main
import (
"os"
"archive/tar"
"io"
"log"
)
func main() {
var srcFile = "passwd.tar"
// 将 tar 包打开
fr, err := os.Open(srcFile)
ErrPrintln(err)
defer fr.Close()
// 通过 fr 创建一个 tar.*Reader 结构,然后将 tr 遍历,并将数据保存到磁盘中
tr := tar.NewReader(fr)
for hdr, err := tr.Next(); err != io.EOF; hdr, err = tr.Next(){
// 处理 err != nil 的情况
ErrPrintln(err)
// 获取文件信息
fi := hdr.FileInfo()
// 创建一个空文件,用来写入解包后的数据
fw, err := os.Create(fi.Name())
ErrPrintln(err)
// 将 tr 写入到 fw
n, err := io.Copy(fw, tr)
ErrPrintln(err)
log.Printf("解包: %s 到 %s ,共处理了 %d 个字符的数据。", srcFile,fi.Name(),n)
// 设置文件权限,这样可以保证和原始文件权限相同,如果不设置,会根据当前系统的 umask 来设置。
os.Chmod(fi.Name(),fi.Mode().Perm())
// 注意,因为是在循环中,所以就没有使用 defer 关闭文件
// 如果想使用 defer 的话,可以将文件写入的步骤单独封装在一个函数中即可
fw.Close()
}
}
func ErrPrintln(err error){
if err != nil {
log.Fatalln(err)
os.Exit(1)
}
}
二、目录示例
打包整个目录,且打包的时候通过 gzip 或者 bzip2 压缩。如果要打包整个目录,可以通过递归的方式来实现。此处只演示 gzip 方式压缩,这个实现非常简单,只需要在 fw 和 tw 之前加上一层压缩即可
1.打包压缩
代码如下(示例):
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
)
func createTarFile(tarFilePath string, sourceDir string) error {
// 创建Tar文件
tarFile, err := os.Create(tarFilePath)
if err != nil {
return err
}
defer tarFile.Close()
// 将 tar 包使用 gzip 压缩,其实添加压缩功能很简单,
// 只需要在 fw 和 tw 之前加上一层压缩就行了,和 Linux 的管道的感觉类似
gw := gzip.NewWriter(tarFile)
defer gw.Close()
// 创建Tar写入器
tarWriter := tar.NewWriter(tarFile)
defer tarWriter.Close()
// 遍历源目录并添加文件到Tar
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 创建Tar头信息
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
// 设置文件路径
//这里的思路就是递归处理目录及目录下的所有文件和目录
header.Name, err = filepath.Rel(filepath.Dir(sourceDir), path)
fmt.Printf("header.Name:%s\n", header.Name)
if err != nil {
return err
}
// 写入头信息
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
// 如果是文件,写入文件内容
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
return err
}
return nil
})
return err
}
func main(){
tarFilePath := "output.tar"
sourceDir := "/etc/sshd/sshd_config"
err = createTarFile(tarFilePath, sourceDir)
if err != nil {
panic(err)
}
}
2.解包
代码如下(示例):
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
)
func main() {
var dst = "" // 不写就是解压到当前目录
var src = "log.tar.gz"
UnTar(dst, src)
}
func UnTar(dst, src string) (err error) {
// 打开准备解压的 tar 包
fr, err := os.Open(src)
if err != nil {
return
}
defer fr.Close()
// 将打开的文件先解压
gr, err := gzip.NewReader(fr)
if err != nil {
return
}
defer gr.Close()
// 通过 gr 创建 tar.Reader
tr := tar.NewReader(gr)
// 现在已经获得了 tar.Reader 结构了,只需要循环里面的数据写入文件就可以了
for {
hdr, err := tr.Next()
switch {
case err == io.EOF:
return nil
case err != nil:
return err
case hdr == nil:
continue
}
// 处理下保存路径,将要保存的目录加上 header 中的 Name
// 这个变量保存的有可能是目录,有可能是文件,所以就叫 FileDir 了……
dstFileDir := filepath.Join(dst, hdr.Name)
// 根据 header 的 Typeflag 字段,判断文件的类型
switch hdr.Typeflag {
case tar.TypeDir: // 如果是目录时候,创建目录
// 判断下目录是否存在,不存在就创建
if b := ExistDir(dstFileDir); !b {
// 使用 MkdirAll 不使用 Mkdir ,就类似 Linux 终端下的 mkdir -p,
// 可以递归创建每一级目录
if err := os.MkdirAll(dstFileDir, 0775); err != nil {
return err
}
}
case tar.TypeReg: // 如果是文件就写入到磁盘
// 创建一个可以读写的文件,权限就使用 header 中记录的权限
// 因为操作系统的 FileMode 是 int32 类型的,hdr 中的是 int64,所以转换下
file, err := os.OpenFile(dstFileDir, os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode))
if err != nil {
return err
}
n, err := io.Copy(file, tr)
if err != nil {
return err
}
// 将解压结果输出显示
fmt.Printf("成功解压: %s , 共处理了 %d 个字符\n", dstFileDir, n)
// 不要忘记关闭打开的文件,因为它是在 for 循环中,不能使用 defer
// 如果想使用 defer 就放在一个单独的函数中
file.Close()
}
}
return nil
}
// 判断目录是否存在
func ExistDir(dirname string) bool {
fi, err := os.Stat(dirname)
return (err == nil || os.IsExist(err)) && fi.IsDir()
}
补充
1、archive/tar打包和解包的操作只能在当前服务器上执行,不能再远程服务器上操作,例如:你ssh到一台远程机器上执行这个打包操作,就会失败,实际上操作的还是当前机器
2、在使用 tar.Writer 时,需要使用 tar.Header 结构体设置文件的元信息,包括文件名、大小等。
3、在读取 tar 归档文件时,可以通过 tar.Reader 的 Next 方法获取下一个文件的头信息,并使用 io.Copy 复制文件内容