Skip to content

So implementieren Sie HTTP-Datei-Uploads in Go | Unterstützung für net/http, multipart und S3

Kategorie:Go・Golang
Dieser Artikel ist derzeit nur auf Japanisch verfügbar. Übersetzte Versionen werden schrittweise veröffentlicht.

Mit den Go-Standardbibliotheken <code>net/http</code> und <code>mime/multipart</code> können Sie Datei-Uploads ohne externe Pakete implementieren. Dieser Artikel erläutert im Detail die produktionsreife Implementierung, von der Analyse des Multipart-Bodys mit <code>r.ParseMultipartForm()</code>, dem Abrufen von Dateien mit <code>r.FormFile()</code>, Dateigrößenbeschränkungen, MIME-Typ-Validierung bis zum S3-Upload mit AWS SDK v2.

Go Multipart-Verarbeitungssequenz Go Datei-Upload-Verarbeitungssequenz Client Handler Storage POST /upload (multipart/form-data) r.Body = http.MaxBytesReader(...) r.ParseMultipartForm(32 << 20) file, hdr, _ := r.FormFile("file") http.DetectContentType(buf[:512]) io.Copy(dst, file) 200 OK / JSON response
Abbildung 1: Verarbeitungssequenz für multipart-Uploads in net/http

Empfang von Multipart-Anfragen mit net/http

Um <code>multipart/form-data</code>-Anfragen zu empfangen, rufen Sie <code>r.ParseMultipartForm(maxMemory)</code> auf. <code>maxMemory</code> ist die maximale Anzahl von Bytes, die im Speicher gespeichert werden, und der Überschuss wird in eine temporäre Datei geschrieben.

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}

	// 最大10MBをメモリに保持し、超過分は /tmp に一時保存
	const maxMemory = 10 << 20 // 10MB
	if err := r.ParseMultipartForm(maxMemory); err != nil {
		http.Error(w, "フォームの解析に失敗しました: "+err.Error(), http.StatusBadRequest)
		return
	}

	// フォームの文字列フィールドを取得
	title := r.FormValue("title")
	fmt.Printf("title: %s\n", title)

	// ファイルを取得
	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "ファイルの取得に失敗しました: "+err.Error(), http.StatusBadRequest)
		return
	}
	defer file.Close()

	fmt.Printf("ファイル名: %s\n", header.Filename)
	fmt.Printf("ファイルサイズ: %d bytes\n", header.Size)
	fmt.Printf("Content-Type: %s\n", header.Header.Get("Content-Type"))

	// 保存先のパスを生成
	timestamp := time.Now().UnixNano()
	safeFilename := filepath.Base(header.Filename) // ディレクトリトラバーサル対策
	destPath := fmt.Sprintf("./uploads/%d_%s", timestamp, safeFilename)

	// ファイルを保存
	dst, err := os.Create(destPath)
	if err != nil {
		http.Error(w, "ファイルの作成に失敗しました", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, "ファイルの書き込みに失敗しました", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"success":true,"filename":%q}`, safeFilename)
}

func main() {
	os.MkdirAll("./uploads", 0755)
	http.HandleFunc("/upload", uploadHandler)
	log.Println("サーバー起動: :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Implementierung des Dateigröße-Limits

Mit <code>http.MaxBytesReader</code> können Sie die Menge der gelesenen Request-Body begrenzen. Da das Argument von <code>ParseMultipartForm</code> allein nicht die Gesamtgröße der Anfrage begrenzen kann, ist die Kombination mit <code>MaxBytesReader</code> wichtig.

package handler

import (
	"errors"
	"net/http"
)

const (
	MaxUploadSize = 50 << 20 // 50MB
	MaxMemory     = 10 << 20 // 10MB(メモリに保持する最大量)
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// リクエストボディ全体のサイズを制限
	r.Body = http.MaxBytesReader(w, r.Body, MaxUploadSize)

	if err := r.ParseMultipartForm(MaxMemory); err != nil {
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			http.Error(w, fmt.Sprintf("ファイルサイズが上限(%dMB)を超えています。", MaxUploadSize>>20), http.StatusRequestEntityTooLarge)
		} else {
			http.Error(w, "不正なリクエストです。", http.StatusBadRequest)
		}
		return
	}

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "ファイルの取得に失敗しました。", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// ヘッダーのサイズも確認(ParseMultipartForm 後でも確認可能)
	if header.Size > MaxUploadSize {
		http.Error(w, "ファイルが大きすぎます。", http.StatusRequestEntityTooLarge)
		return
	}

	// ... 以降の処理
}

MIME-Typ-Überprüfung

Der vom Client deklarierten <code>Content-Type</code>-Header kann gefälscht werden. Es ist sicher, die ersten Bytes des eigentlichen Dateiinhalts (Magic Number) zu lesen und die Überprüfung mit <code>http.DetectContentType</code> durchzuführen.

package handler

import (
	"fmt"
	"mime/multipart"
	"net/http"
)

var allowedMIMETypes = map[string]bool{
	"image/jpeg":      true,
	"image/png":       true,
	"image/gif":       true,
	"image/webp":      true,
	"application/pdf": true,
	"application/zip": true,
}

// detectMIMEType はファイルの先頭512バイトを読んでMIMEタイプを判定します
func detectMIMEType(file multipart.File) (string, error) {
	// 先頭512バイトを読む(http.DetectContentType は最大512バイトを使用)
	buf := make([]byte, 512)
	n, err := file.Read(buf)
	if err != nil && err.Error() != "EOF" {
		return "", fmt.Errorf("ファイルの読み込みに失敗しました: %w", err)
	}

	// ファイルポインタを先頭に戻す(後続の読み込みのため)
	if seeker, ok := file.(interface{ Seek(int64, int) (int64, error) }); ok {
		if _, err := seeker.Seek(0, 0); err != nil {
			return "", fmt.Errorf("シークに失敗しました: %w", err)
		}
	}

	mimeType := http.DetectContentType(buf[:n])
	return mimeType, nil
}

func validateFile(file multipart.File, header *multipart.FileHeader) error {
	// 実際のMIMEタイプを検出
	mimeType, err := detectMIMEType(file)
	if err != nil {
		return err
	}

	if !allowedMIMETypes[mimeType] {
		return fmt.Errorf("許可されていないファイル形式です(%s)", mimeType)
	}

	return nil
}

S3-Upload mit AWS SDK v2

Das AWS SDK v2 für Go ist gegenüber SDK v1 deutlich verbessert und zeichnet sich durch Kontext-Unterstützung und Modulteilung aus. Multipart-Upload wird automatisch von <code>s3manager.Upload</code> verarbeitet.

go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
go get github.com/aws/aws-sdk-go-v2/feature/s3/manager
package s3upload

import (
	"context"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

type S3Uploader struct {
	uploader *manager.Uploader
	bucket   string
}

func NewS3Uploader(ctx context.Context) (*S3Uploader, error) {
	// 環境変数(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)から設定を読み込む
	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithRegion(os.Getenv("AWS_REGION")),
	)
	if err != nil {
		return nil, fmt.Errorf("AWS 設定の読み込みに失敗しました: %w", err)
	}

	client := s3.NewFromConfig(cfg)
	uploader := manager.NewUploader(client, func(u *manager.Uploader) {
		u.PartSize = 10 * 1024 * 1024 // 10MB パートサイズ
		u.Concurrency = 3             // 並列アップロード数
	})

	return &S3Uploader{
		uploader: uploader,
		bucket:   os.Getenv("AWS_S3_BUCKET"),
	}, nil
}

func (u *S3Uploader) Upload(ctx context.Context, file io.Reader, header *multipart.FileHeader, mimeType string) (string, error) {
	// ユニークなキーを生成
	timestamp := time.Now().UnixNano()
	key := fmt.Sprintf("uploads/%d_%s", timestamp, header.Filename)

	result, err := u.uploader.Upload(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(u.bucket),
		Key:         aws.String(key),
		Body:        file,
		ContentType: aws.String(mimeType),
		// ACL を使わずバケットポリシーで制御する場合は省略
	})
	if err != nil {
		return "", fmt.Errorf("S3 へのアップロードに失敗しました: %w", err)
	}

	return result.Location, nil
}

// Presigned URL の生成(プライベートファイルへの一時アクセス)
func GeneratePresignedURL(ctx context.Context, bucket, key string, expiry time.Duration) (string, error) {
	cfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return "", err
	}

	client := s3.NewFromConfig(cfg)
	presigner := s3.NewPresignClient(client)

	req, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	}, s3.WithPresignExpires(expiry))
	if err != nil {
		return "", fmt.Errorf("Presigned URL の生成に失敗しました: %w", err)
	}

	return req.URL, nil
}

Implementierungsbeispiel mit Echo-Framework

Mit dem Web-Framework <code>Echo</code> können Sie Routing und Middleware einfach konfigurieren. Die Verarbeitung von Datei-Uploads funktioniert auf die gleiche Weise wie das Standard-<code>net/http</code>.

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.POST("/upload", func(c echo.Context) error {
		// マルチパートフォームを解析(最大32MBまでメモリに保持)
		if err := c.Request().ParseMultipartForm(32 << 20); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
		}

		file, err := c.FormFile("file")
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{"error": "ファイルが見つかりません"})
		}

		src, err := file.Open()
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}
		defer src.Close()

		// MIMEタイプの検証
		mimeType, err := detectMIMEType(src)
		if err != nil || !allowedMIMETypes[mimeType] {
			return c.JSON(http.StatusUnsupportedMediaType, map[string]string{
				"error": "許可されていないファイル形式です",
			})
		}

		return c.JSON(http.StatusOK, map[string]interface{}{
			"filename": file.Filename,
			"size":     file.Size,
			"mime":     mimeType,
		})
	})

	e.Logger.Fatal(e.Start(":8080"))
}

Testdatei zur Verwendung in diesem Artikel (kostenlos)

  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG-Testbild (1MB)</a> — Zum Test der MIME-Typ-Validierung und <code>http.DetectContentType</code>
  • → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF-Testdatei (1MB)</a> — Zur Überprüfung von S3-Upload und Multipart-Verarbeitung
  • → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">ZIP-Testdatei (1MB)</a> — Für Grenzwert-Tests der Dateigröße

Verwandte Artikel

  • → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Zusammenfassung der Datei-Upload-Limits von AWS S3 und CloudFront</a>
  • → <a href="/ja/blog/file-validation-checklist/" class="text-primary-600 dark:text-primary-400 hover:underline">Checkliste zur Implementierung der Dateiverifizierung für Web-Formulare</a>
  • → <a href="/ja/blog/laravel-file-upload/" class="text-primary-600 dark:text-primary-400 hover:underline">Implementierungsleitfaden für Datei-Uploads in Laravel | Validierung, Storage und S3-Unterstützung</a>