はじめに
前回の記事の続きです。
前回の記事はこちら
ファイルのアップロード処理
前回はフォームの複数回送信の防止を行いました。
今回はファイルアップロード処理です。
早速下記のhtmlを書いてみましょう。upload.gptl で保存します。
<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>長いので下記に gist コードを貼り付けました。
| package main | |
| import ( | |
| "crypto/md5" | |
| "fmt" | |
| "html/template" | |
| "io" | |
| "log" | |
| "net/http" | |
| "net/url" | |
| "os" | |
| "strconv" | |
| "strings" | |
| "time" | |
| ) | |
| func sayhelloName(w http.ResponseWriter, r *http.Request) { | |
| r.ParseForm() //urlが渡すオプションを解析します。POSTに対してはレスポンスパケットのボディを解析します(request body) | |
| //注意:もしParseFormメソッドがコールされなければ、以下でフォームのデータを取得することができません。 | |
| fmt.Println(r.Form) //これらのデータはサーバのプリント情報に出力されます | |
| fmt.Println("path", r.URL.Path) | |
| fmt.Println("scheme", r.URL.Scheme) | |
| fmt.Println(r.Form["url_long"]) | |
| for k, v := range r.Form { | |
| fmt.Println("key:", k) | |
| fmt.Println("val:", strings.Join(v, "")) | |
| } | |
| fmt.Fprintf(w, "Hello astaxie!") //ここでwに書き込まれたものがクライアントに出力されます。 | |
| } | |
| func login(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("html/login.gtpl") | |
| t.Execute(w, token) | |
| } else { | |
| //ログインデータがリクエストされ、ログインのロジック判断が実行されます。 | |
| r.ParseForm() | |
| token := r.Form.Get("token") | |
| if token != "" { | |
| //tokenの合法性を検証します。 | |
| var cookie = getCookie(w, r, "token") | |
| if cookie == nil { | |
| setCookie(w, "token", token) | |
| fmt.Println("クッキーをセットしたよ! : ", token) | |
| } else if cookie != nil && cookie.Value != token { | |
| fmt.Println("2回更新しているよ!") | |
| fmt.Println("送信された値", token) | |
| fmt.Println("保持している値", cookie.Value) | |
| } | |
| } else { | |
| fmt.Println("tokenがないです!") | |
| } | |
| fmt.Println("username:", r.Form["username"]) | |
| fmt.Println("password:", r.Form["password"]) | |
| fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //サーバ側に出力されます。 | |
| fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) | |
| template.HTMLEscape(w, []byte(r.Form.Get("username"))) //クライアントに出力されます。 | |
| t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) | |
| err = t.ExecuteTemplate(w, "T", template.HTML("<script>alert('you have been pwned')</script>")) | |
| if err != nil { | |
| fmt.Println("error") | |
| } | |
| fmt.Println("age: ", r.Form["age"]) | |
| fmt.Println("GET query string", r.URL) | |
| // バリデーションしてみる | |
| errors := validate(r.Form) | |
| fmt.Println(errors) | |
| } | |
| } | |
| // /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("html/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) | |
| } | |
| } | |
| func main() { | |
| http.HandleFunc("/", sayhelloName) //アクセスのルーティングを設定します | |
| http.HandleFunc("/login", login) //アクセスのルーティングを設定します | |
| http.HandleFunc("/upload", upload) | |
| err := http.ListenAndServe(":9090", nil) //監視するポートを設定します | |
| if err != nil { | |
| log.Fatal("ListenAndServe: ", err) | |
| } | |
| } | |
| func validate(form url.Values) (errors []string) { | |
| const requiredErr = "%sは必須です。" | |
| const numErr = "%sは数字で入力してください。" | |
| const rangeErr = "%sは0~99の値で入力してください。" | |
| if len(form["username"][0]) == 0 { | |
| errors = append(errors, fmt.Sprintf(requiredErr, "ユーザ名")) | |
| } | |
| if form.Get("age") != "" { | |
| getint, err := strconv.Atoi(form.Get("age")) | |
| if err != nil { | |
| errors = append(errors, fmt.Sprintf(numErr, "年齢")) | |
| } else { | |
| if getint < 0 || getint > 99 { | |
| errors = append(errors, fmt.Sprintf(rangeErr, "年齢")) | |
| } | |
| } | |
| } | |
| return | |
| } | |
| func setCookie(w http.ResponseWriter, name string, value string) { | |
| cookie := &http.Cookie{ | |
| Name: name, | |
| Value: value, | |
| } | |
| http.SetCookie(w, cookie) | |
| } | |
| func getCookie(w http.ResponseWriter, r *http.Request, name string) *http.Cookie { | |
| cookie, err := r.Cookie(name) | |
| if err != nil { | |
| return nil | |
| } | |
| return cookie | |
| } |
| <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> |
早速実行してみましょう。
どうやらうまくいってそうです。upload 関数を見ていきます。
if r.Method == "GET" のパスは、 token のチェックと、 htmlを描画している処理ですね。
これは前回までにやっていた部分です。
else ケースを見てみます。
r.ParseMultipartForm(32 << 20)
file, handler, err := r.FormFile("uploadfile")
if err != nil {
fmt.Println(err)
return
}この部分は、
上のコードでは、ファイルのアップロードを処理するためには
r.ParseMultipartFormをコールする必要があります。引数にはmaxMemoryが表示されています。
ということですね。アップロードされたファイルを処理するものです。
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)この部分は、
ParseMultipartFormをコールした後、アップロードするファイルはmaxMemoryのサイズのメモリに保存されます。
もしファイルのサイズがmaxMemoryを超えた場合、残った部分はシステムのテンポラリファイルに保存されます。r.FormFileによって上のファイルハンドルを取得することができます。
テンポラリの部分は、1個前のfile, handler, err := r.FormFile("uploadfile") で開いていますね。
保存用のファイルオブジェクトを、handler.Filename でファイル名を取得して作成していますね。
その後実例の中ではio.Copyを使ってファイルを保存しています。
取得できたファイルを、io.Copy を使用してファイル保存がされていますね。
個々まで読んできて思ったのは、先程のアップロード処理は実は成功していませんでしたね。
4枚目のキャプチャのコンソール出力に、 open ./test/20170903_つきこ.png: The system cannot find the path specified. と書いてあります。
test フォルダを直下に作る必要がありそうです。
作ってみましょうか。

できてました!
最後にフォーム処理について読んでみます。
上の実例を通して、ファイルのアップロードには主に3ステップの処理があることが分かります:
- フォームに
enctype="multipart/form-data"を追加する。- サーバで
r.ParseMultipartFormをコールし、アップロードするファイルをメモリとテンポラリファイルに保存する。r.FormFileを使用して、ファイルハンドルを取得し、ファイルに対して保存等の処理を行う。
なるほど・・・!
クライアントによるファイルのアップロード
クライアントのアップロードをエミュレート(擬似的な操作)できる機能があるようです。
早速試してみましょう。
(アップロードのテストとかが捗りそうですね!)
client.go を作成してコピーしました。
サーバは起動しっぱなしで、 client.go を実行してみましょう。

20170822_リーシャ.png というのを直下にそのまま置きました。
これをアップロードする予定となります。

おぉ!うまく行ってますね!
おわりに
フォーム編はこれで終了となります。
明日からは、データベースへのアクセスですね。
各DBはDockerで用意しようかな~。



