ruby-on-railsrails-activestorage

How to get attachment URL without having public URLs in ActiveStorage


I have ActiveStorage installed and set up. There is a model invoice which can have a PDF file attached:

class Invoice < ApplicationRecord
  has_one_attached :generated_pdf

Attaching PDFs works fine and I can download them with their public URL. Now because the data is invoice data I would like to turn off public URLs and serve the file via redirect from inside a controller. This way I can add authorization for requested files.

I have turned off public URLs following the guide's section 5.3.

config.active_storage.draw_routes = false

For the sake of getting this to work, for now I try to redirect to the attachment in the invoice controller show method.

class InvoicesController < ApplicationController
  def show
    redirect_to @invoice.generated_pdf.url
  end
end

However, I get the error message Cannot generate URL for file.pdf using Disk service, please set ActiveStorage::Current.url_options.

A bit of research and it seems adding the following to the controller could fix this:

class InvoicesController < ApplicationController
  include ActiveStorage::SetCurrent

Now I get a different error message undefined method rails_disk_service_url for module #<Module:0x0000000124de6550>

I have read and re-read section 5.3 but I can't wrap my head around what I have to do here.


Solution

  • When you try to call .url on an ActiveStorage attachment with Disk service, it raises because Disk storage doesn't support public URLs — it's meant to serve via Rails routes

    In any case if you decide to hide public attachment URL, it's better to stream attachment instead

    To make it reusable, define method in the parent controller (i.e. ApplicationController in your case) or using concerns

    include ActionController::Live
    
    def stream_active_storage_attachment(attachment)
      send_stream(filename: attachment.filename.to_s, type: attachment.content_type) do |stream|
        attachment.download { |chunk| stream.write(chunk) }
      end
    end
    

    and call it inside your controller after authorization

    def show
      # some authorization to prevent download by users without priveliges
    
      stream_active_storage_attachment(@invoice.generated_pdf)
    end
    

    In this case you will not show any URLs outside the backend

    Another option is to stream using web server (e.g. nginx). In this case you will need to to use X-Accel-Redirect header. It's better for very huge files