SpringでS3にあるいくつかのファイルをZipに固めてダウンロードする

2021.03.16

こんにちは。サービスGの金谷です。

今回はSpringプロジェクトでS3に置いてあるファイルをZipにまとめてダウンロードする方法を紹介します。

はじめに

今回紹介する方法はダウンロード対象となるS3オブジェクトに逐一アクセスしにいくような形なので、

ファイル数やサイズが大きくなるとパフォーマンス面でネックになるかもしれないのと、

S3はデータの取り出し操作にも多少の料金が発生するので、

可能であれば事前にS3側でファイルをZipにまとめてからダウンロードするのが一番良いと思います。

あくまでいくつかある方法の中の一つとして考えていただければと思います。

Springのプロジェクトを用意する

Spring initializr でプロジェクトを作成します。

今回は以下の設定で作成しました。

設定
Project Gradle Project
Language Java
Spring Boot 2.4.3
Project Metadata 特に変更なし
Packaging Jar
Java 8
Dependencies Spring Boot DevTools, Spring Web

設定できたらGENERATEを押してダウンロードします。

AWS SDKの依存関係を追加する

プロジェクトを取り込んだらAWS SDKの依存関係をbuild.gradleに追記します。

今回はS3のみ使用するのでaws-java-sdk-s3を追加します。

また、今回はversion1.11.969を使います。

dependencies {
  // ・・・略
	implementation group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.969'
}

S3にサンプルデータを配置

適当にバケットを作成し、ファイルを配置します。

今回の例では以下のデータを用意したことにします(実際は存在しません。各環境で適宜読み替えてください。)

バケット名: my-sample-bucket

オブジェクト1:directory1/sample.pdf

オブジェクト2:directory2/sample.pdf

Controllerを作成する

まずは適当にControllerクラスを追加してみます。

@RestController
public class DemoController {
    @GetMapping("/")
    public String index() {
        return "Hello Java";
    }
}

localhost:8080でアクセスしてHello Javaが表示できればOKです。

次にS3のファイルを取得するエンドポイントを追加していきます。

S3から1つのファイルをダウンロードする

以下のようにダウンロード処理をControllerに追記します。

@RestController
public class DemoController {

    @GetMapping("/download")
    public ResponseEntity<byte[]> download() throws IOException {
        // Profileやリージョンは適宜置き換える
        AmazonS3 s3 = AmazonS3ClientBuilder.standard()
                .withCredentials(new ProfileCredentialsProvider("PROFILE"))
                .withRegion(Regions.AP_NORTHEAST_1)
                .build();

        // バケット名とキーは適宜置き換える
        S3Object o = s3.getObject("my-sample-bucket", "directory1/sample.pdf");
        byte[] data = IOUtils.toByteArray(o.getObjectContent());

        HttpHeaders respHeaders = new HttpHeaders();
        respHeaders.setContentType(MediaType.APPLICATION_PDF);
        respHeaders.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=sample.pdf");
        return new ResponseEntity<byte[]>(data, respHeaders, HttpStatus.OK);
    }
}

S3クライアントの生成方法は色々あるので、各環境に合った方法で実行してください。今回はProfileから生成しています。

公式ドキュメントのサンプルでは使われていませんでしたが、S3オブジェクトからJavaのbyte配列への変換にはcom.amazonaws.util.IOUtilsを使用すると比較的楽に変換できました。

あとはいつも通りHeaderを設定してResponseEntityを返します。

複数のファイルをZipに固めてダウンロード

Java SDKには複数のS3のファイルをダウンロードするような機能はないので、自前で頑張ります。

最終的に以下のような形で動きました。

@RestController
public class DemoController {

    @GetMapping("/zip")
    public ResponseEntity<byte[]> zip() throws IOException {
        // キーの配列
        String[] objectKeys = {"directory1/sample.pdf", "directory2/sample.pdf"};

        // クライアントの作成
        AmazonS3 s3 = AmazonS3ClientBuilder.standard()
                .withCredentials(new ProfileCredentialsProvider("PROFILE"))
                .withRegion(Regions.AP_NORTHEAST_1)
                .build();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try(ZipOutputStream zos = new ZipOutputStream(baos)) {
            for(String objectKey : objectKeys) {
                // S3からデータ取得
                S3Object o = s3.getObject("my-sample-bucket", objectKey);
                byte[] data = IOUtils.toByteArray(o.getObjectContent());

                ZipEntry zip = new ZipEntry(objectKey);
                zos.putNextEntry(zip);
                zos.write(data);
                zos.closeEntry();
            }
        }
        byte[] responseData = baos.toByteArray();

        HttpHeaders respHeaders = new HttpHeaders();
        respHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        respHeaders.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=data.zip");
        return new ResponseEntity<byte[]>(responseData, respHeaders, HttpStatus.OK);
    }
}

キーの数だけループし、オブジェクトを取得しつつZipにまとめています。

署名付きURLなどを使う場合もS3Objectを取得する部分を変更すれば使用できると思います。

JavaでのZip圧縮周りのことは前回書いた記事にまとめていますのでそちらをご参照ください。