13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

福島高専Advent Calendar 2020

Day 5

【Go】ファイルアップロードをする処理

Last updated at Posted at 2020-12-04

:robot: この記事は福島高専アドベントカレンダー5日目の記事です。:robot:

この記事ではGoでファイルアップロードの処理を作成していきます。

主に以下の記事を参考にさせていただきました。ありがとうございました。
How to process file uploads in Go

内容は私を含む初学者向けです!
プログラムの書き方などで気になる点があったらお教えください。
もっと色々面白いこと書くつもりだったのですが忙しくて内容が薄くなってしまいました:disappointed_relieved:

目標

  • Goでのファイルアップロードの処理の書き方が分かる
  • ioパッケージやosパッケージの基本的な使い方が分かるようになる

前提条件

  • Go1.11以上かつGoが自分のパソコンにインストール済みであること

はじめに

まず最初に、作業する新しいディレクトリを作成します。私は**file-img-secret/**とします。

bash
$ mkdir file-img-secret
$ cd file-img-secret/

この新しいプロジェクトディレクトリ内で、次のコマンドを実行してください。
goモジュールを使用してプロジェクトを初期化します。

bash
$ go mod init github.com/Sheerlore/file-img-secret
go: creating new go.mod: module github.com/Sheerlore/file-img-secret

この新しいディレクトリ内に、Goプログラムへのメインエントリポイントとなるmain.goファイルを作成します。

file-img-secret/main.go
package main

import "fmt"

func main() {
	fmt.Println("FILE UPLOAD MONITOR")
}

つぎに、http:// localhost:8080で実行される単純なnet/httpベースのサーバーを作成します。

file-img-secret/main.go
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を作成して入力してください。

file-img-secret/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が表示されるようにしていきます。以下のように追加してください。

file-img-secret/main.go
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のformaction="/uploadmain.go/uploadが対応していることに注意してください。
また今はアクセスしても何も起きません。

ファイル処理

プロジェクト内にfileという名前で新しいディレクトリを作成してください。その中にupload.goという新しいファイルも作成します。
ここにuploadの際の処理を書いていきましょう!

ファイルのサイズ制限を付ける

巨大なファイルをアップロードさせないように、ファイルアップロードのサイズを制限する必要があります。
すぐに思いつく方法はリクエストヘッダーのContent-Lengthを確認してそれを最大サイズと比較する方法ですが、Content-Lengthヘッダーはクライアント側で任意の値に変更できてしまうため、この方法では不十分です。

if (r.ContentLength > Max_UPLOAD_SIZE) {エラー処理}

ここではhttpパッケージのhttp.MaxBytesReaderメソッドを使用します。以下のようにfile.goに追加してください。

file-img-secret/file/upload.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の末尾に以下のようにコードを追加します。

file-img-secret/file/upload.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に以下のように追加します。

file-img-secret/file/upload.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しましょう。以下のように追加してください。

file-img-secret/main.go
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というフォルダが作られて画像が保存できていることが確認できたと思います。

参考サイト

How to process file uploads in Go
Goならわかるシステムプログラミング

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?