Go Go - Archive files with archive/tar lib

前言

由於專案要提供 API 來讓使用者 export 匯出檔案, 因此需要將所需檔案集結成一個 archive file。這個流程是透過 Go 的標準 lib archive/tar 來處理,以下文章將簡單介紹流程和實作方式,並於最後附上完整程式碼。

Tar

tar 在UNIX/Linux系統中是最常見的打包工具,透過 tar 的協助,我們可以把數個檔案打包成一個 <file name>.tar 檔案,以利進行後續處理。

![Go-Tar-1.png]({{ site.url }}/assets/images/Go-Tar-1.png)

tar file format

.tar 的檔案格式主要是由 file object 和其對應的 header 所組成。 header 裡面包含了一個 file 的 metadata(例如檔案名稱,數據大小等),這樣系統就可以透過 header 去檢測檔案屬性和完整度。

Tar files with Go

![Go-Tar-2.png]({{ site.url }}/assets/images/Go-Tar-2.png)

1. Create a Tar.Writer

在打包檔案時,首要先做的就是建立一個 tar file 的 writer,後續才能將需要打包的檔案 file 和 header 寫入到 tar file 中。

1// 建立一個 tar file 的檔案位置.
2target := filepath.Join(tarPath, fmt.Sprintf("%s.tar", tarName))
3// 根據上面所建立的檔案位置來創建檔案. (回傳值為 *io.File,實作了 io.writer interface)
4tarfile, err := os.Create(target)
5if err != nil {
6	return err
7}
8// 建立一個內含 tar 檔案的 tar writer
9tarWriter := tar.NewWriter(tarfile)

2.讀取要打包的檔案

接下來,我們要一一讀取需要打包的檔案們,以便後續將這些檔案寫入到 tar.Writer 中。透過標準函式庫 filepath.Walk() 就能輕鬆走訪各 folder 底下的檔案群。

 1source := "<source folder>"
 2
 3// Check source is existing.
 4info, err := os.Stat(source)
 5if err != nil {
 6	return nil
 7}
 8
 9filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
10  // Read each file
11})

3. 將要打包的檔案寫入到 tar.Writer 中

上面有提到,在打包 file 時,需要包含其對應的 header,因此我們可以透過 tar.FileInfoHeader 的幫助來去擷取 io.File 內的資訊,然後得到我們所要的 header

1header, err := tar.FileInfoHeader(info, info.Name())
2// func FileInfoHeader(fi os.FileInfo, link string) (*Header, error)
3// 這邊的 link 是提供給 file mode 為 ModeSymlink 時使用。
4// 詳細可以參考 https://en.wikipedia.org/wiki/Symbolic_link
1// 這邊要特別注意的是, io.File Name 不包含路徑,所以如果你的檔案是放在其他子 folder 底下,要記得重新設定 `header.name`。如果沒有重新設定的話,就不會放到對應 folder 底下啦。
2header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
3// strings.TrimPrefix(path, source) 這個用意是為了去除 source path

有了 header 之後,就把它寫入 tar.Writer 吧!

1if err := tarWriter.WriteHeader(header); err != nil {
2	return err
3}

寫入了 header 之後,再把 file 內容也寫進去。

1// 打開檔案
2file, err := os.Open(path)
3if err != nil {
4	return err
5}
6defer file.Close()
7// 將檔案複製到 tar.Writer
8_, err = io.Copy(tarWriter, file)

4. 關閉 tar.Writer

當把所有檔案和 header 寫到 tar file 之後,就可以把 tar.Writer 和 file stream 關掉。

1tarfile.Close()
2tarWriter.Close()

Source Code

 1// Archive Create a tar file with multiple resources
 2func Archive(sources []string, tarPath, tarName string) error {
 3
 4	var err error
 5	// Create the tar path
 6	target := filepath.Join(tarPath, fmt.Sprintf("%s.tar", tarName))
 7	// Create the tar file
 8	tarfile, err := os.Create(target)
 9	if err != nil {
10		return err
11	}
12	defer tarfile.Close()
13
14	// Create a tar writer
15	tarWriter := tar.NewWriter(tarfile)
16	defer tarWriter.Close()
17
18	for _, source := range sources {
19		if err = tarFile(source, tarWriter); err != nil {
20			return err
21		}
22	}
23	return nil
24}
25
26func tarFile(source string, tarWriter *tar.Writer) error {
27	// Check source is existing.
28	info, err := os.Stat(source)
29	if err != nil {
30		return nil
31	}
32
33	var baseDir string
34	if info.IsDir() {
35		baseDir = filepath.Base(source)
36	}
37	return filepath.Walk(source,
38		func(path string, info os.FileInfo, err error) error {
39			if err != nil {
40				return err
41			}
42			// Create a tar header for a single file
43			header, err := tar.FileInfoHeader(info, info.Name())
44			if err != nil {
45				return err
46			}
47			fmt.Printf("%#v \n", header)
48
49			if baseDir != "" {
50				// Name of file entry, a full path is needed.
51				// strings.TrimPrefix - Remove unnecessarily path
52				header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
53			}
54
55			if err := tarWriter.WriteHeader(header); err != nil {
56				return err
57			}
58
59			// Write
60			if info.IsDir() {
61				return nil
62			}
63
64			file, err := os.Open(path)
65			if err != nil {
66				return err
67			}
68			defer file.Close()
69			// io.Copy is copy the content of src file
70			_, err = io.Copy(tarWriter, file)
71			return err
72		})
73}