kenju's blog

About Programming, Mathematics, Security and Blockchain

AWS SDK for RubyでS3のファイルを扱う時はdownload_file/upload_fileを使う

tl;dr

  • S3へのファイルのアップロードには、#put#upload_fileもいずれも使えるが、特別な理由がない限り#upload_fileを使うべき
  • 同様に、ダウンロードする際は#download_fileを使う
s3 = Aws::S3::Resource.new(...)

obj = s3.bucket('bucket-name').object('key')

# upload
obj.upload_file('/source/file/path', acl:'public-read')

# download
obj.download_file('key')

理由

AWS::S3::Object#upload_file, AWS::S3::Object#download_fileいずれも、容量が大きいオブジェクトの処理の時に自動でmultipartで処理をしてくれるから。

Uploading Files to Amazon S3 | AWS Developer Blog

I recommend you use #upload_file whenever possible. If you need to manage large object copies, then you will need to use the multipart interfaces. There are restrictions on the minimum file, and part sizes you should be aware of. Typically these are reserved for advanced use cases.

補足

#download_fileの方だけ、ライブラリ側の実装を見てみる。

https://github.com/aws/aws-sdk-ruby/blob/master/aws-sdk-resources/lib/aws-sdk-resources/services/s3/object.rb

内部的には、FileDownloaderクラスを呼び出しているだけ。したがって、次はFileDownloaderを見ていく。

      def download_file(destination, options = {})
        downloader = FileDownloader.new(client: client)
        downloader.download(
          destination, options.merge(bucket: bucket_name, key: key))
        true
      end

https://github.com/aws/aws-sdk-ruby/blob/master/aws-sdk-resources/lib/aws-sdk-resources/services/s3/file_downloader.rb

      def download(destination, options = {})
        ...

        case @mode
        when "auto" then multipart_download
        when "single_request" then single_request
        when "get_range"
          if @chunk_size
            resp = @client.head_object(bucket: @bucket, key: @key)
            # ここがポイント
            multithreaded_get_by_ranges(construct_chunks(resp.content_length))
          else
          ...
        end
      end

     def multithreaded_get_by_ranges(chunks)
        # thread_batchesメソッドで色々処理をしているらしい
        thread_batches(chunks, 'range')
     end

ということで、multipart download処理の本質は、private methodのmultithreaded_get_by_rangesで処理されているようだ。メソッド名からして、マルチスレッドでファイルを処理していることが予想される。

早速見てみる。

    def thread_batches(chunks, param)
        batches = file_batches(chunks, param)
        parts = batches.flat_map(&:values)
        begin
          # chunksにもとづいて分割されたバッチ処理インスタンスをiterate
          batches.each do |batch|
            threads = []
            batch.each do |chunk, file|
              # ここで新しいスレッドを生成
              threads << Thread.new do
                # ここでget_objectしているようだ
                resp = @client.get_object(
                  :bucket => @bucket,
                  :key => @key,
                  param.to_sym => chunk,
                  :response_target => file
                )
              end
            end
            # スレッドを結合
            threads.each(&:join)
          end
          concatenate_parts(parts)
        ensure
          clean_up_parts(parts)
        end
      end

HTTP Clientで、multipart download処理をマルチスレッドで行う際の、実装例として参考になるだろう。multipart uploadのコードは読んでいないが、おそらく同じロジックで実現できる。