この記事は福島高専アドベントカレンダー5日目の記事です。
この記事ではGoでファイルアップロードの処理を作成していきます。
主に以下の記事を参考にさせていただきました。ありがとうございました。
How to process file uploads in Go
内容は私を含む初学者向けです!
プログラムの書き方などで気になる点があったらお教えください。
もっと色々面白いこと書くつもりだったのですが忙しくて内容が薄くなってしまいました
目標
- Goでのファイルアップロードの処理の書き方が分かる
-
io
パッケージやos
パッケージの基本的な使い方が分かるようになる
前提条件
- Go1.11以上かつGoが自分のパソコンにインストール済みであること
はじめに
まず最初に、作業する新しいディレクトリを作成します。私は**file-img-secret/
**とします。
$ mkdir file-img-secret
$ cd file-img-secret/
この新しいプロジェクトディレクトリ内で、次のコマンドを実行してください。
goモジュールを使用してプロジェクトを初期化します。
$ go mod init github.com/Sheerlore/file-img-secret
go: creating new go.mod: module github.com/Sheerlore/file-img-secret
この新しいディレクトリ内に、Goプログラムへのメインエントリポイントとなるmain.goファイルを作成します。
package main
import "fmt"
func main() {
fmt.Println("FILE UPLOAD MONITOR")
}
つぎに、http:// localhost:8080
で実行される単純なnet/http
ベースのサーバーを作成します。
package main
import (
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func setupRoutes() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
func main() {
fmt.Println("FILE UPLOAD MONITOR")
setupRoutes()
}
現時点でgo run mian.go
をターミナルで入力し、ブラウザでlocalhost:8080にアクセスするとHello Worldの文字が表示されると思います。
今回の目的はファイルをアップロードした時の処理をGoで書くことです。ファイルをブラウザから選択できるように必要なHTMLファイルを記述し、main.go
に少し書き加えていきましょう。
最初にHTMLファイルは以下のようなシンプルなものとします。index.html
を作成して入力してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FILE UPLOAD</title>
</head>
<body>
<h1>FILE UPLOAD DEMO</h1>
<form id="form" enctype="multipart/form-data" action="/upload" method="POST">
<input type="file" name="file" class="input file-input" multiple>
<button class="button" type="submit">Submit</button>
</form>
</body>
</html>
次にアップロード用のエンドポイントをmain.go
に追加し、index.html
が表示されるようにしていきます。以下のように追加してください。
package main
import (
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
http.ServeFile(w, r, "index.html") // 追加
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
}
func setupRoutes() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/upload", uploadHandler) // 追加
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
func main() {
fmt.Println("FILE UPLOAD MONITOR")
setupRoutes()
}
ここでgo run main.go
をターミナルで打ち込み、localhost:8080
にアクセスすれば、index.html
の内容が表示されていると思います。
HTMLのform
のaction="/upload
とmain.go
の/upload
が対応していることに注意してください。
また今はアクセスしても何も起きません。
ファイル処理
プロジェクト内にfile
という名前で新しいディレクトリを作成してください。その中にupload.go
という新しいファイルも作成します。
ここにuploadの際の処理を書いていきましょう!
ファイルのサイズ制限を付ける
巨大なファイルをアップロードさせないように、ファイルアップロードのサイズを制限する必要があります。
すぐに思いつく方法はリクエストヘッダーのContent-Length
を確認してそれを最大サイズと比較する方法ですが、Content-Length
ヘッダーはクライアント側で任意の値に変更できてしまうため、この方法では不十分です。
if (r.ContentLength > Max_UPLOAD_SIZE) {エラー処理}
ここではhttp
パッケージのhttp.MaxBytesReader
メソッドを使用します。以下のようにfile.go
に追加してください。
package file
import "net/http"
const MaxUploadSize = 1024 * 1024
func UploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "許可されていないメソッドです", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, MaxUploadSize)
if err := r.ParseMultipartForm(MaxUploadSize); err != nil {
http.Error(w, "アップロードされたファイルが大きすぎます。1MB以下のファイルを選択してください", http.StatusBadRequest)
}
}
http.MaxByteReader()
は受け取ったリクエストボディのサイズを制限する際に用いられるメソッドです。
単一のファイルをアップロードする場合は、リクエストボディのサイズを制限するのに使うことができます。
ソース内のParseMultipartForm()
は引数で与えられた最大メモリサイズまでリクエストボディをmultipart/form-data
として解析します。
アップロードされたファイルを保存する
アップロードされたファイルを取得して、保存したいと思います。file.go
の末尾に以下のようにコードを追加します。
// 省略
func UploadHandler(w http.ResponseWriter, r *http.Request) {
//省略
// ------------------------------
//FormFileの引数はHTML内のform要素のnameと一致している必要があります
file, fileHeader, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// 存在していなければ、保存用のディレクトリを作成します。
err = os.MkdirAll("./uploadimages", os.ModePerm)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 保存用ディレクトリ内に新しいファイルを作成します。
dst, err := os.Create(fmt.Sprintf("./uploadimages/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename)))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// アップロードされたファイルを先程作ったファイルにコピーします。
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Upload successful")
}
いきなり色々追加されて迷いますが、やっていることは単純です。大きく分けると4つ追加されたと思います。
r.FormFile()
os.MkdirAll()
os.Create()
io.Copy()
引数は省略されていますがメソッドの名前を見ればやっていることはだいたい想像つくかと思います。
フォームのファイルを取得し、保存用のディレクトリを作成し、保存するファイルを作成して、フォームのファイルをそこにコピーしているだけです。
アップロードされたファイルのタイプを制限する
アップロードされるファイルの種類を画像(JPEG、PNG)のみとします。そのためにはアップロードされたファイルのMIMEタイプを比較し、サーバー側で処理をするかどうかを判断する必要がありそうです。
ファイル入力でaccept
属性を使用し、ファイルのタイプを定義できますが、入力が改ざんされていないかはサーバー側で再確認する必要があります。
file.go
に以下のように追加します。
package file
import "net/http"
const MaxUploadSize = 1024 * 1024
func UploadHandler(w http.ResponseWriter, r *http.Request) {
...
defer file.Close()
// 省略
buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filetype := http.DetectContentType(buff)
if filetype != "image/jpeg" && filetype != "image/png" {
http.Error(w, "許可されていないファイルタイプです。JPEGかPNGをアップロードしてください", http.StatusBadRequest)
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//省略
...
}
http
パッケージのDetectContentType()
は与えられたデータのコンテンツタイプを検出するためのメソッドです。MIMEタイプを決定するために、最初の512バイトのデータを読み取る最大サイズとして考えます。
したがって、ファイルの最初の512バイトを空のバッファに読み取り、DetectContentType()
に渡します。
渡されたファイルがJPEGでもPNGでもなかった場合はエラー返されます。
コンテンツタイプを判別するためにアップロードされたファイルの最初から512バイト読み取ると、FileStreamポインターが512バイト分進んでしまい、io.Copy()
が呼び出された場合、その位置からの読み取りが続行されるため、画像ファイルが破損してしまいます。
そのため、file.Seek()
メソッドを使用して、ポインタをファイルの先頭に戻しています。
一度実行
ここまでで基本的なファイルアップロードの処理は完成しました。
main.go
に先程まで作ったfile
パッケージをimportしましょう。以下のように追加してください。
package main
import (
"github.com/Sheerlore/file-img-secret/file" //追加
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
http.ServeFile(w, r, "index.html")
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
file.UploadHandler(w, r) //追加
}
func setupRoutes() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/upload", uploadHandler)
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatal(err)
}
}
func main() {
fmt.Println("FILE UPLOAD MONITOR")
setupRoutes()
}
一度go run main.go
をターミナルで打ち込み、localhost:8080からファイルを選択してSubmitしてみてください。uploadimages
というフォルダが作られて画像が保存できていることが確認できたと思います。