Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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の対応をしてくれないだろうか…

chchu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした