Skip to content

Latest commit

 

History

History
159 lines (126 loc) · 6.7 KB

File metadata and controls

159 lines (126 loc) · 6.7 KB

4.5 ファイルのアップロード処理

ユーザによるファイルのアップロードを処理したいとします。例えば、現在Instagramのようなホームページを作成しているとします。ユーザが撮影した写真を保存する必要があります。このような要求はどのように実現するのでしょうか?

フォームにファイルをアップロードさせるためには、まずformのenctype属性を追加する必要があります。enctype属性には以下の3つの種類があります:

application/x-www-form-urlencoded   送信前にすべての文字列をエンコードする(デフォルト)
multipart/form-data	  文字列に対してエンコードしません。ファイルのアップロードウィジェットを含むフォームを使用するときはこの値が必要です。
text/plain	  空白を"+"記号に置き換えます。ただし、特殊文字に対してエンコードは行われません。

そのため、フォームのhtmlコードはこのようになります:

<html>
<head>
	<title>ファイルアップロード</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1:9090/upload" method="post">
  <input type="file" name="uploadfile" />
  <input type="hidden" name="token" value="{{.}}"/>
  <input type="submit" value="upload" />
</form>
</body>
</html>

サーバでは、handlerFuncをひとつ追加します:

http.HandleFunc("/upload", upload)

// /uploadを処理するロジック
func upload(w http.ResponseWriter, r *http.Request) {
	fmt.Println("method:", r.Method) //リクエストを受け取るメソッド
	if r.Method == "GET" {
		crutime := time.Now().Unix()
		h := md5.New()
		io.WriteString(h, strconv.FormatInt(crutime, 10))
		token := fmt.Sprintf("%x", h.Sum(nil))

		t, _ := template.ParseFiles("upload.gtpl")
		t.Execute(w, token)
	} else {
		r.ParseMultipartForm(32 << 20)
		file, handler, err := r.FormFile("uploadfile")
		if err != nil {
			fmt.Println(err)
			return
		}
		defer file.Close()
		fmt.Fprintf(w, "%v", handler.Header)
		f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
		if err != nil {
			fmt.Println(err)
			return
		}
		defer f.Close()
		io.Copy(f, file)
	}
}

上のコードでは、ファイルのアップロードを処理するためにはr.ParseMultipartFormをコールする必要があります。引数にはmaxMemoryが表示されています。ParseMultipartFormをコールした後、アップロードするファイルはmaxMemoryのサイズのメモリに保存されます。もしファイルのサイズがmaxMemoryを超えた場合、残った部分はシステムのテンポラリファイルに保存されます。r.FormFileによって上のファイルハンドルを取得することができます。その後実例の中ではio.Copyを使ってファイルを保存しています。

その他のファイルではないフィールド情報を取得する時はr.ParseFormをコールする必要はありません。必要な時はGoが自動でコールします。またParseMultipartFormを一度コールすると、その後にもう一度コールしても効果はありません。

上の実例を通して、ファイルのアップロードには主に3ステップの処理があることが分かります:

  1. フォームにenctype="multipart/form-data"を追加する。
  2. サーバでr.ParseMultipartFormをコールし、アップロードするファイルをメモリとテンポラリファイルに保存する。
  3. r.FormFileを使用して、ファイルハンドルを取得し、ファイルに対して保存等の処理を行う。

ファイルhandlerはmultipart.FileHeaderです。この中には以下のような構造体が保存されています。

type FileHeader struct {
	Filename string
	Header   textproto.MIMEHeader
	// contains filtered or unexported fields
}

上のコード例では以下のようにファイルのアップロードを出力します。

図4.5 ファイルのアップロードを行った後サーバが受け取った情報の出力

クライアントによるファイルのアップロード

上の例でどのようにフォームからファイルをアップロードするのか示しました。その後サーバでファイルを処理しますが、Goは実はクライアントのフォームによるファイルのアップロードをエミュレートする機能をサポートしています。詳しい使用方法は以下の例をご覧ください:

package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
)

func postFile(filename string, targetUrl string) error {
	bodyBuf := &bytes.Buffer{}
	bodyWriter := multipart.NewWriter(bodyBuf)

	//キーとなる操作
	fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
	if err != nil {
		fmt.Println("error writing to buffer")
		return err
	}

	//ファイルハンドル操作をオープンする
	fh, err := os.Open(filename)
	if err != nil {
		fmt.Println("error opening file")
		return err
	}
	defer fh.Close()

	//iocopy
	_, err = io.Copy(fileWriter, fh)
	if err != nil {
		return err
	}

	contentType := bodyWriter.FormDataContentType()
	bodyWriter.Close()

	resp, err := http.Post(targetUrl, contentType, bodyBuf)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	resp_body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	fmt.Println(resp.Status)
	fmt.Println(string(resp_body))
	return nil
}

// sample usage
func main() {
	target_url := "http://localhost:9090/upload"
	filename := "./astaxie.pdf"
	postFile(filename, target_url)
}

上の例ではクライアントが如何にサーバに対し一つのファイルをアップロードするのかご説明しました。クライアントはmultipart.Writeを通してファイルの本文をバッファの中に書き込みます。その後、httpのPostメソッドをコールしてバッファからサーバに転送します。

もしあなたが他にusernameといった普通のフィールドを同時に書き込む場合は、multipartのWriteFieldメソッドをコールして、その他の似たようなフィールドを複数書き込むことができます。

links