AWS

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

MMM Corporation
mmmuser

こんにちは。
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を使うと、重い処理が簡単に並列で書けて素晴らしいですね!
ぜひ参考にしてみてください。

参考

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

AUTHOR
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社はアマゾン ウェブ サービス(AWS)に 専門性や実績を認定された公式パートナーです。
記事URLをコピーしました