Edited at

Rails の ActiveStorage を S3 で使ったらPublicなURLが取れなかった


ActiveStorageの必要性

長年愛用していたRailsのファイルアップロードgemであるpaperclipがDeprecationとなってしまったため、仕方なくActiveStorageを使ってみたところ、PublicなURL(S3への直接のURL)が取得できませんでした。

仕方なく別途S3用のクラウドサービスを追加して対応しました。


環境


  • Rails 5.2.1

  • ActiveStorage 5.2.1


問題点

ActiveStorageではデフォルトでクラウドサービスへのアップロードに対応しており

- AWS S3

- GCP Cloud Storage

- Azure Storage

へのファイルアップロードは設定ファイルを書くだけで対応できます。

ただし、ファイルアップロードの際にアクセス権限を指定することができずS3ではprivateでのファイルアップロードとなります。

下記がgem内のS3サービスのファイルです。

ファイルアップロード処理(put)で渡しているoptionにacl(アクセス権限)が含まれていません。

@upload_options への設定の渡し方を調べればなんとかなるとは思っていましたが、次に調べたURLの取得でファイル自体を変更しないといけないことが判明しました。


activestorage-5.2.1/lib/active_storage/service/s3_service.rb

    def initialize(bucket:, upload: {}, **options)

@client = Aws::S3::Resource.new(**options)
@bucket = @client.bucket(bucket)

@upload_options = upload
end

def upload(key, io, checksum: nil)
instrument :upload, key: key, checksum: checksum do
begin
object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
rescue Aws::S3::Errors::BadDigest
raise ActiveStorage::IntegrityError
end
end
end


ファイルのURLを取得するときに事前署名URLpresigned_urlとなり、URLを使い回すことや長期間使用することが想定されていません。


activestorage-5.2.1/lib/active_storage/service/s3_service.rb

    def url(key, expires_in:, filename:, disposition:, content_type:)

instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i,
response_content_disposition: content_disposition_with(type: disposition, filename: filename),
response_content_type: content_type

payload[:url] = generated_url

generated_url
end
end


ちなみに、expires_inは最大1週間指定できるようですが、今回はOGPのog:imageに指定するURLに使用したかったのでPublicなURLじゃないと駄目です。


新S3サービスのファイルを作成

以下に既存の3サービスファイルからの修正点を記載します。


app/lib/active_storage/service/s3_service.rb

    def upload(key, io, checksum: nil)

instrument :upload, key: key, checksum: checksum do
begin
# aclでpublic-readを指定して必ず公開設定に変更.
object_for(key).put(upload_options.merge(body: io, content_md5: checksum, acl: 'public-read'))
rescue Aws::S3::Errors::BadDigest
raise ActiveStorage::IntegrityError
end
end
end

# ... 略

def url(key, expires_in:, filename:, disposition:, content_type:)
instrument :url, key: key do |payload|
# public_url で署名なしのPublic URLを取得.
generated_url = object_for(key).public_url
payload[:url] = generated_url
generated_url
end
end



注意点


  • S3へアップロードするファイルすべてがpublic-readになります。privateにアップロードするファイルとの共存はできなくなります。

  • ActiveStorage gemをアップデートする場合は、互換性がなくなる可能性があるため、ベースのS3ファイルとの差分をチェックすること

はやくActiveStorage自体でPublic URLの対応をしてくれないだろうか…