Paperclip dynamic url? - ruby-on-rails

I have a Rails ActiveModel Product with a Paperclip image attachment column that needs to get it's image.url from 2 sources. One is an old S3 bucket/CloudFront, other is our new S3 bucket/CloudFront. They have completely different credentials.
If the instance Product.image_file_name is containing "old:" I want the URL to be something like cloudfront_url/products/file_name, if it doesn't - it should use the new S3 bucket/CloudFront. Upload is going to happen only on the new S3 bucket, but it'll fallback on the old one if image_file_name is containing old: as I mentioned.
Currently I'm authorized only with the new S3 bucket, not with the old one.
I have read that I should do something like:
class Product
has_attached_file: :image, url: dynamic_url_method
def dynamic_url_method
.... do some logic based on image_file_name
return constructed_url
end
end
However when I do that, I get undefined local variable dynamic_url_method.
If I wrap it in a lambda as said in https://stackoverflow.com/a/10493048 I get Error "no implicit conversion of Proc into String".
Have you guys successfully gotten Paperclip to work with a dynamic URL? It would be a life-saver if you know how to do so.

Completely scrap the whole idea of dynamic URL parameter given to the Paperclip attachment. It breaks S3 image uploading cause Paperclip can't figure out which URL to use.
The solution is to introduce a new column in your schema called image_url.
The column will be updated on initialize/update in the ActiveModel and used in the web pages.
In code
class Product
has_attached_file: :image
after_create :update_image_url
after_update :update_image_url
def update_image_url
new_image_url = # some logic based on image_file_name that would either return the CloudFront URL or save the URL from image.url which is generated by Paperclip
# Paperclip does not update image_file_name_changed? so we can't say
# after_create or after_update if: image_file_name_changed? instead
# we have to manually check that image_url and new_image_url are different
update(image_url: new_image_url) if image_url != new_image_url
end
end

Related

How to test if a new file is sent with Active Storage in the model?

In my Rails model, I have this code to force the filename to be changed when uploading:
before_save :set_filename
def set_filename
if file.attached?
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
The problem is the filename is changed even if a new file is not sent in the form (when editing).
My attachement is simply named file:
# active storage
has_one_attached :file
How to really test that a new file is attached when uploading ?
Thanks,
EDIT: more clarifications
I have a form with a file_field.
I want to test if a new file is sent via the form, when I add or modify the object of the form.
My model is called Image and the attached file is called file.
class Image
has_one_attached :file
end
I want to change the filename every time a new file is sent via the form, and not of course is the file_field stays empty.
You can use new_record? to check if file is new ie:
def set_filename
if file.attached? && file.new_record?
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
Alternatively, use before_create instead of before_save so that set_name only runs when uploading new file.
Updated
Interestingly, ActiveStorage handles blob change outside model hooks. Apparently, it doesn't even support validation right now. There's no way to verify blob has changed as its state is not persisted anywhere. If you peek into rails log, notice rails purge old blob as soon as a new one is added.
Few options I can think of:
1.Update filename in controller eg:
original_name = params[:file].original_name
params[:file].original_name = # your logic goes here
2.Store blob file name in parent model and compare in before_save.
def set_filename
if file.attached? && file.blob.filename != self.old_filename
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
None of these solutions are ideal but hope they give you some ideas.
I have just solved this problem (or something very similar) using the blob record associated with the attachment.
Since the blob is an ActiveStorage::Blob, which is derived from ActiveRecord::Base, you can use the "dirty" methods on it to see if it has been changed. e.g.
def set_filename
if file.attached? && (file_blob.has_changes_to_save? || file_blob.saved_changes?)
self.file.blob.update(filename: "#{new_file_name()}.#{self.file.blob.content_type.split('/')[1]}")
end
end
Depending on where in the lifecycle set_filename is called from, there may only be a need to check one of has_changes_to_save? or saved_changes?. Since, in your example, you're calling this in before_save, you would only need file_blob.has_changes_to_save?

ActiveStorage 5.2.1 - uploaded asset is nil since upload has not finished. How to wait for finished upload?

I use ActiveStorage for user generated stylesheets which will be uploaded to s3 in order to include them in a custom user styled web page.
So I have a model CustomeTheme
has_one_attached :style, dependent: :purge_later
and an after_save callback which does the upload after the custom style has been saved
self.style.attach(io: File.open(File.join(asset_path, name)), filename: name, content_type: 'text/css')
Included in a layout
= stylesheet_link_tag url_for(#custom_theme.style)
The problem now is, that the user saves the style and and sees a preview of the custom web page but without the custom style (404 at this point of time) since the uploaded to s3 has not finished yet, at least thats what I suppose.
to_model delegated to attachment, but attachment is nil
/usr/local/bundle/gems/activesupport-5.2.1/lib/active_support/core_ext/module/delegation.rb:278:in `rescue in method_missing'
/usr/local/bundle/gems/activesupport-5.2.1/lib/active_support/core_ext/module/delegation.rb:274:in `method_missing'
/usr/local/bundle/gems/actionpack-5.2.1/lib/action_dispatch/routing/polymorphic_routes.rb:265:in `handle_model'
/usr/local/bundle/gems/actionpack-5.2.1/lib/action_dispatch/routing/polymorphic_routes.rb:280:in `handle_model_call'
/usr/local/bundle/gems/actionview-5.2.1/lib/action_view/routing_url_for.rb:117:in `url_for'
So the question remains unclear to me how could i know that the asset (no matter whether it is a style or an image) is ready to be displayed?
2 possible approaches:
Define a route for upload status checks and then run an interval in Javascript to check for upload status for a given upload id. When it finishes, the endpoint returns the asset URL, which then you can use. (e.g. If the asset is an image, then you just put that on an <img> tag src attribute).
Another approach would be something like what Delayed Paperclip does:
In the default setup, when you upload an image for the first time and try to display it before the job has been completed, Paperclip will be none the wiser and output the url of the image which is yet to be processed, which will result in a broken image link being displayed on the page.
To have the missing image url be outputted by paperclip while the image is being processed, all you need to do is add a #{attachment_name}_processing column to the specific model you want to enable this feature for.
class AddAvatarProcessingToUser < ActiveRecord::Migration
def self.up
add_column :users, :avatar_processing, :boolean
end
def self.down
remove_column :users, :avatar_processing
end
end
#user = User.new(avatar: File.new(...))
#user.save
#user.avatar.url #=> "/images/original/missing.png"
# Process job
#user.reload
#user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148"

How can I customize the path of a Rails 5.2 ActiveStorage attachment in Amazon S3?

When adding attachments such as
has_one_attached :resume_attachment
saved files end up in the top level of the S3 bucket. How can I add them to subdirectories? For example, my old paperclip configuration could categorize in directories by model name.
You can not. There is only one option possible, at that time, for has_one_attached, has_many_attached macros that is :dependent.
https://github.com/rails/rails/blob/master/activestorage/lib/active_storage/attached/macros.rb#L30
see (maybe the reason why you have downvotes, but it is about "direct" upload so...) : How to specify a prefix when uploading to S3 using activestorage's direct upload?.
The response is from the main maintainer of Active Storage.
Use a before_validation hook to set the desired key on S3 and the desired filename for the content disposition object properties on S3.
The key and filename properties on the attachment model make their way through to the ActiveStorage S3 gem and are converted into S3 key + content disposition object properties.
class MyCoolItem < ApplicationRecord
has_one_attached :preview_image
has_one_attached :download_asset
before_validation :set_correct_attachment_filenames
def preview_image_path
# This key has to be unique across all assets. Fingerprint it yourself.
"/previews/#{item_id}/your/unique/path/on/s3.jpg"
end
def download_asset_path
# This key has to be unique across all assets. Fingerprint it yourself.
"/downloads/#{item_id}/your/unique/path/on/s3.jpg"
end
def download_asset_filename
"my-friendly-filename-#{item_id}.jpg"
end
def set_correct_attachment_filenames
# Set the location on S3 for new uploads:
preview_image.key = preview_image_path if preview_image.new_record?
download_asset.key = download_asset_path if download_asset.new_record?
# Set the content disposition header of the object on S3:
download_asset.filename = download_asset_filename if download_asset.new_record?
end
end

Rails + ActiveStorage on S3: Set filename on download?

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")

How can I reference images in the asset pipeline from a model?

I have a model with a method to return a url to a person's avatar that looks like this:
def avatar_url
if self.avatar?
self.avatar.url # This uses paperclip
else
"/images/avatars/none.png"
end
end
I'm in the midst of upgrading to 3.1, so now the hard-coded none image needs be referenced through the asset pipeline. In a controller or view, I would just wrap it in image_path(), but I don't have that option in the model. How can I generate the correct url to the image?
I struggled with getting this right for a while so I thought I'd post the answer here. Whilst the above works for a standard default image (i.e. same one for each paperclip style), if you need multiple default styles you need a different approach.
If you want to have the default url play nice with the asset pipeline and asset sync and want different default images per style then you need to generate the asset path without fingerprints otherwise you'll get lots of AssetNotPrecompiled errors.
Like so:
:default_url => ActionController::Base.helpers.asset_path("/missing/:style.png", :digest => false)
or in your paperclip options:
:default_url => lambda { |a| "#{a.instance.create_default_url}" }
and then an instance method in the model that has the paperclip attachment:
def create_default_url
ActionController::Base.helpers.asset_path("/missing/:style.png", :digest => false)
end
In this case you can still use the interpolation (:style) but will have to turn off the asset fingerprinting/digest.
This all seems to work fine as long as you are syncing assets without the digest as well as those with the digest.
Personally, I don't think you should really be putting this default in a model, since it's a view detail. In your (haml) view:
= image_tag(#image.avatar_url || 'none.png')
Or, create your own helper and use it like so:
= avatar_or_default(#image)
When things like this are hard in rails, it's often a sign that it's not exactly right.
We solved this problem using draper: https://github.com/jcasimir/draper. Draper let us add a wrapper around our models (for use in views) that have access to helpers.
Paperclip has an option to specify default url
has_attached_file :avatar, :default_url => '/images/.../missing_:style.png'
You can use this to serve default image' in case user has not uploaded avatar.
Using rails active storage I solved this problem by doing this:
# Post.rb
def Post < ApplicationRecord
has_one_attached :image
def thumbnail
self.image.attached? ? self.image.variant(resize: "150x150").processed.service_url : 'placeholder.png';
end
def medium
self.image.attached? ? self.image.variant(resize: "300x300").processed.service_url : 'placeholder.png';
end
def large
self.image.attached? ? self.image.variant(resize: "600x600").processed.service_url : 'placeholder.png';
end
end
Then in your views simply call:
<%= image_tag #post.thumbnail %>,

Resources