前言
由於專案要提供 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}