Golang創建 .tar.gz 壓縮包
tar 包實現了文件的打包功能,可以將多個文件或目錄存儲到單一的 .tar 文件中,tar 本身不具有壓縮功能,只能打包文件或目錄:
import "archive/tar"
這里以打包單個文件為例進行解說,后面會給出打包整個目錄的詳細示例。
向 tar 文件中寫入數據是通過 tar.Writer 完成的,所以首先要創建 tar.Writer,可以通過 tar.NewWriter 方法來創建它,該方法要求提供一個 os.Writer 對象,以便將打包后的數據寫入該對象中。可以先創建一個文件,然后將該文件提供給 tar.NewWriter 使用。這樣就可以將打包后的數據寫入文件中:
// 創建空文件 fw 用於保存打包后的數據
// dstTar 是要創建的 .tar 文件的完整路徑
fw, err := os.Create(dstTar)
if err != nil {
return err
}
defer fw.Close()
// 通過 fw 創建 tar.Writer 對象
tw := tar.NewWriter(fw)
defer tw.Close()
此時,我們就擁有了一個 tar.Writer 對象 tw,可以用它來打包文件了。這里要注意一點,使用完 tw 后,一定要執行 tw.Close() 操作,因為 tar.Writer 使用了緩存,tw.Close() 會將緩存中的數據寫入到文件中,同時 tw.Close() 還會向 .tar 文件的最后寫入結束信息,如果不關閉 tw 而直接退出程序,那么將導致 .tar 文件不完整。
存儲在 .tar 文件中的每個文件都由兩部分組成:文件信息和文件內容,所以向 .tar 文件中寫入每個文件都要分兩步:第一步寫入文件信息,第二步寫入文件數據。對於目錄來說,由於沒有內容可寫,所以只需要寫入目錄信息即可。
文件信息由 tar.Header 結構體定義:
type Header struct {
Name string // 文件名稱
Mode int64 // 文件的權限和模式位
Uid int // 文件所有者的用戶 ID
Gid int // 文件所有者的組 ID
Size int64 // 文件的字節長度
ModTime time.Time // 文件的修改時間
Typeflag byte // 文件的類型
Linkname string // 鏈接文件的目標名稱
Uname string // 文件所有者的用戶名
Gname string // 文件所有者的組名
Devmajor int64 // 字符設備或塊設備的主設備號
Devminor int64 // 字符設備或塊設備的次設備號
AccessTime time.Time // 文件的訪問時間
ChangeTime time.Time // 文件的狀態更改時間
}
我們首先將被打包文件的信息填入 tar.Header 結構體中,然后再將結構體寫入 .tar 文件中。這樣就完成了第一步(寫入文件信息)操作。
在 tar 包中有一個很方便的函數 tar.FileInfoHeader,它可以直接通過 os.FileInfo 創建 tar.Header,並自動填寫 tar.Header 中的大部分信息,當然,還有一些信息無法從 os.FileInfo 中獲取,所以需要你自己去補充:
// 獲取文件信息
// srcFile 是要打包的文件的完整路徑
fi, err := os.Stat(srcFile)
if err != nil {
return err
}
// 根據 os.FileInfo 創建 tar.Header 結構體
hdr, err := tar.FileInfoHeader(fi, "")
if err != nil {
return err
}
這里的 hdr 就是文件信息結構體,已經填寫完畢。如果你要填寫的更詳細,你可以自己將 hdr 補充完整。
下面通過 tw.WriteHeader 方法將 hdr 寫入 .tar 文件中(tw 是我們剛才創建的 tar.Writer):
// 將 tar.Header 寫入 .tar 文件中
err = tw.WriteHeader(hdr)
if err != nil {
return err
}
至此,第一步(寫入文件信息)操作完畢,下面開始第二步(寫入文件數據)操作,寫入數據很簡單,通過 tw.Write 方法寫入數據即可:
// 打開要打包的文件准備讀取
fr, err := os.Open(srcFile)
if err != nil {
return err
}
defer fr.Close()
// 將文件數據寫入 .tar 文件中,這里通過 io.Copy 函數實現數據的寫入
_, err = io.Copy(tw, fr)
if err != nil {
return err
}
下面說說解包的方法,從 .tar 文件中讀出數據是通過 tar.Reader 完成的,所以首先要創建 tar.Reader,可以通過 tar.NewReader 方法來創建它,該方法要求提供一個 os.Reader 對象,以便從該對象中讀出數據。可以先打開一個 .tar 文件,然后將該文件提供給 tar.NewReader 使用。這樣就可以將 .tar 文件中的數據讀出來了:
// 打開要解包的文件,srcTar 是要解包的 .tar 文件的路徑
fr, er := os.Open(srcTar)
if er != nil {
return er
}
defer fr.Close()
// 創建 tar.Reader,准備執行解包操作
tr := tar.NewReader(fr)
此時,我們就擁有了一個 tar.Reader 對象 tr,可以用 tr.Next() 來遍歷包中的文件,然后將文件的數據保存到磁盤中:
// 遍歷包中的文件
for hdr, er := tr.Next(); er != io.EOF; hdr, er = tr.Next() {
if er != nil {
return er
}
// 獲取文件信息
fi := hdr.FileInfo()
// 創建空文件,准備寫入解壓后的數據
fw, _ := os.Create(dstFullPath)
if er != nil {
return er
}
defer fw.Close()
// 寫入解壓后的數據
_, er = io.Copy(fw, tr)
if er != nil {
return er
}
// 設置文件權限
os.Chmod(dstFullPath, fi.Mode().Perm())
}
至此,單個文件的打包和解包都實現了。要打包和解包整個目錄,可以通過遞歸的方法實現,下面給出完整的代碼:
============================================================
package main
import (
"archive/tar"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
)
func main() {
TarFile := "test.tar"
src := "test"
dstDir := "test_ext"
if err := Tar(src, TarFile, false); err != nil {
fmt.Println(err)
}
if err := UnTar(TarFile, dstDir); err != nil {
fmt.Println(err)
}
}
// 將文件或目錄打包成 .tar 文件
// src 是要打包的文件或目錄的路徑
// dstTar 是要生成的 .tar 文件的路徑
// failIfExist 標記如果 dstTar 文件存在,是否放棄打包,如果否,則會覆蓋已存在的文件
func Tar(src string, dstTar string, failIfExist bool) (err error) {
// 清理路徑字符串
src = path.Clean(src)
// 判斷要打包的文件或目錄是否存在
if !Exists(src) {
return errors.New("要打包的文件或目錄不存在:" + src)
}
// 判斷目標文件是否存在
if FileExists(dstTar) {
if failIfExist { // 不覆蓋已存在的文件
return errors.New("目標文件已經存在:" + dstTar)
} else { // 覆蓋已存在的文件
if er := os.Remove(dstTar); er != nil {
return er
}
}
}
// 創建空的目標文件
fw, er := os.Create(dstTar)
if er != nil {
return er
}
defer fw.Close()
// 創建 tar.Writer,執行打包操作
tw := tar.NewWriter(fw)
defer func() {
// 這里要判斷 tw 是否關閉成功,如果關閉失敗,則 .tar 文件可能不完整
if er := tw.Close(); er != nil {
err = er
}
}()
// 獲取文件或目錄信息
fi, er := os.Stat(src)
if er != nil {
return er
}
// 獲取要打包的文件或目錄的所在位置和名稱
srcBase, srcRelative := path.Split(path.Clean(src))
// 開始打包
if fi.IsDir() {
tarDir(srcBase, srcRelative, tw, fi)
} else {
tarFile(srcBase, srcRelative, tw, fi)
}
return nil
}
// 因為要執行遍歷操作,所以要單獨創建一個函數
func tarDir(srcBase, srcRelative string, tw *tar.Writer, fi os.FileInfo) (err error) {
// 獲取完整路徑
srcFull := srcBase + srcRelative
// 在結尾添加 "/"
last := len(srcRelative) - 1
if srcRelative[last] != os.PathSeparator {
srcRelative += string(os.PathSeparator)
}
// 獲取 srcFull 下的文件或子目錄列表
fis, er := ioutil.ReadDir(srcFull)
if er != nil {
return er
}
// 開始遍歷
for _, fi := range fis {
if fi.IsDir() {
tarDir(srcBase, srcRelative+fi.Name(), tw, fi)
} else {
tarFile(srcBase, srcRelative+fi.Name(), tw, fi)
}
}
// 寫入目錄信息
if len(srcRelative) > 0 {
hdr, er := tar.FileInfoHeader(fi, "")
if er != nil {
return er
}
hdr.Name = srcRelative
if er = tw.WriteHeader(hdr); er != nil {
return er
}
}
return nil
}
// 因為要在 defer 中關閉文件,所以要單獨創建一個函數
func tarFile(srcBase, srcRelative string, tw *tar.Writer, fi os.FileInfo) (err error) {
// 獲取完整路徑
srcFull := srcBase + srcRelative
// 寫入文件信息
hdr, er := tar.FileInfoHeader(fi, "")
if er != nil {
return er
}
hdr.Name = srcRelative
if er = tw.WriteHeader(hdr); er != nil {
return er
}
// 打開要打包的文件,准備讀取
fr, er := os.Open(srcFull)
if er != nil {
return er
}
defer fr.Close()
// 將文件數據寫入 tw 中
if _, er = io.Copy(tw, fr); er != nil {
return er
}
return nil
}
func UnTar(srcTar string, dstDir string) (err error) {
// 清理路徑字符串
dstDir = path.Clean(dstDir) + string(os.PathSeparator)
// 打開要解包的文件
fr, er := os.Open(srcTar)
if er != nil {
return er
}
defer fr.Close()
// 創建 tar.Reader,准備執行解包操作
tr := tar.NewReader(fr)
// 遍歷包中的文件
for hdr, er := tr.Next(); er != io.EOF; hdr, er = tr.Next() {
if er != nil {
return er
}
// 獲取文件信息
fi := hdr.FileInfo()
// 獲取絕對路徑
dstFullPath := dstDir + hdr.Name
if hdr.Typeflag == tar.TypeDir {
// 創建目錄
os.MkdirAll(dstFullPath, fi.Mode().Perm())
// 設置目錄權限
os.Chmod(dstFullPath, fi.Mode().Perm())
} else {
// 創建文件所在的目錄
os.MkdirAll(path.Dir(dstFullPath), os.ModePerm)
// 將 tr 中的數據寫入文件中
if er := unTarFile(dstFullPath, tr); er != nil {
return er
}
// 設置文件權限
os.Chmod(dstFullPath, fi.Mode().Perm())
}
}
return nil
}
// 因為要在 defer 中關閉文件,所以要單獨創建一個函數
func unTarFile(dstFile string, tr *tar.Reader) error {
// 創建空文件,准備寫入解包后的數據
fw, er := os.Create(dstFile)
if er != nil {
return er
}
defer fw.Close()
// 寫入解包后的數據
_, er = io.Copy(fw, tr)
if er != nil {
return er
}
return nil
}
// 判斷檔案是否存在
func Exists(name string) bool {
_, err := os.Stat(name)
return err == nil || os.IsExist(err)
}
// 判斷文件是否存在
func FileExists(filename string) bool {
fi, err := os.Stat(filename)
return (err == nil || os.IsExist(err)) && !fi.IsDir()
}
// 判斷目錄是否存在
func DirExists(dirname string) bool {
fi, err := os.Stat(dirname)
return (err == nil || os.IsExist(err)) && fi.IsDir()
}
============================================================
如果要創建 .tar.gz 也很簡單,只需要在創建 tar.Writer 或 tar.Reader 之前創建一個 gzip.Writer 或 gzip.Reader 就可以了,gzip.Writer 負責將 tar.Writer 中的數據壓縮后寫入文件,gzip.Reader 負責將文件中的數據解壓后傳遞給 tar.Reader。要修改的部分如下:
============================================================
package main
import (
// ...
"compress/gzip" // 這里導入 compress/gzip 包
// ...
)
func Tar(src string, dstTar string, failIfExist bool) (err error) {
// ...
fw, er := os.Create(dstTar)
// ...
gw := gzip.NewWriter(fw) // 這里添加一個 gzip.Writer
// ...
tw := tar.NewWriter(gw) // 這里傳入 gw
// ...
}
func UnTar(srcTar string, dstDir string) (err error) {
// ...
fr, er := os.Open(srcTar)
// ...
gr, er := gzip.NewReader(fr) // 這里添加一個 gzip.Reader
// ...
tr := tar.NewReader(gr) // 這里傳入 gr
// ...
}
============================================================
有個問題,用 golang 創建的 .tar 或 .tar.gz 文件無法在 Ubuntu 下用“歸檔管理器”修改,只能讀取和解壓,不知道為什么。
