代碼片斷 - Golang 建立 .tar.gz 壓縮包

Golang建立 .tar.gz 壓縮包golang

 

  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 下用「歸檔管理器」修改,只能讀取和解壓,不知道爲何。


相關文章
相關標籤/搜索