AWS SDKでS3バケット内のフォルダ構造を維持したまま、ZIPファイル形式でダウンロードする方法

## はじめに
Fusicでエンジニアをしています。みかみよしきです。
Fusic Advent Calendar を機に記事執筆に初挑戦しました。よろしくお願いします。

AWS SDKを使用してS3バケット内のファイルをフォルダ構造を維持したままZIP形式でダウンロードする方法を解説します。この機能はS3上の大量データを効率的に取得するために有用です。

Ruby on Rails環境でAWS SDKを用いてこの機能を実装する方法について、コード例を提示しつつ説明していきます。S3バケットからのデータ取得、ZIPファイルの作成、そしてクライアントへのダウンロードまでの工程を行います。

## 前提知識
AWS SDK、Amazon S3、rubyzipについて前提知識を説明します。

### AWS SDK
AWSのサービスをプログラムで利用するためのツールキット(Software Devlopment Kit)です。このSDKを使用することで、AWSのリソースを直接コードから管理できます。

### Amazon S3
Amazon S3は、インターネット経由で利用できるオブジェクトストレージサービスです。データのバックアップ、アーカイブ、大規模データの保存など、多様な用途で使用されています。S3は高い耐久性と可用性を持ち、大量のデータを効率的に扱うことが可能です。

### rubyzip
rubyzipはRubyでZIPファイルを扱うためのライブラリで、ファイルやディレクトリの圧縮、解凍機能を提供します。

### 説明を省く内容
- AWSのセットアップ、セキュリティ設定方法
- ローカル開発環境のセットアップ方法（credentialsなど）
- viewの実装

## 実装手順
AWS SDKを用いてRuby on RailsアプリケーションでZIPダウンロードを行う具体的な手順を紹介します。S3からファイルを取得し、ローカルシステム上でZIPファイルを作成し、最終的にクライアントにそのZIPファイルを提供します。

### 1. 必要なGemのインストール、Aws.configの設定
rubyzipとaws-sdk-s3を追加し、bundle installを実行します。

```ruby:Gemfile
gem 'rubyzip'
gem 'aws-sdk-s3'
```

initializersに新規ファイルを作成し、S3、クレデンシャルの設定を追記します。

```ruby:aws_sdk.rb
Aws.config.update(
  region: 'リージョン名',
  credentials: Aws::Credentials.new(
    Rails.application.credentials.aws.access_key_id,
    Rails.application.credentials.aws.secret_access_key
  )
)
```

### 2. ルーティングの設定
ダウンロード用のルートを設定します。

```ruby:routes.rb
get "sample/download" => "samples#download"
```

### 3. コントローラの作成
downloadアクションを定義します。ここではS3からファイルを取得し、ZIPファイルを作成してレスポンスとして送ります。

send_dataメソッドを使用してZIPファイルをクライアントに送信し、処理が終わったら作成したZIPファイルをサーバーから削除します。

```ruby:samples_controller.rb
class SamplesController < ApplicationController
  # ダウンロード処理のアクション
  def download
    folder_name = 'S3フォルダ名'
    folder_path = File.join(folder_name)
    zip_file_name = "#{folder_name}.zip"
    zip_file_path = Rails.root.join(zip_file_name)

    # ZipCreaterインタラクターを使用してZIPファイルを作成
    ZipCreater.new.call(zip_file_name, zip_file_path, folder_path)

    # ~ 6.の内容を省略 ~
  end
end
```


### 4. インタラクター（ZipCreater）の作成
ZIPファイルの作成を担当するZipCreaterクラスを作成します。このクラスは、一時ディレクトリにS3からファイルをダウンロードし、それをZIP形式でアーカイブします。

```ruby:zip_creater.rb
require 'zip'
class ZipCreater
  # ZIPファイルの作成と書き出しを行うメソッド
  def call(zip_file_name, zip_file_path, folder_path)
    # 一時ディレクトリを作成し、そこにファイルをダウンロードしてZIP形式で保存
    exported_data = Dir.mktmpdir do |tmp_dir|
      # AWS S3クライアントを使用して指定されたフォルダからファイルをダウンロード
      AwsS3Client.new.download_folder(tmp_dir, folder_path)

      tmp_zip_file_path = File.join(tmp_dir, zip_file_name)
      create_zip_file(tmp_dir, tmp_zip_file_path)

      # ZIPファイルを読み込み、その内容を返す
      File.read(tmp_zip_file_path)
    end

    # 一時ディレクトリからZIPファイルを本来のパスに書き出す
    File.open(zip_file_name, 'wb') { |f| f.write(exported_data) }
  end

  private

  # 指定されたディレクトリ内のファイルをZIP形式でアーカイブするメソッド
  def create_zip_file(dir, zip_file_path)
    Zip::File.open(zip_file_path, Zip::File::CREATE) do |zipfile|
      Dir[File.join(dir, '**', '**')].each do |file|
        next if File.directory?(file)  # ディレクトリは無視

        # ディレクトリ構造を維持しつつ、ファイルをZIPに追加
        zipfile.add(file.sub("#{dir}/", ''), file)
      end
    end
  end
end


```


### 5. AWS S3クライアントの設定
AwsS3Clientクラスを作成し、S3バケットからファイルを取得するメソッドを実装します。このクラスはS3サービス関連の以下のような機能を担当しています。

- コンストラクタでAWSの設定を更新し、S3クライアントとバケットオブジェクトを初期化
- download_folderメソッドは、指定されたS3フォルダからファイルを取得し、それらをローカルの一時ディレクトリに保存
- file_pathsメソッドは、指定されたフォルダ内のすべてのファイルパスを取得するために使用

```ruby:aws_s3_client.rb
class AwsS3Client
  # 初期化時にAWSの設定を行う
  def initialize
    bucket_name = 'S3バケット名'
    @s3_client = Aws::S3::Client.new
    @bucket = Aws::S3::Bucket.new(bucket_name)
  end

  # 指定されたS3フォルダからファイルをダウンロードするメソッド
  def download_folder(dir, folder_path)
    file_paths(folder_path).each do |file_path|
      sub_path = file_path.sub(folder_path, '')
      temporary_file_path = File.join(dir, sub_path)
      FileUtils.mkdir_p(File.dirname(temporary_file_path))

      # S3からファイルを取得し、一時ファイルに書き出す
      File.open(temporary_file_path, 'wb') do |f|
        file_body = @s3_client.get_object(bucket: @bucket.name, key: file_path).body.read
        f.write(file_body)
      end
    end
  end

  private

  # 指定されたフォルダパスにあるファイルのパスを取得するメソッド
  def file_paths(folder_path)
    @bucket.objects(prefix: folder_path).each_with_object([]) do |object, result|
      key = object.key
      next if key == folder_path || key.end_with?('/')  # フォルダは無視

      result << key
    end
  end
end
```


### 6. ZIPファイルのダウンロード
downloadアクションでZIPファイルを作成し、クライアントに送信します。
```ruby:samples_controller.rb
def download
  # ~ 3.の内容を省略 ~
  
  # 作成したZIPファイルをクライアントに送信
  send_data(
    File.read(zip_file_path),
    filename: zip_file_name,
    type: 'application/zip',
    disposition: 'attachment'
  )
  File.delete(zip_file_path)
end
```

以上の手順により、S3バケット内のファイルをZIP形式でダウンロードする機能が実装できます。

## まとめ
この記事では、Ruby on Railsを使用してAWS SDKでS3バケット内のファイルをフォルダ構造を維持したままZIP形式でダウンロードする方法について説明しました。初めての記事執筆ですので、至らぬ点は指摘いただけると幸いです。

## 参考文献
### 公式リファレンス
AWS SDK for Ruby APIドキュメント
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/

Rubyzip Github 
https://github.com/rubyzip/rubyzip

Ruby on Railsガイド
https://railsguides.jp/

### 参考になった記事
 https://qiita.com/Kta-M/items/b9f22277e983a05389fb