前言
由於專案要提供 API 來讓使用者 export 匯出檔案, 因此需要將所需檔案集結成一個 archive file。這個流程是透過 Go 的標準 lib archive/tar 來處理,以下文章將簡單介紹流程和實作方式,並於最後附上完整程式碼。
Tar
tar 在UNIX/Linux系統中是最常見的打包工具,透過 tar 的協助,我們可以把數個檔案打包成一個 <file name>.tar 檔案,以利進行後續處理。

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

1. Create a Tar.Writer
在打包檔案時,首要先做的就是建立一個 tar file 的 writer,後續才能將需要打包的檔案 file 和 header 寫入到 tar file 中。
1
2
3
4
5
6
7
8
9
| // 建立一個 tar file 的檔案位置.
target := filepath.Join(tarPath, fmt.Sprintf("%s.tar", tarName))
// 根據上面所建立的檔案位置來創建檔案. (回傳值為 *io.File,實作了 io.writer interface)
tarfile, err := os.Create(target)
if err != nil {
return err
}
// 建立一個內含 tar 檔案的 tar writer
tarWriter := tar.NewWriter(tarfile)
|
2.讀取要打包的檔案
接下來,我們要一一讀取需要打包的檔案們,以便後續將這些檔案寫入到 tar.Writer 中。透過標準函式庫 filepath.Walk() 就能輕鬆走訪各 folder 底下的檔案群。
1
2
3
4
5
6
7
8
9
10
11
| source := "<source folder>"
// Check source is existing.
info, err := os.Stat(source)
if err != nil {
return nil
}
filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
// Read each file
})
|
3. 將要打包的檔案寫入到 tar.Writer 中
上面有提到,在打包 file 時,需要包含其對應的 header,因此我們可以透過 tar.FileInfoHeader 的幫助來去擷取 io.File 內的資訊,然後得到我們所要的 header。
1
2
3
4
| header, err := tar.FileInfoHeader(info, info.Name())
// func FileInfoHeader(fi os.FileInfo, link string) (*Header, error)
// 這邊的 link 是提供給 file mode 為 ModeSymlink 時使用。
// 詳細可以參考 https://en.wikipedia.org/wiki/Symbolic_link
|
1
2
3
| // 這邊要特別注意的是, io.File Name 不包含路徑,所以如果你的檔案是放在其他子 folder 底下,要記得重新設定 `header.name`。如果沒有重新設定的話,就不會放到對應 folder 底下啦。
header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
// strings.TrimPrefix(path, source) 這個用意是為了去除 source path
|
有了 header 之後,就把它寫入 tar.Writer 吧!
1
2
3
| if err := tarWriter.WriteHeader(header); err != nil {
return err
}
|
寫入了 header 之後,再把 file 內容也寫進去。
1
2
3
4
5
6
7
8
| // 打開檔案
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// 將檔案複製到 tar.Writer
_, err = io.Copy(tarWriter, file)
|
4. 關閉 tar.Writer
當把所有檔案和 header 寫到 tar file 之後,就可以把 tar.Writer 和 file stream 關掉。
1
2
| tarfile.Close()
tarWriter.Close()
|
Source Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| // Archive Create a tar file with multiple resources
func Archive(sources []string, tarPath, tarName string) error {
var err error
// Create the tar path
target := filepath.Join(tarPath, fmt.Sprintf("%s.tar", tarName))
// Create the tar file
tarfile, err := os.Create(target)
if err != nil {
return err
}
defer tarfile.Close()
// Create a tar writer
tarWriter := tar.NewWriter(tarfile)
defer tarWriter.Close()
for _, source := range sources {
if err = tarFile(source, tarWriter); err != nil {
return err
}
}
return nil
}
func tarFile(source string, tarWriter *tar.Writer) error {
// Check source is existing.
info, err := os.Stat(source)
if err != nil {
return nil
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(source)
}
return filepath.Walk(source,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Create a tar header for a single file
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
fmt.Printf("%#v \n", header)
if baseDir != "" {
// Name of file entry, a full path is needed.
// strings.TrimPrefix - Remove unnecessarily path
header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
// Write
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// io.Copy is copy the content of src file
_, err = io.Copy(tarWriter, file)
return err
})
}
|