GolangでS3からダウンロードしたファイルをzipにしてレスポンスする

こんにちは。
MMMサーバサイドエンジニアの柳沼です。お世話になっております。
北海道の夏はそろそろ終わりで、夜はだいぶ寒いです。

前回に引き続きgolangについて書いていきます。
S3のファイルをzipに固めて、それをAPIからレスポンスするやり方を紹介します。

S3からローカルにファイルをダウンロードする

流れとしては、

  • S3からローカルにファイルをDLする
  • ローカルのファイルをzipに固めて、レスポンスする

になります。

ダウンロードは、AWS SDKを使います。
s3managerという便利パッケージがあるので、こちらからダウンロードしていきます。

ダウンロードの流れとしては、

  • セッション・s3managerを作成
  • ローカルにファイルを作成する
  • そこにS3からダウンロードしたファイルを書き込む

になります。
以下が実コードです。

1
// s3manager作成
// リージョンは環境に合わせる
sess := (session.New(&aws.Config{Region: aws.String("ap-northeast-1")}))
downloader := s3manager.NewDownloader(sess)

// ダウンロード領域を作成
os.MkdirAll("/tmp/download", 0755)
f, _ := os.Create("/tmp/download/sample.pdf")
defer f.Close()

// download実行
_, err = downloader.Download(f, &s3.GetObjectInput{
  Bucket: aws.String("bucket_name_sample"),
  Key:    aws.String("sample/sample.pdf"),
})

errがnilであれば、ダウンロードには成功すると思います。

ローカルにダウンロードしたファイルをzipに固める

zipファイルを扱うには、archive/zipパッケージを使います。

zip.NewWriter(w io.Writer) 関数で、zipWriterを作成するのですが、
http.ResponseWriterを引数に取れるので、

1
// wはhttp.ResponseWriter
zipWriter := zip.NewWriter(w)

こんな感じでzipWriterを作成します。(http.ResponseWriterは各APIフレームワークによって生成方法が異なるので、環境にあったやり方で作成してください。)
また、この時、zipをレスポンスするためには、レスポンスヘッダの設定が不可欠なので、

1
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=sample.zip")

も書いておいてください。

zipを作成する手順としては、

  • ダウンロードしたファイルを開く
  • zipの中に領域を作成する
  • zipの中にダウンロードしたファイルを書き込む

になります。

1
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()

file, _ := os.Open("/tmp/download/sample.pdf")
defer file.Close()
writer, _ := zipWriter.Create("download/sample.pdf")
defer writer.Close()
io.Copy(writer, file)

こんな感じで、zipをレスポンスすることができます。
これはサンプルコードなので省いてますが、実際はエラーハンドリングが必要になります。
注意点として、例えばエラー時はzipを返したくない場合、

1
if err := nil {
	w.Header().Del("Content-Disposition")

としないと、不正なzipがレスポンスされてしまうので、気をつけてください。
(この辺はクライアントの仕様にもよりますが…)

ダウンロードを高速化してみよう

実際はS3から複数のファイルをダウンロードする箇所もあると思います。
その時、ファイルを1つずつダウンロードしていると、ファイルが多い時時間がかかります。
ここはgoroutineを使って、並列実行するようにしてみましょう。

今回は、複数のファイルをダウンロードしたいけど、もしひとつでもダウンロードにコケたらエラーとしたかったので、
sync.errgroupを使ってみます。

先にコードからお見せします。

1
func main() error {
	fileNames := []string{"sample1.pdf","sample2.pdf","sample3.pdf"}
	eg := errgroup.Group{}
	for _, fileName := range fileNames {
	  r := r
	  eg.Go(func() error {
	    err := download(fileName)
	    return err
	  })
	}
	
	if err := eg.Wait(); err != nil {
	  panic(err)
	}
	// この後にレスポンス処理を書く
}

func download(fileName string) {
	// s3manager作成
	// リージョンは環境に合わせる
	sess := (session.New(&aws.Config{Region: aws.String("ap-northeast-1")}))
	downloader := s3manager.NewDownloader(sess)
	
	// ダウンロード領域を作成
	os.MkdirAll("/tmp/download", 0755)
	f, _ := os.Create("/tmp/download/" + fileName)
	defer f.Close()
	
	// download実行
	_, err = downloader.Download(f, &s3.GetObjectInput{
	  Bucket: aws.String("bucket_name_sample"),
	  Key:    aws.String("sample/" + fileName),
	})
	return err
}

eg.Go() の中のファンクションは、errgroupがgoroutineを立てて並列実行してくれます。
その後、 eg.Wait() が、立てたgoroutineの中の最初のエラーを返してくれるようになっています。

まとめ

goroutineを使うと、重い処理が簡単に並列で書けて素晴らしいですね!
ぜひ参考にしてみてください。

参考

sync.ErrGroupで複数のgoroutineを制御する

このエントリーをはてなブックマークに追加