GolangでS3からダウンロードしたファイルをzipにしてレスポンスする
こんにちは。
MMMサーバサイドエンジニアの柳沼です。お世話になっております。
北海道の夏はそろそろ終わりで、夜はだいぶ寒いです。
前回に引き続きgolangについて書いていきます。
S3のファイルをzipに固めて、それをAPIからレスポンスするやり方を紹介します。
S3からローカルにファイルをダウンロードする
流れとしては、
- S3からローカルにファイルをDLする
- ローカルのファイルをzipに固めて、レスポンスする
になります。
ダウンロードは、AWS SDKを使います。
s3managerという便利パッケージがあるので、こちらからダウンロードしていきます。
ダウンロードの流れとしては、
- セッション・s3managerを作成
- ローカルにファイルを作成する
- そこにS3からダウンロードしたファイルを書き込む
になります。
以下が実コードです。
// 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を引数に取れるので、
// wはhttp.ResponseWriter
zipWriter := zip.NewWriter(w)
こんな感じでzipWriterを作成します。(http.ResponseWriterは各APIフレームワークによって生成方法が異なるので、環境にあったやり方で作成してください。)
また、この時、zipをレスポンスするためには、レスポンスヘッダの設定が不可欠なので、
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=sample.zip")
も書いておいてください。
zipを作成する手順としては、
- ダウンロードしたファイルを開く
- zipの中に領域を作成する
- zipの中にダウンロードしたファイルを書き込む
になります。
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を返したくない場合、
if err := nil {
w.Header().Del("Content-Disposition")
としないと、不正なzipがレスポンスされてしまうので、気をつけてください。
(この辺はクライアントの仕様にもよりますが…)
ダウンロードを高速化してみよう
実際はS3から複数のファイルをダウンロードする箇所もあると思います。
その時、ファイルを1つずつダウンロードしていると、ファイルが多い時時間がかかります。
ここはgoroutineを使って、並列実行するようにしてみましょう。
今回は、複数のファイルをダウンロードしたいけど、もしひとつでもダウンロードにコケたらエラーとしたかったので、
sync.errgroupを使ってみます。
先にコードからお見せします。
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を使うと、重い処理が簡単に並列で書けて素晴らしいですね!
ぜひ参考にしてみてください。