ActiveStorageのダイレクトアップロードを付属のJavaScriptライブラリ以外で使う
たとえば、graphqlなどアップロード機能を提供したいとき、
- ダイレクトアップロード用の
ActiveStorage::Blob
とURL等を生成 - 結果を受け取ってレコードにファイルを添付する
をMutationで実装したいことがあります。
- クライアントはアップロードしようとするファイルのMD5チェックサムとファイルサイズ、MIMEタイプを1のAPIサーバに送信します。
- APIサーバは、受け取ったパラメータからアップロード用の情報を返します。
- クライアントは受け取った情報を使ってクラウドストレージへダイレクトアップロードします。アップロードが終わったらAPIサーバにアップロードしたファイルを特定できる情報を送ります。
- APIサーバは対象のファイルを特定しレコードに紐付けます。
試したバージョン: Rails 6.1
1. ファイルの情報を取得する
Rubyによるサンプルコードです。 実際にはクライアントで実装することになるので、JavaだったりSwiftだったりします。 ファイルの情報を何らかの方法でサーバに送信します。
file_path = "/src/spec/fixtures/files/male.jpg" file_data = File.read(file_path) byte_size = file_data.bytes.size # => 57937 checksum = Base64.strict_encode64(Digest::MD5.digest(file_data)) # => "0Nq1WCcyKbbw4wipYw1xag==" content_type = Mime[File.extname(file_path).split('.').last].to_s # => "image/jpeg"
2. サーバでダイレクトアップロード用の情報を生成する
create_before_direct_upload!
で ActiveStorage::Blob
レコードを作成します。
ActiveStorage::Blob#service_url_for_direct_upload
および ActiveStorage::Blob#service_headers_for_direct_upload
でダイレクトアップロードに使用するURLとHTTPヘッダーを生成することができます。
blob = ActiveStorage::Blob.create_before_direct_upload!( filename: "file.jpg", byte_size: byte_size, checksum: checksum, content_type: content_type, service_name: "amazon" # 今回はS3を使う ) # アップロード用のURLとHTTPヘッダーを生成 blob_signed_id = blob.signed_id # アップロード後の紐付けに使う署名付きURL direct_upload_url = blob.service_url_for_direct_upload direct_upload_headers = blob.service_headers_for_direct_upload
アップロード用の情報には、MD5チェックサムやMIMEタイプなどのデジタル署名が含まれ、一致しないファイルのアップロードを防ぐことができます。 なので、サーバ側で事前にファイルサイズやMIMEタイプなどをみてアップロード許可を出すこともできます。
pp direct_upload_url # => "https://your_bucket_name.s3.ap-northeast-1.amazonaws.com/b6msshsvihnanisrlfwgpab9miqk?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AK000000000000000000%2F20201219%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20201219T021918Z&X-Amz-Expires=300&X-Amz-SignedHeaders=content-length%3Bcontent-md5%3Bcontent-type%3Bhost&X-Amz-Signature=c43651b09080ed543b1deb2f0baa8971251f06ff52360f8d31a1da7d6bc992ee" pp direct_upload_headers # => {"Content-Type"=>"image/jpeg", # "Content-MD5"=>"0Nq1WCcyKbbw4wipYw1xag==", # "Content-Disposition"=> # "inline; filename=\"file.jpg\"; filename*=UTF-8''file.jpg"}
# config/storage.yml amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: <%= Rails.application.credentials.dig(:aws, :region) %> bucket: your_bucket_name
3. クラウドストレージへダイレクトアップロード
Rubyによるサンプルコード。
ファイルのバイナリデータを、受け取ったURLあてに、受け取ったHTTPヘッダーと共に送ります。
一点注意が必要なのが Content-Length
ヘッダーでHTTPクライアントによっては自動でつけてくれないんので、その場合は送信するバイナリのサイズ(最初にサーバに伝えたバイト数)を自分で付けないと SignatureDoesNotMatch
になります。
url = URI.parse(direct_upload_url) req = Net::HTTP::Put.new(url.request_uri) req.initialize_http_header(direct_upload_headers) req.body = file_data # RubyのHTTP::Requestクラスはこれで content-length もつけてくれる req.each_header { |k,v| p "#{k}=#{v}" } # => "content-type=image/jpeg" # "content-md5=0Nq1WCcyKbbw4wipYw1xag==" # "content-disposition=inline; filename=\"file.jpg\"; filename*=UTF-8''file.jpg" # "connection=close" # "host=your_bucket_name .s3.ap-northeast-1.amazonaws.com" # "content-length=57937" http = Net::HTTP.new(url.host, url.port) http.use_ssl = true res = http.request(req) # => #<Net::HTTPOK 200 OK readbody=true>
4. サーバでファイルを特定しレコードに紐付ける
class User < ApplicationRecord has_one_attached :photo end blob = ActiveStorage::Blob.find_signed!(blob_signed_id) User.find(user_id).photo.attach(blob)