1
0

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 1 year has passed since last update.

チーム開発参加の記録【2023-06~2023-08】(1) Go+Ginで画像をダウンロード/アップロードするAPIを作る

Last updated at Posted at 2023-06-10

2023年6月から、あるオンラインサロンでチーム開発が始まりまして、学習目的で参加しています(2023年8月に終了します)。
私はチーム03のバックエンド側メンバーに加わりました。
チーム03のバックエンドは、Go+Ginを使用することになりました。
チーム開発に参加しながら、私の学習の軌跡を記事にしていきたいと思います。

本シリーズのリンク

※ 本記事のソースコードは主に学習・検証目的で書いたものであり、プロダクトにそのまま使用できる品質を目指していません。

本記事で行うこと

  • Go+Ginで、画像ファイルをダウンロード/アップロードするAPIを作成します。
  • アップロードするAPIは2種類作成します。
    • GoCVを使用してWebP形式に変換してアップロードするAPI
    • 画像形式を変換せずにそのままアップロードするAPI
  • アップロード先はCloud Storageとします。
  • アプリのデプロイ先はCloud Runとします。

ソースコード

ソースコード全体は後に載せることにして、まずは各APIを見ていきます。

画像ダウンロードAPI(GET)

  • クエリパラメータ
    • path:Cloud Storageのpathを指定

func getImage(c *gin.Context) {
	ctx := context.Background()

	// クエリパラメータ
	path := c.Query("path")

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// Cloud Storageから画像read
	obj := bucket.Object(path)
	reader, err := obj.NewReader(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer reader.Close()

	c.DataFromReader(http.StatusOK, reader.Attrs.Size, reader.Attrs.ContentType, reader, nil)
}

このAPIの動作確認のために、あらかじめCloud Storageに私が撮影した写真ファイルを用意しました。

  • path:2023-06-10/うちの雪の日の庭.jpg

image.png

Postmanを使ってAPIを呼ぶと、以下のように画像が表示されました。

image.png

画像アップロードAPI:WebP形式に変換バージョン(POST)

  • リクエストボディ
    • dir:Cloud Storageの格納先ディレクトリ
    • uploadFile:画像ファイル

type postWebpRequest struct {
	Dir string `form:"dir"`
}

type postWebpResponse struct {
	Message string `form:"message"`
	Path    string `form:"path"`
}

func postWebp(c *gin.Context) {
	ctx := context.Background()

	// リクエストボディ(uploadFile以外)
	var request postWebpRequest
	err := c.Bind(&request)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// リクエストボディ(uploadFile)
	fi, uploadedFile, err := c.Request.FormFile("uploadFile")
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer fi.Close()

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// ファイルをバイナリ形式で読み込み
	buf := bytes.NewBuffer(nil)
	if _, err := io.Copy(buf, fi); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// バイナリを画像形式に変換
	img, err := gocv.IMDecode(buf.Bytes(), gocv.IMReadColor)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// 画像をWebP形式に変換。圧縮率80にしました。
	webp, err := gocv.IMEncodeWithParams(".webp", img, []int{gocv.IMWriteWebpQuality, 80})
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// アップロード先pathの調整(画像ファイル名に拡張子「.webp」を追加)
	dir := request.Dir
	if 0 < len(dir) {
		if dir[0] == '/' {
			dir = dir[1:]
		}
	}
	if 0 < len(dir) {
		if dir[len(dir)-1] == '/' {
			dir = dir[:len(dir)-1]
		}
	}
	var path string
	if 0 < len(dir) {
		path = dir + "/" + uploadedFile.Filename + ".webp"
	} else {
		path = uploadedFile.Filename + ".webp"
	}

	// WebP画像をCloud Storageにwrite
	obj := bucket.Object(path)
	writer := obj.NewWriter(ctx)
	if _, err := writer.Write(webp.GetBytes()); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	if err := writer.Close(); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	c.JSON(http.StatusOK, postWebpResponse{
		Message: "file uploaded successfully",
		Path:    writer.Attrs().Name,
	})
}

このAPIを使って、まずjpegファイル「うちの雪の日の庭.jpg」をアップロードしてみます(Postman使用)。

image.png

APIを呼んだら、Cloud Storage側に新たに「うちの雪の日の庭.jpg.webp」が現れました。
ファイルサイズが元のjpgファイルより小さくなっています。

image.png

image.png

次にpngファイル「開発画面.png」もアップロードしてみます(Postman使用)。

image.png

APIを呼んだら、Cloud Storage側に新たに「開発画面.png.webp」が現れました。

image.png

image.png

画像アップロード(画像形式そのままバージョン) POST API

WebP形式の画像をアップロードするために用意したAPIですが、WebP以外でも使えます。

  • リクエストボディ
    • dir:Cloud Storageの格納先ディレクトリ
    • uploadFile:画像ファイル

type postImageRequest struct {
	Dir string `form:"dir"`
}

type postImageResponse struct {
	Message string `form:"message"`
	Path    string `form:"path"`
}

func postImage(c *gin.Context) {
	ctx := context.Background()

	// リクエストボディ(uploadFile以外)
	var request postImageRequest
	err := c.Bind(&request)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// リクエストボディ(uploadFile)
	fi, uploadedFile, err := c.Request.FormFile("uploadFile")
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer fi.Close()

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// アップロード先pathの調整
	dir := request.Dir
	if 0 < len(dir) {
		if dir[0] == '/' {
			dir = dir[1:]
		}
	}
	if 0 < len(dir) {
		if dir[len(dir)-1] == '/' {
			dir = dir[:len(dir)-1]
		}
	}
	var path string
	if 0 < len(dir) {
		path = dir + "/" + uploadedFile.Filename
	} else {
		path = uploadedFile.Filename
	}

	// 画像をCloud Storageにwrite
	obj := bucket.Object(path)
	writer := obj.NewWriter(ctx)
	if _, err := io.Copy(writer, fi); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	if err := writer.Close(); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	c.JSON(http.StatusOK, postImageResponse{
		Message: "file uploaded successfully",
		Path:    writer.Attrs().Name,
	})
}

このAPIを使って、pngファイル「開発画面.png」をアップロードしてみます(Postman使用)。

image.png

APIを呼んだら、Cloud Storage側に新たに「開発画面.png」が現れました。

image.png

image.png

ソースコード全体

main.go

main.go

package main

import (
	"bytes"
	"cloud.google.com/go/storage"
	"context"
	"github.com/gin-gonic/gin"
	"gocv.io/x/gocv"
	"io"
	"net/http"
)

type httpError struct {
	Error string `json:"error"`
}

var bucketName string = "your_bucket_0"

func healthCheck(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "ok",
	})
}

func getImage(c *gin.Context) {
	ctx := context.Background()

	// クエリパラメータ
	path := c.Query("path")

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// Cloud Storageから画像read
	obj := bucket.Object(path)
	reader, err := obj.NewReader(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer reader.Close()

	c.DataFromReader(http.StatusOK, reader.Attrs.Size, reader.Attrs.ContentType, reader, nil)
}

type postWebpRequest struct {
	Dir string `form:"dir"`
}

type postWebpResponse struct {
	Message string `form:"message"`
	Path    string `form:"path"`
}

func postWebp(c *gin.Context) {
	ctx := context.Background()

	// リクエストボディ(uploadFile以外)
	var request postWebpRequest
	err := c.Bind(&request)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// リクエストボディ(uploadFile)
	fi, uploadedFile, err := c.Request.FormFile("uploadFile")
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer fi.Close()

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// ファイルをバイナリ形式で読み込み
	buf := bytes.NewBuffer(nil)
	if _, err := io.Copy(buf, fi); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// バイナリを画像形式に変換
	img, err := gocv.IMDecode(buf.Bytes(), gocv.IMReadColor)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// 画像をWebP形式に変換。圧縮率80にしました。
	webp, err := gocv.IMEncodeWithParams(".webp", img, []int{gocv.IMWriteWebpQuality, 80})
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// アップロード先pathの調整(画像ファイル名に拡張子「.webp」を追加)
	dir := request.Dir
	if 0 < len(dir) {
		if dir[0] == '/' {
			dir = dir[1:]
		}
	}
	if 0 < len(dir) {
		if dir[len(dir)-1] == '/' {
			dir = dir[:len(dir)-1]
		}
	}
	var path string
	if 0 < len(dir) {
		path = dir + "/" + uploadedFile.Filename + ".webp"
	} else {
		path = uploadedFile.Filename + ".webp"
	}

	// WebP画像をCloud Storageにwrite
	obj := bucket.Object(path)
	writer := obj.NewWriter(ctx)
	if _, err := writer.Write(webp.GetBytes()); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	if err := writer.Close(); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	c.JSON(http.StatusOK, postWebpResponse{
		Message: "file uploaded successfully",
		Path:    writer.Attrs().Name,
	})
}

type postImageRequest struct {
	Dir string `form:"dir"`
}

type postImageResponse struct {
	Message string `form:"message"`
	Path    string `form:"path"`
}

func postImage(c *gin.Context) {
	ctx := context.Background()

	// リクエストボディ(uploadFile以外)
	var request postImageRequest
	err := c.Bind(&request)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	// リクエストボディ(uploadFile)
	fi, uploadedFile, err := c.Request.FormFile("uploadFile")
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer fi.Close()

	// Cloud Storageのclientとbucket
	client, err := storage.NewClient(ctx)
	if err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	defer client.Close()
	bucket := client.Bucket(bucketName)

	// アップロード先pathの調整
	dir := request.Dir
	if 0 < len(dir) {
		if dir[0] == '/' {
			dir = dir[1:]
		}
	}
	if 0 < len(dir) {
		if dir[len(dir)-1] == '/' {
			dir = dir[:len(dir)-1]
		}
	}
	var path string
	if 0 < len(dir) {
		path = dir + "/" + uploadedFile.Filename
	} else {
		path = uploadedFile.Filename
	}

	// 画像をCloud Storageにwrite
	obj := bucket.Object(path)
	writer := obj.NewWriter(ctx)
	if _, err := io.Copy(writer, fi); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}
	if err := writer.Close(); err != nil {
		c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
		return
	}

	c.JSON(http.StatusOK, postImageResponse{
		Message: "file uploaded successfully",
		Path:    writer.Attrs().Name,
	})
}

func main() {
	router := gin.Default()
	router.GET("/", healthCheck)
	router.GET("/getImage", getImage)
	router.POST("/postWebp", postWebp)
	router.POST("/postImage", postImage)

	router.Run("0.0.0.0:8080")
}

go.mod

go.mod

module exercise_image

go 1.20

require (
	cloud.google.com/go/storage v1.30.1
	github.com/gin-gonic/gin v1.9.1
	gocv.io/x/gocv v0.32.1
)

require (
	cloud.google.com/go v0.110.0 // indirect
	cloud.google.com/go/compute v1.18.0 // indirect
	cloud.google.com/go/compute/metadata v0.2.3 // indirect
	cloud.google.com/go/iam v0.12.0 // indirect
	github.com/bytedance/sonic v1.9.1 // indirect
	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.14.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
	github.com/golang/protobuf v1.5.2 // indirect
	github.com/google/go-cmp v0.5.9 // indirect
	github.com/google/uuid v1.3.0 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
	github.com/googleapis/gax-go/v2 v2.7.1 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
	github.com/leodido/go-urn v1.2.4 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.11 // indirect
	go.opencensus.io v0.24.0 // indirect
	golang.org/x/arch v0.3.0 // indirect
	golang.org/x/crypto v0.9.0 // indirect
	golang.org/x/net v0.10.0 // indirect
	golang.org/x/oauth2 v0.6.0 // indirect
	golang.org/x/sys v0.8.0 // indirect
	golang.org/x/text v0.9.0 // indirect
	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
	google.golang.org/api v0.114.0 // indirect
	google.golang.org/appengine v1.6.7 // indirect
	google.golang.org/genproto v0.0.0-20230320184635-7606e756e683 // indirect
	google.golang.org/grpc v1.53.0 // indirect
	google.golang.org/protobuf v1.30.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

Cloud Runにデプロイ

OpenCVビルド済のコンテナイメージをDocker Hubにpushする

GoCVを使うために、OpenCV 4.7.0をソースコードからビルドする必要がありました。
ビルドに時間がかかるため、ビルドしたらDockerイメージをDocker Hubにpushして、以後使い回すことにしました。
以下のDockerfileを使用します。


FROM golang:1.20.5-bullseye

RUN apt-get update && apt-get install -y \
    unzip \
    cmake \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

RUN curl -SL https://github.com/opencv/opencv/archive/4.7.0.zip -o opencv-4.7.0.zip \
    && curl -SL https://github.com/opencv/opencv_contrib/archive/refs/tags/4.7.0.zip -o opencv_contrib-4.7.0.zip \
    && unzip opencv-4.7.0.zip \
    && unzip opencv_contrib-4.7.0.zip -d ./opencv-4.7.0/
RUN cd opencv-4.7.0 \
    && mkdir build \
    && cd build \
    && cmake -DOPENCV_GENERATE_PKGCONFIG=ON -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib-4.7.0/modules .. \
    && cmake --build . \
    && make \
    && make install \
    && ldconfig

Docker Hubの以下の場所にpushしました。

アプリをCloud Runにデプロイする

お次はアプリ用のDockerfileを用意します。
Docker Hubに上げた「kanedasmec/golang_opencv:1.20.5_4.7.0」を利用しています。


FROM kanedasmec/golang_opencv:1.20.5_4.7.0 as builder

ENV APP_HOME /app
WORKDIR $APP_HOME
COPY ./ ./
RUN go build main.go

FROM debian:bullseye-slim
LABEL version="0.1"

RUN apt-get update && apt-get install -y \
    lsb-release \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /usr/local/ /usr/local/
COPY --from=builder /app/main /app/

ENV APP_HOME /app
WORKDIR $APP_HOME
RUN ldconfig

EXPOSE 8080
CMD ["./main"]

このDockerfileを使って、Cloud Run用にビルドします。
your_projectにはGoogle Cloudのプロジェクト名を入れます。
your_tagにはタグ(バージョン)を設定します。


gcloud builds submit --tag gcr.io/your_project/exercise:your_tag

ビルドに成功したら、Cloud Runにデプロイします。
各パラメータ等は調節した方が良いでしょう。


gcloud run deploy exercise \
    --image gcr.io/your_project/exercise:your_tag \
    --platform managed \
    --cpu 1 \
    --max-instances 1 \
    --concurrency 1 \
    --memory 1024Mi \
    --timeout 3600 \
    --allow-unauthenticated \
    --region us-central1 \
    --service-account your_identity

デプロイ後、ブラウザでGET APIを呼んでみました。

image.png

POST APIの動作確認は、Postmanを使用すると良いでしょう。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?