Go言語(golang)でAmazon S3から複数ファイルをダウンロードする

author potpro(ぼとぷろ)
2020/03/16

Go言語(golang)でAmazon S3から複数ファイルをダウンロードする

Go言語(golang)でAmazon S3へ複数ファイルをダウンロードする。

ここで重要なのはまとめてダウンロード。

単体のダウンロードが出来るんだから、それをforなりで回せばいいじゃないという話ではあるが、毎回S3 Getが飛ぶしパフォーマンス的にも良くない。1回で複数ダウンロードしたい。

割と簡単な感じがあるんですけど、実際調べても全然国内の記事が見つからない。

それで探したところ、ドキュメント上にはちゃんと書いてあった。でもやっぱりというか説明不足感はある感じではあったので、記事を書く。

一番乗りの記事ということで。単に出てこなかっただけかもしれないけど。

aws-sdk-goをインストール

今回使用しているものはAWS公式のaws-sdk-goです。

これはAWS SDKでS3以外にも使用可能。s3を操作するときは基本的にs3managerを使用すればOK。

単体ダウンロードの場合

s3managerのDownload関数を使用することで行える。

package main

import (
    "fmt"
    "os"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
    creds := credentials.NewStaticCredentials("AccessKey", "secret", "")

    sess := session.Must(session.NewSession(&aws.Config{
        Credentials: creds,
        Region:      aws.String("ap-northeast-1"),
    }))

    filename := "test.txt"

    // Create a downloader with the session and default options
    svc := s3manager.NewDownloader(sess)

    // Create a file to write the S3 Object contents to.
    f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(fmt.Errorf("failed to create file %q, %v", filename, err))
        return
    }
    defer f.Close()
    // Write the contents of S3 Object to the file
    n, err := svc.Download(f, &s3.GetObjectInput{
        Bucket: aws.String("bucketName"),
        Key:    aws.String("/test.txt"),
    })
    if err != nil {
        fmt.Println(fmt.Errorf("failed to download file, %v", err))
        f.Close()
        os.Remove(fileName)
        return
    }
    fmt.Printf("file downloaded, %d bytes\n", n)
}

単体の場合はこんな感じ。ダウンロードはそのままファイルに出力してくれないので、Writerを使用して書き込む必要がある。なので、ファイル出力せず使用することも可能となっている。

でも一度createする必要があるということでエラーだと空ファイルが出来ちゃったりする。 なのでエラー時os.Removeで削除するコードを入れてます。

複数ダウンロードの場合

s3managerのDownloadWithIterator関数を使用することで行える。

これを使用する場合はs3manager.BatchDownloadObjectのスライスを作成し、イテレータにセットすることで動作する。

関数とかはいろいろ略。

    creds := credentials.NewStaticCredentials("AccessKey", "secret", "")

    sess := session.Must(session.NewSession(&aws.Config{
        Credentials: creds,
        Region:      aws.String("ap-northeast-1"),
    }))
    svc := s3manager.NewDownloader(sess)
    objects := []s3manager.BatchDownloadObject{}
    fileNames := []string{
        "test1.txt",
        "test2.txt",
    }
    for _, fileName := range files {
        file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
            fmt.Println(err)
        }
        defer file.Close()
        objects = append(objects, s3manager.BatchDownloadObject{
            Object: &s3.GetObjectInput{
                Bucket: aws.String("bucketName"),
                Key:    aws.String(fileName),
            },
            Writer: file,
        })
    }
    iter := &s3manager.DownloadObjectsIterator{Objects: objects}
    err := svc.DownloadWithIterator(aws.BackgroundContext(), iter)
    if err != nil {
        fmt.Println(err)
    }

こんな感じ。DownloadObjectsIteratorにダウンロードしたいものを全て書き込んだ後にダウンロードする。

エラー処理

この場合、失敗した時のエラーはどうなるのか?という話なのだが・・・

DownloadWithIteratorのエラーは通常のerrorなので、全てのファイルのエラーが固まった状態で帰ってくる。

つまり、100ファイルをダウンロードして、1ファイルがエラーになってもerrが返ってきてしまう。

なので1ファイルごとのエラーを取得できないか、という話になってくるんだけど、実はできる。

DownloadObjectにAfterという関数が用意されていて、これが処理後にエラーがあれば格納されるっぽい。

エラーが無ければ何も入らない。


# 処理後
  svc.DownloadWithIterator(aws.BackgroundContext(), iter)
    if iter.Next(){
        if iter.DownloadObject().After(){
            fmt.Println(iter.DownloadObject().After())
        }
    }

また、iter.DownloadObject().ObjectにKeyやWriterの情報が入っているので、ここを使うことで1つずつのエラーの処理が出来る。

しかし、個別ダウンロードでやった"エラー時は削除"の処理は難しかった・・・というかiter.DownloadObject().Objectにfilenameが存在しなくてどうしよう・・・

ということで、処理が終わった後に全部見て、空のファイルを削除するという処理を入れた。

// emptyFileDelete 空のファイルを削除
func emptyFileDelete(dir string) {
    files, _ := ioutil.ReadDir(dir)
    for _, file := range files {
        if !file.IsDir() && file.Size() == 0 {
            os.Remove(filepath.Join(dir, file.Name()))
        }
    }
    return
}

しかしこれだとS3に置いてあるファイルが空だと消されちゃうんじゃ・・・という気もしてきた。難しいですね・・・。

やっぱり普通よりエラー処理がかなり難しくなるので、本当に大量のファイルを取得するバッチ処理とかじゃなければ1ファイルずつ処理でいいかもしれない。

以上、今回は小ネタな感じです。