golang标准库archive/tar实现打包压缩及解压


文章目录


前言

这个包就是将文件进行打包和解包,通俗理解就是Linux 下的 tar 命令。 主要是通过 tar.Reader 读取 tar 包,通过 tar.Writer 写入 tar包,在写入的过程中再设置一下头,详细的过程以示例的方式进行展示,可以查看代码里面的注释。

标准库 tar 中文文档https://studygolang.com/static/pkgdoc/pkg/archive_tar.html


一、单个文件操作

1.单个文件打包示例

go 复制代码
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.单个文件解包示例

go 复制代码
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.打包压缩

代码如下(示例):

go 复制代码
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.解包

代码如下(示例):

go 复制代码
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()
}

补充

yaml 复制代码
1、archive/tar打包和解包的操作只能在当前服务器上执行,不能再远程服务器上操作,例如:你ssh到一台远程机器上执行这个打包操作,就会失败,实际上操作的还是当前机器
2、在使用 tar.Writer 时,需要使用 tar.Header 结构体设置文件的元信息,包括文件名、大小等。
3、在读取 tar 归档文件时,可以通过 tar.Reader 的 Next 方法获取下一个文件的头信息,并使用 io.Copy 复制文件内容

相关推荐
Yana.nice22 分钟前
Linux 只保留 30 天内日志(find命令删除日志文件)
linux·运维·chrome
码智社37 分钟前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海39 分钟前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖2 小时前
JDK 26 新特性详解
java·开发语言
马优晨2 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
2601_960567963 小时前
电商套图自动化效率的工程量化分析——从逐张生成到批量套图的架构演进
运维·架构·自动化
吳所畏惧4 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
人邮异步社区4 小时前
怎么把C语言学到精通?
c语言·开发语言
HiDev_4 小时前
【非标自动化】2、认识元器件(确定目标)
运维·自动化
心平气和量大福大5 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf