Build Web Application With Golang を試してやってみる 16

はじめに

前回の記事の続きです。
前回の記事はこちら

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

前回はフォームの複数回送信の防止を行いました。
今回はファイルアップロード処理です。

早速下記の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
}
view raw 4.5.go hosted with ❤ by GitHub
<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>
view raw upload.gtpl hosted with ❤ by GitHub
https://gist.github.com/katsuobushiFPGA/594c5dcacf77b23929ed7ac68158ae2a

早速実行してみましょう。

web-server-01
web-server-02
web-server-03
web-server-04

どうやらうまくいってそうです。
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 フォルダを直下に作る必要がありそうです。
作ってみましょうか。

web-server-05

できてました!

最後にフォーム処理について読んでみます。

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

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

なるほど・・・!

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

クライアントのアップロードをエミュレート(擬似的な操作)できる機能があるようです。
早速試してみましょう。
(アップロードのテストとかが捗りそうですね!)

client.go を作成してコピーしました。
サーバは起動しっぱなしで、 client.go を実行してみましょう。

web-server-06

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

web-server-07

おぉ!うまく行ってますね!

おわりに

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

Hugo で構築されています。
テーマ StackJimmy によって設計されています。