MultiPart のファイルアップロード実装のCLIクライアント・サーバーコードを理解する

Go の シンプルで動作の早いWebフレームワークである[gin](https://github.com/gin-gonic/gin)と、multipartのライブラリを使って、サーバーとクライアントの複数ファイルアップロードの仕組みを記述してみたいと思います。
# サーバー側

[gin](https://github.com/gin-gonic/gin)はサンプルが充実しているので書くのがとても簡単でした。ルーターを作って、そこにルーティングを設定するだけです。

```golang
func main() {
	router := gin.Default()

	router.POST("/csv", ReceiveFiles)

	srv := &http.Server{
		Addr:    ":38081",
		Handler: router,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// send request
	err := SendFiles()
	log.Printf("error: %v\n", err)

}
```
POST の登録箇所の定義を見てみると、相対パスとハンドラがあります。

```golang
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle("POST", relativePath, handlers)
}
```
ハンドラの条件はコンテキストを持った関数であればよさそうです。ここでは、ハンドラとして、ReceiveFiles という後で出てくるメソッドを渡しています。もちろん、他に,GET, PUT, DELETE 等の一通りのメソッドが定義されています。
ちなみに、go routine を使って server を起動しているのは、サーバーが上がった後の処理を書きたいからです。具体的には、このままでは、ctrl+c を押下しても停止しませんので、[以前書いたブログ](https://qiita.com/TsuyoshiUshio@github/items/e308ca273915026e4a3b)のような作戦を使って、シグナルを受け取ったら終了するようにコードを書きます。

```golang
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
```

さて、実際のファイルを受け取る部分も簡単です。`gin`が便利なユーティリティ関数を用意してくれていいます。

```golang

func ReceiveFiles(c *gin.Context) {
	// Save file
	form, _ := c.MultipartForm()
	files := form.File["file"]
	configPath := filepath.Join(".", "volley", "csv")
	if _, err := os.Stat(configPath); os.IsNotExist(err) {
		err = os.MkdirAll(configPath, os.ModePerm)
	}
	for _, file := range files {
		log.Println(file.Filename)
		dist := filepath.Join(configPath, file.Filename)
		c.SaveUploadedFile(file, dist)
	}

}
```

マルチパートの受け取りはこの関数で

```golang
// MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error) {
```

このFormストラクトは下記の定義です。ValueとFileのmapを持っています。Fileからは、ファイルヘッダの構造体が返ります。

```golang

// Form is a parsed multipart form.
// Its File parts are stored either in memory or on disk,
// and are accessible via the *FileHeader's Open method.
// Its Value parts are stored as strings.
// Both are keyed by field name.
type Form struct {
	Value map[string][]string
	File  map[string][]*FileHeader
}
```

ファイルヘッダは、その中身やファイル名を持っていますので、

```golang
// A FileHeader describes a file part of a multipart request.
type FileHeader struct {
	Filename string
	Header   textproto.MIMEHeader
	Size     int64

	content []byte
	tmpfile string
}

:
// Open opens and returns the FileHeader's associated File.
func (fh *FileHeader) Open() (File, error) {
	if b := fh.content; b != nil {
		r := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b)))
		return sectionReadCloser{r}, nil
	}
	return os.Open(fh.tmpfile)
}
```

SaveUploadedFile 関数を使えば、場所さえ指定したらファイルを書いてくれます。これは、multipart.FileHeader構造体に、Open()メソッドが生えているのですね。

```golang
// SaveUploadedFile uploads the form file to specific dst.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error {
	src, err := file.Open()
	if err != nil {
		return err
	}
	defer src.Close()

	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	return err
}
```

こんな感じでとても簡単にサーバー側のコードが書けました。

# Multipartアップロードのクライアント側実装

## ローカルのファイル探索

この例では、`csv`の拡張子を持つファイルを対象に、アップロードしています。アップロードするために、アップロード対象のファイルの検索を行うのは、`filepath.Walk`メソッドが便利そうです。ルートディレクトリを指定すると、そのルートディレクトリにあるファイル、ディレクトリ、サブディレクトリを探索してくれます。ただし、あんまり深いディレクトリだと、効率が悪いそうです。(ソースコードのコメントより） `WalkFunc` はファイルが見つかったときのコールバックで、ファイルのパスと、`os.FileInfo`が返ります。最後のエラーは、探索の最中でエラーがあった場合の振る舞いを、WalkFunc側で決めるためのものです。

```golang
func Walk(root string, walkFn WalkFunc) error {
     :
type WalkFunc func(path string, info os.FileInfo, err error) error
```
`os.FileInfo` はそのまま「ファイルの情報」ですね。

```golang
// A FileInfo describes a file and is returned by Stat and Lstat.
type FileInfo interface {
	Name() string       // base name of the file
	Size() int64        // length in bytes for regular files; system-dependent for others
	Mode() FileMode     // file mode bits
	ModTime() time.Time // modification time
	IsDir() bool        // abbreviation for Mode().IsDir()
	Sys() interface{}   // underlying data source (can return nil)
}
```

## マルチパートの作成

[Go言語のmime/multipartパッケージでファイルをアップロードしましょう](https://medium.com/eureka-engineering/multipart-file-upload-in-golang-c4a8eb15a3ee)というブログが素晴らしい内容でした。MultiPartは本来、生では次のようになります。具体的なデータは上記のブログから転載しています。

```html
POST /upload HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip
Content-Length: 254
Content-Type: multipart/form-data; boundary=c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c
User-Agent: Go-http-client/1.1
--c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c
Content-Disposition: form-data; name="file"; filename="hello.txt"
Content-Type: application/octet-stream
hello
--c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c--
```

マルチパートは、上記のように、`boundary` が設定されており、これを境界としていくつものファイルを一度に送ることができます。`Content-Disposition` というヘッダの定義を見てみましょう。

> 本文が multipart/form-data である場合、Content-Disposition ヘッダーは、マルチパートを構成する各サブパートに付与され、そのフィールドに関する情報を示します。サブパートはContent-Type ヘッダーで定義された boundary によって区切られます。マルチパートの本文体に付与した場合、Content-Disposition は何の意味も持ちません。上記の例ではフィールド名を`file` として、１つのファイル分を、`boundary`の中で表現しています。複数のファイルを`boundary`で区切って一回で送信することもできます。サーバーのところで出てきた`file` はこのフィールド名ですね。

> 例
> Content-Disposition: form-data
Content-Disposition: form-data; name="fieldName"
Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"

* [Content-Disposition](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition)より引用

## コードの本文

### multipart.NewWriter
さて、具体的にマルチパートのアップロードの部分を見てみると、`multipart.NewWriter` 関数がまずあります。ここで、適当にバウンダリをランダムに作って保持しています。Writer を持っていますね。

```golang
// NewWriter returns a new multipart Writer with a random boundary,
// writing to w.
func NewWriter(w io.Writer) *Writer {
	return &Writer{
		w:        w,
		boundary: randomBoundary(),
	}
}
```

### multipart.CreateFromFile
こちらのメソッド見てみると、面倒なContent-Disposition やContent-Typeをファイルから作ってくれるようです。ただし、ヘッダのみなので、ご注意。

```golang
// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
	h := make(textproto.MIMEHeader)
	h.Set("Content-Disposition",
		fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
			escapeQuotes(fieldname), escapeQuotes(filename)))
	h.Set("Content-Type", "application/octet-stream")
	return w.CreatePart(h)
}
````

そのあとで、`func Copy(dst Writer, src Reader) (written int64, err error)` によって、`CreateFromFile` で返ってきたWriterにファイルの本体をコピーします。

### multipart.FromDataContentType
こちらで、ContentType を返します。このヘッダにboundaryが入っていますので、設定しています。

```golang
// FormDataContentType returns the Content-Type for an HTTP
// multipart/form-data with this Writer's Boundary.
func (w *Writer) FormDataContentType() string {
	b := w.boundary
	// We must quote the boundary if it contains any of the
	// tspecials characters defined by RFC 2045, or space.
	if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) {
		b = `"` + b + `"`
	}
	return "multipart/form-data; boundary=" + b
}
```

```golang
func SendFiles() error {
	body := &bytes.Buffer{}
	mw := multipart.NewWriter(body)
	fieldName := "file"
	matchCsv := regexp.MustCompile(`.*\.csv`)
	err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
		if matchCsv.MatchString(path) {
			file, err := os.Open(path)
			if err != nil {
				return err
			}
			fw, err := mw.CreateFormFile(fieldName, path)
			if err != nil {
				return err
			}
			_, err = io.Copy(fw, file)
			if err != nil {
				return err
			}
			file.Close()
		}
		return nil
	})
	if err != nil {
		return err
	}
	contentType := mw.FormDataContentType()
	err = mw.Close()
	if err != nil {
		return err
	}
	resp, err := http.Post("http://localhost:38081/csv", contentType, body)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	responseBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	fmt.Println(string(responseBody))
	return nil
}
```

最後にPOSTリクエストを送信します。内部のマルチパートの組み立ては`multipart`パッケージの便利メソッドが程よくやってくれるので、楽ちんですね！

```golang
resp, err := http.Post("http://localhost:38081/csv", contentType, body)
```

# 実行結果

これら２つのファイルをアップロード

```bash
$ ls
DocumentApi.csv  DocumentApi2.csv  go.mod  go.sum  main.go  
```

プログラムを動かすとサーバーが起動して、アップロードしてファイルを書いたら終了します。Windowsだと、ファイアーウォールのポート公開の警告がでますが、その頃には処理は終わっています。

```bash
$ go run main.go                     
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /csv                      --> main.ReceiveFiles (3 handlers)
2019/12/23 08:48:58 DocumentApi.csv
2019/12/23 08:48:58 DocumentApi2.csv
[GIN] 2019/12/23 - 08:48:58 | 200 |     16.1636ms |       127.0.0.1 | POST     /csv

2019/12/23 08:48:58 error: <nil>
```

ディレクトリを見ると、２つのファイルが作成されています。ちなみに、ファイルが存在してもエラーは出ず、上書きされるようですね。

```bash
$ cd volley/csv 
$ ls
DocumentApi.csv  DocumentApi2.csv
```

挙動が理解できました。