Rails + ActiveStorage on S3: Set filename on download? - ruby-on-rails

Is there a way to change/set the filename on download?
Example: Jon Smith uploaded his headshot, and the filename is 4321431-small.jpg. On download, I'd like to rename the file to jon_smith__headshot.jpg.
View:
<%= url_for user.headshot_file %>
This url_for downloads the file from Amazon S3, but with the original filename.
What are my options here?

The built-in controller serves blobs with their stored filenames. You can implement a custom controller that serves them with a different filename:
class HeadshotsController < ApplicationController
before_action :set_user
def show
redirect_to #user.headshot.service_url(filename: filename)
end
private
def set_user
#user = User.find(params[:user_id])
end
def filename
ActiveStorage::Filename.new("#{user.name.parameterize(separator: "_")}__headshot#{user.headshot.filename.extension_with_delimiter}")
end
end
Starting with 5.2.0 RC2, you won’t need to pass an ActiveStorage::Filename; you can pass a String filename instead.

I know this was already answered, but I would like to add a second way of doing this. You could update your file name when the user object is saved. Using OP's example of the user model and the headshot_file field, this is how you could solve this:
# app/models/user.rb
after_save :set_filename
def set_filename
file.blob.update(filename: "ANYTHING_YOU_WANT.#{file.filename.extension}") if file.attached?
end

The approach of #GuilPejon will work. The problem with directly calling the service_url is:
It is short-lived (not recommended by rails team)
It will not work if the service is disk in development mode.
The reason it does not work for disk service is that disk service requires ActiveStorage::Current.host to be present for generating the URL. And ActiveStorage::Current.host gets set in app/controllers/active_storage/base_controller.rb, so it will be missing when service_url gets called.
ActiveStorage as of now gives one more way of accessing the URL of the attachments:
Using rails_blob_(url|path) (recommended way)
But if you use this, you can only provide content-disposition and not the filename.
If you see the config/routes.rb in the `ActiveStorage repo you will find the below code.
get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
direct :rails_blob do |blob, options|
route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
end
and when you look into blobs_controller you will find the below code:
def show
expires_in ActiveStorage::Blob.service.url_expires_in
redirect_to #blob.service_url(disposition: params[:disposition])
end
So it is clear that in rails_blob_(url|path) you can only pass disposition and nothing more.

I haven't played with ActiveStorage yet, so this is kind of a shot in the dark.
Looking at the ActiveStorage source for the S3 service, it looks like you can specify the filename and disposition for the upload. From the guides it seems that you can use rails_blob_path to access the raw URL of the upload and pass these parameters. Therefor you might try:
rails_blob_url(user.headshot_file, filename: "jon_smith__headshot.jpg")

Related

Rails 5.2: authorize access to ActiveStorage::BlobsController#show

I would like to authorize access to ActiveStorage attachments and looking at the source code of BlobsController (https://github.com/rails/rails/blob/master/activestorage/app/controllers/active_storage/blobs_controller.rb) is stated the following:
# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
# Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
# security-through-obscurity factor of the signed blob references, you'll need to implement your own
# authenticated redirection controller.
class ActiveStorage::BlobsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage.service_urls_expire_in
redirect_to #blob.service_url(disposition: params[:disposition])
end
end
But even the notes above suggest to create a custom controller I would need also to override the routes generated by ActiveStorage, since they are pointing to the original controllers, and redefining them on my routes.rb seems to throw an exception. Also I don't want to expose these routes anymore as they are not being authorized and someone could take the signed_id of the blob and get the attachment using the original endpoint.
Looping over the routes on the app initialization and deleting the old ActiveStorage routes and inserting the new ones seems the best solution for now, but I would like to avoid that.
Any suggestions? 🙄
Create a new controller to override the original: app/controllers/active_storage/blobs_controller.rb then add the authorization method accordingly with your needs:
#app/controllers/active_storage/blobs_controller.rb
class ActiveStorage::BlobsController < ActiveStorage::BaseController
include ActiveStorage::SetBlob
def show
redirect_to #blob.service_url(disposition: params[:disposition])
authorize! :show, #blob # NOT TESTED!
end
end
The show action is triggered when you click on a link to the attachment.
#blob.class #=> ActiveStorage::Blob

How can I get url of my attachment stored in active storage in my rails controller

How can I get url of my has_one model attachment stored in active storage in my rails controller. So, that I would be able to send it as full link as api in json.
So far, I have tried following methods but each of them are giving various issues:
current_user.image.service_url ---- undefined method `service_url' for #<ActiveStorage::Attached::One:0x....
Rails.application.routes.url_helpers.rails_disk_blob_path(current_user.image, only_path: true), it gives me an output like:
"/rails/blobs/%23%3CActiveStorage::Attached::One:0x007f991c7b41b8%3E"
but this is not a url, right? I am not able to hit and get image on browser.
url_for ----
undefined method `active_storage_attachment_url' for #<Api::V1::UsersController:0x007f991c1eaa98
Use the method rails_blob_path for attachements in a controller and models
For example, if you need to assign a variable (e.g. cover_url) in a controller, first you should include url_helpers and after use method rails_blob_path with some parameters. You can do the same in any model, worker etc.
Complete example below:
class ApplicationController < ActionController::Base
include Rails.application.routes.url_helpers
def index
#event = Event.first
cover_url = rails_blob_path(#event.cover, disposition: "attachment", only_path: true)
end
end
Sometimes, e.g. an API needs to return the full url with host / protocol for the clients (e.g. mobile phones etc.). In this case, passing the host parameter to all of the rails_blob_url calls is repetitive and not DRY. Even, you might need different settings in dev/test/prod to make it work.
If you are using ActionMailer and have already configuring that host/protocol in the environments/*.rb you can reuse the setting with rails_blob_url or rails_representation_url.
# in your config/environments/*.rb you might be already configuring ActionMailer
config.action_mailer.default_url_options = { host: 'www.my-site.com', protocol: 'https' }
I would recommend just calling the full Rails.application.url_helpers.rails_blob_url instead of dumping at least 50 methods into your model class (depending on your routes.rb), when you only need 2.
class MyModel < ApplicationModel
has_one_attached :logo
# linking to a variant full url
def logo_medium_variant_url
variant = logo.variant(resize: "1600x200>")
Rails.application.routes.url_helpers.rails_representation_url(
variant,
Rails.application.config.action_mailer.default_url_options
)
end
# linking to a original blob full url
def logo_blob_url
Rails.application.routes.url_helpers.rails_blob_url(
logo.blob,
Rails.application.config.action_mailer.default_url_options
)
end
end
I didn't have used rails active storage but what i have read in documentation this might help you
Try rails_blob_url(model.image)
For more http://edgeguides.rubyonrails.org/active_storage_overview.html
I was able to view the image in the browser using the following:
<%= link_to image_tag(upload.variant(resize: "100x100")), upload %>
Where upload is an attached image.

Fill up and update an uploaded PDF form online and save it back to the server - Ruby on Rails

Here is the requirement:
In my web-app developed in Ruby on Rails, we require to have an option to upload a PDF form template to the system, take it back in the browser itself and the user should be able to fill up the PDF form online and finally save it back to the server.
Then, user will come and download the updated PDF form from the application. I've searched a lot but couldn't find a proper solution to it. Please suggest.
As I stated for prebuilt PDF's with form fields already embedded I use pdtk Available Here and the active_pdftk gem Available Here. This is the standard process I use but yours may differ:
class Form
def populate(obj)
#Stream the PDF form into a TempFile in the tmp directory
template = stream
#turn the streamed file into a pdftk Form
#pdftk_path should be the path to the executable for pdftk
populated_form = ActivePdftk::Form.new(template,path: pdftk_path)
#This will generate the form_data Hash based on the fields in the form
#each form field is specified as a method with or without arguments
#fields with arguments are specified as method_name*args for splitting purposes
form_data = populated_form.fields.each_with_object({}) do |field,obj|
meth,args = field.name.split("*")
#set the Hash key to the value of the method with or without args
obj[field.name] = args ? obj.send(meth,args) : obj.send(meth)
end
fill(template,form_data)
end
private
def fdf(waiver_data,path)
#fdf ||= ActivePdftk::Fdf.new(waiver_data)
#fdf.save_to path
end
def fill(template,waiver_data)
rand_path = generate_tmp_file('.fdf')
initialize_pdftk.fill_form(template,
fdf(waiver_data,rand_path),
output:"#{rand_path.gsub(/fdf/,'pdf')}",
options:{flatten:true})
end
def initialize_pdftk
#pdftk ||= ActivePdftk::Wrapper.new(:path =>pdftk_path)
end
end
Basically what this does is it streams the form to a tempfile. Then it converts it to a ActivePdftk::Form. Then it reads all the fields and builds a Hash of field_name => value structure. From this it generates an fdf file and uses that to populate the actual PDF file and then outputs it to another tempfile flattened to remove the fields from the final result.
Your use case might differ but hopefully this example will be useful in helping you achieve your goal. I have not included every method used as I am assuming you know how to do things like read a file. Also my forms require a bit more dynamics like methods with arguments. Obviously if you are just filling in raw fixed data this portion could be changed a bit too.
An example of usage given your class is called Form and you have some other object to fill the form with.
class SomeController < ApplicationController
def download_form
#form = Form.find(params[:form_id])
#object = MyObject.find(params[:my_object_id])
send_file(#form.populate(#object), type: :pdf, layout:false, disposition: 'attachment')
end
end
This example will take #form and populate it from #object then present it to the end user as a filled and flattened PDF. If you just needed to save it back into the database I am sure you can figure this out using an uploader of some kind.

How to save a raw_data photo using paperclip

I'm using jpegcam to allow a user to take a webcam photo to set as their profile photo. This library ends up posting the raw data to the sever which I get in my rails controller like so:
def ajax_photo_upload
# Rails.logger.info request.raw_post
#user = User.find(current_user.id)
#user.picture = File.new(request.raw_post)
This does not work and paperclip/rails fails when you try to save request.raw_post.
Errno::ENOENT (No such file or directory - ????JFIF???
I've seen solutions that make a temporary file but I'd be curious to know if there is a way to get Paperclip to automatically save the request.raw_post w/o having to make a tempfile. Any elegant ideas or solutions out there?
UGLY SOLUTION (Requires a temp file)
class ApiV1::UsersController < ApiV1::APIController
def create
File.open(upload_path, 'w:ASCII-8BIT') do |f|
f.write request.raw_post
end
current_user.photo = File.open(upload_path)
end
private
def upload_path # is used in upload and create
file_name = 'temp.jpg'
File.join(::Rails.root.to_s, 'public', 'temp', file_name)
end
end
This is ugly as it requires a temporary file to be saved on the server. Tips on how to make this happen w/o the temporary file needing to be saved? Can StringIO be used?
The problem with my previous solution was that the temp file was already closed and therefore could not be used by Paperclip anymore. The solution below works for me. It's IMO the cleanest way and (as per documentation) ensures your tempfiles are deleted after use.
Add the following method to your User model:
def set_picture(data)
temp_file = Tempfile.new(['temp', '.jpg'], :encoding => 'ascii-8bit')
begin
temp_file.write(data)
self.picture = temp_file # assumes has_attached_file :picture
ensure
temp_file.close
temp_file.unlink
end
end
Controller:
current_user.set_picture(request.raw_post)
current_user.save
Don't forget to add require 'tempfile' at the top of your User model file.

ActiveRecord Custom Field Method

Lets imagine that I have a custom image upload for a particular record and I add two columns into the model.
thumbnail_url
thumbnail_path
Now lets imagine that I have a form with a :file field which is the uploaded file in multipart form. I would need to somehow have the model pickup the file within the given hash and then issue it over to a custom method which performs the upload and saves it as apart of the model.
Right now I'm doing this:
def initialize(options = nil)
if options
if options[:file]
self.upload_thumbnail(options[:file])
options.delete(:file)
end
super options
else
super
end
end
def update_attributes(options = nil)
if options
if options[:file]
self.upload_thumbnail(options[:file])
options.delete(:file)
end
super options
else
super
end
end
It works, but I am doing some unnecessary overriding here. Is there a simpler way of doing this? Something that would only require overriding perhaps one method?
You are looking for virtual attributes. Just define:
def file
# Read from file or whatever
end
def file=(value)
# Upload thumbnail and store file
end
and initialize, update_attributes and cousins will pick the methods for you.
That, or save the hassle and use paperclip as Kandada suggested.
Have you considered using using paperclip gem? It performs the functions you describe in your question.

Resources