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

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

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

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

流れとしては、

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

になります。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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
2
// wはhttp.ResponseWriter
zipWriter := zip.NewWriter(w)

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

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

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

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

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

になります。

1
2
3
4
5
6
7
8
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
2
if err := nil {
w.Header().Del("Content-Disposition")

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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を制御する

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