37
rails version 5.2

I have a scenario where I need to access the public URL of Rails Active Storage with Amazon S3 storage to make a zip file with Sidekiq background job.

I am having difficulty getting the actual file URL. I have tried rails_blob_url but it gives me following

http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBZUk9IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9598613be650942d1ee4382a44dad679a80d2d3b/sample.pdf

How do I access the real file URL through Sidekiq?

storage.yml

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

development:
  service: S3
  access_key_id: 'xxxxx'
  secret_access_key: 'xxxxx'
  region: 'xxxxx'
  bucket: 'xxxxx'

development.rb

  config.active_storage.service = :development

I can access fine these on web interface but not within Sidekiq

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Shani
  • 2,433
  • 2
  • 19
  • 23

8 Answers8

94

Use ActiveStorage::Blob#service_url. For example, assuming a Post model with a single attached header_image:

@post.header_image.service_url

Update: Rails 6.1

Since Rails 6.1 ActiveStorage::Blob#service_url is deprecated in favor of ActiveStorage::Blob#url.

So, now

@post.header_image.url

is the way to go.

Sources:

Marian13
  • 7,740
  • 2
  • 47
  • 51
George Claghorn
  • 26,261
  • 3
  • 48
  • 48
  • 6
    thanks for that answer. I'm used to using paperclip, so i'm trying to get down Active Storage. implemented an S3 solution w/ Active Storage and it works great. however, this solution is saying it's a temporary URL? do you know how to get the permanent URL of the image? is that possible with Active Storage? paperclip only returned the default permanent URL so i was expecting something similar with Active Storage – James N Jul 14 '18 at 05:43
  • @JamesN https://edgeguides.rubyonrails.org/active_storage_overview.html#linking-to-files Rails provides a rails blob url to redirect to the actual service – hungmi Aug 02 '18 at 03:43
  • 2
    This doesn't seem to create public URLs? I get `Request specific response headers cannot be used for anonymous GET requests.` from Amazon – HappyCry Aug 28 '18 at 23:16
  • @HappyCry I have the same problem, but I can't find any docs about this. the only lead I have is https://kevinjalbert.com/rails-activestorage-configuration-for-minio/ – Murilo Azevedo Jan 30 '19 at 18:52
  • Hey all, I had the same needs/issues as this comment chain and ended up here, so I added my final solution below for posterity https://stackoverflow.com/a/55172966/484689 – genkilabs Mar 14 '19 at 22:31
  • This works! Hallelujah. My object has multiple attachments and this worked for me: @listing.images.first.service_url. I only need the first image, but could easily create a loop to get all pics if needed. – tomb Apr 08 '19 at 12:57
  • As explained in the docs (https://api.rubyonrails.org/classes/ActiveStorage/Blob.html#method-i-service_url) this will return only a temporarily accessible URL. – Christian Butzke Apr 06 '20 at 13:47
  • As such, you can use it if the URL is only required for a short time. However, if e.g. those URLs will be cached somewhere else and reused later, they will not work anymore. In that case, rails_blob_url is more convenient, since the URL will be a permalink – Christian Butzke Apr 06 '20 at 14:27
  • Late here but I strongly recommend denormalizing the `service_url` on a column of the object this image is for. You don't want accidental n+1 problem when you load a multitude of posts, for example. – rnmp May 06 '20 at 18:58
  • FYI url method should be used to generate short-lived private URLs which then shouldn't be visible to users directly as documentation suggest here -> https://api.rubyonrails.org/v6.1.0/classes/ActiveStorage/Blob.html#method-i-url However, it does not specify how to get long-lived url which will redirect user to the signed url. It can be done like this -> Rails.application.routes.url_helpers.url_for(blob) – Tomáš Tibenský Jan 21 '21 at 13:35
  • Thanks. It helped me to read an ActiveStorage S3 image with Rmagick – Yshmarov Oct 14 '22 at 10:02
17

If you need all your files public then you must make public your uploads:

In file config/storage.yml

amazon:
  service: S3
  access_key_id: zzz
  secret_access_key: zzz
  region: zzz
  bucket: zzz
  upload:
    acl: "public-read"

In the code

attachment = ActiveStorage::Attachment.find(90)
attachment.blob.service_url # returns large URI
attachment.blob.service_url.sub(/\?.*/, '') # remove query params

It will return something like: "https://foo.s3.amazonaws.com/bar/buz/2yoQMbt4NvY3gXb5x1YcHpRa"

It is public readable because of the config above.

Aivils Štoss
  • 858
  • 11
  • 9
16

My use case was to upload images to S3 which would have public access for ALL images in the bucket so a job could pick them up later, regardless of request origin or URL expiry. This is how I did it. (Rails 5.2.2)

First, the default for new S3 bucked is to keep everything private, so to defeat that there are 2 steps.

  1. Add a wildcard bucket policy. In AWS S3 >> your bucket >> Permissions >> Bucket Policy
{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "AllowPublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        }
    ]
}
  1. In your bucket >> Permissions >> Public Access Settings, be sure Block public and cross-account access if bucket has public policies is set to false

Now you can access anything in your S3 bucket with just the blob.key in the url. No more need for tokens with expiry.

Second, to generate that URL you can either use the solution by @Christian_Butzke: @post.header_image.service.send(:object_for, @post.header_image.key).public_url

However, know that object_for is a private method on service, and if called with public_send would give you an error. So, another alternative is to use the service_url per @George_Claghorn and just remove any params with a url&.split("?")&.first. As noted, this may fail in localhost with a host missing error.

Here is my solution or an uploadable "logo" stored on S3 and made public by default:

#/models/company.rb
has_one_attached :logo
def public_logo_url
    if self.logo&.attachment
        if Rails.env.development?
            self.logo_url = Rails.application.routes.url_helpers.rails_blob_url(self.logo, only_path: true)
        else
            self.logo_url = self.logo&.service_url&.split("?")&.first
        end
    end
    #set a default lazily
    self.logo_url ||= ActionController::Base.helpers.asset_path("default_company_icon.png")
end

Enjoy ^_^

genkilabs
  • 2,966
  • 30
  • 36
2

I had a few problems getting this working. Thought I'd document them for posterity.

  • In rails 6.0 use @post.header_image.service_url
  • In rails >= 6.1 use @post.header_image.url as @GeorgeClaghorn recommends.

I got this error:

error: uninitialized constant Analyzable

It's a weird bug in rails 6.0, which is fixed by placing this in config/application.rb

config.autoloader = :classic

I then see this error:

URI::InvalidURIError (bad URI(is not URI?): nil) Active Storage service_url

Fix it by simply adding this to your application_controller.rb

include ActiveStorage::SetCurrent

Now something like @post.image.blob.service_url will work as you expect =)

stevec
  • 41,291
  • 27
  • 223
  • 311
1

Using the service_url method combined with striping the params to get a public URL was good idea, thanks @genkilabs and @Aivils_Štoss!

There is however a potential scaling issue involved if you are using this method on large number of files, eg. if you are showing a list of records that have files attached. For each call to service_url you will in your logs see something like:

DEBUG -- : [8df9220c-e8c9-45b7-a1ee-b746e623ca1b]   S3 Storage (1.4ms) Generated URL for file at key: ...

You can't eager load these calls either, so you can potentially have a large number of calls to S3 Storage to generate those URLs for each record you are showing.

I worked around it by creating a Presenter like this:

class FilePresenter < SimpleDelegator
  def initialize(obj)
    super
  end

  def public_url
    return dev_url if Rails.env.development? || Rails.env.test? || assest_host.nil?

    "#{assest_host}/#{key}"
  end

  private

  def dev_url
    Rails.application.routes.url_helpers.rails_blob_url(self, only_path: true)
  end

  def assest_host
    @assest_host ||= ENV['ASSET_HOST']
  end
end

Then I set an ENV variable ASSET_HOST with this:

https://<your_app_bucket>.s3.<your_region>.amazonaws.com

Then when I display the image or just the file link, I do this:

<%= link_to(image_tag(company.display_logo),
    FilePresenter.new(company.logo).public_url, target: "_blank", rel:"noopener") %>

<a href=<%= FilePresenter.new(my_record.file).public_url %> 
   target="_blank" rel="noopener"><%= my_record.file.filename %></a>

Note, you still need to use display_logo for images so that it will access the variant if you are using them.

Also, this is all based on setting my AWS bucket public as per @genkilabs step #2 above, and adding the upload: acl: "public-read" setting to my 'config/storage.yml' as per @Aivils_Štoss!'s suggestion.

If anyone sees any issues or pitfalls with this approach, please let me know! This seemed to work great for me in allowing me to display a public URL but not needing to hit the S3 Storage for each record to generate that URL.

Jeff Sholl
  • 63
  • 6
1

Also see public access in rails active storage. This was introduced in Rails 6.1.

Specify public: true in your app's config/storage.yml. Public services will always return a permanent URL.

Jeremy Lynch
  • 6,780
  • 3
  • 52
  • 63
1

Just write this if You are using minio or aws S3 to get attachment url on server.

@post.header_image&.service_url&.split("?")&.first
0

A bit late, but you can get the public URL also like this (assuming a Post model with a single attached header_image as in the example above):

@post.header_image.service.send(:object_for, @post.header_image.key).public_url

Update 2020-04-06

  1. You need to make sure, that the document is saved with public ACLs (e.g. setting the default to public)

  2. rails_blob_url is also usable. Requests will be served by rails, however, those requests will be probably quite slow, since a private URL needs to be generated on each request. (FYI: outside the controller you can generate that URL also like this: Rails.application.routes.url_helpers.rails_blob_url(@post, only_path: true))

Christian Butzke
  • 1,080
  • 12
  • 8
  • Hi @Christian, just tried it now, but does not seem to work(the code returns a shorter URL though): "This XML file does not appear to have any style information associated with it. The document tree is shown below." Do I have to set some configurations for the URL to work. – Ricardo Green Apr 01 '19 at 07:19
  • It's bad practice to use 'send' - the private method is a private method for a reason - so that the implementation can change "under the hood" and if the implementation changes your code won't break. This suggestion could well break when you upgrade to a new version of Rails or ActiveStorage. – oskarpearson May 19 '19 at 22:59
  • @RicardoGreen This error means probably that you did not grant public access rights to the document and AWS is sending an XML containing an error message back. This will fail with the error above because XML was not expected as a response – Christian Butzke Apr 06 '20 at 13:58
  • @oskarpearson Thank you very much for your commend. Indeed, "send" is a dirty way to call private methods, which can change at any time, and should be avoided whenever possible. However, ActiveSupport did not support providing public URLs, which was required in my case, in order to share the URLs with third parties. Thus, they needed to be valid for a long time and not just a few minutes, like the URLs provided by "service_url" – Christian Butzke Apr 06 '20 at 14:03
  • commend -> comment – Christian Butzke Apr 06 '20 at 14:21