I am aware of the ActiveStorage url_for(user.avatar)
method. Example:
<%= link_to "Download this file", url_for(@user.avatar) %>
This is great, but it appears like it has no authorization built around it. Anyone with this link can download this document.
In the past when I used paperclip, I had a link that went to a custom controller action. That custom controller action did authorization, and if all is good, then I used send_file
to send the file to the user. It was something like this:
def deliver_the_file
authorize :my_authorization
send_file @user.avatar.path
end
How can I do this with active_storage? All I have gotten to work is the url_for
implementation which does not authorize the user at all.
I am looking specifically at the Download Files part of the Rails Guides on ActiveStorage.
Relevant example code:
# model
class Book < ApplicationRecord
has_one_attached :book_cover
end
class BooksController < ApplicationController
...
# custom action to authorize and then deliver the active_storage file
def deliver_it
# ... assume authorization already happened
send_file rails_blob_path(@book.book_cover, disposition: "attachment")
end
end
This errors out and says:
Cannot read file
I also attempted this:
def deliver_it
# ... assume authorization already happened
send_file @book.book_cover.download
end
This returned the error:
string contains null byte
And I also attempted this:
def deliver_it
@book.book_cover.download
end
This returned the error:
BooksController#deliver_it is missing a template for this request
I have also tried this:
def deliver_it
send_file @book.book_cover.blob
end
This errors out and says:
no implicit conversion of ActiveStorage::Blob into String
Your best bet is to use ActiveStorage::Blob#service_url
to redirect to a signed, short-lived URL for the blob:
class Books::CoversController < ApplicationController
before_action :set_active_storage_host, :set_book
def show
redirect_to @book.cover.service_url
end
private
def set_active_storage_host
ActiveStorage::Current.host = request.base_url
end
def set_book
@book = Current.person.books.find(params[:book_id])
end
end
Alternatively, serve the file yourself. You can use ActionController::Live
to stream the file to the client instead of reading the entire file into memory at once.
class Books::CoversController < ApplicationController
include ActionController::Live
before_action :set_book
def show
response.headers["Content-Type"] = @book.cover.content_type
response.headers["Content-Disposition"] = "attachment; #{@book.cover.filename.parameters}"
@book.cover.download do |chunk|
response.stream.write(chunk)
end
ensure
response.stream.close
end
# ...
end
I derived this solution from a comment on this ActiveStorage issue
def deliver_it
send_data @book.book_cover.download, filename: @book.book_cover.filename.to_s, content_type: @book.book_cover.content_type
end
I used redirect_to
here and it worked:
redirect_to rails_blob_path(@book.book_cover, disposition: "attachment")
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With