Default Value for ActiveStorage Rails 5.2.0 - ruby-on-rails

I am using Rails ActiveStorage. I want that whenever the value of attachment (in my case image is null then replace it with "abc.png" which is present in assets folder..)
This is what my model.rb file looks like but this code does not seem to work. I am looking for how to set default / nil value for avatar.
has_one_attached :avatar #bot icon
after_create_commit check_avatar(self)
def check_avatar(self)
if(!self.avatar.present?)
{
self.avatar = "abc.png"
}
end

Active storage doesn't provide default option like paperclip does, however, you can do write your method to attach default file in case image is nil. you can use attach method to do so.
def image_nil
if !self.image?
user.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'image/jpeg')
end
end
you can omit content_type but it is good to provide it, the content type you provide will serve as a fallback in case analyzer can't do it.
Hope this helps!

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?

How to save an image from a url with rails active storage?

I'm looking to save a file (in this case an image) located on another http web server using rails 5.2 active storage.
I have an object with a string parameter for source url. Then on a before_save I want to grab the remote image and save it.
Example: URL of an image http://www.example.com/image.jpg.
require 'open-uri'
class User < ApplicationRecord
has_one_attached :avatar
before_save :grab_image
def grab_image
#this indicates what I want to do but doesn't work
downloaded_image = open("http://www.example.com/image.jpg")
self.avatar.attach(downloaded_image)
end
end
Thanks in advance for any suggestions.
Just found the answer to my own question. My first instinct was pretty close...
require 'open-uri'
class User < ApplicationRecord
has_one_attached :avatar
before_save :grab_image
def grab_image
downloaded_image = open("http://www.example.com/image.jpg")
self.avatar.attach(io: downloaded_image , filename: "foo.jpg")
end
end
Update: please note comment below, "you have to be careful not to pass user input to open, it can execute arbitrary code, e.g. open("|date")"
Like said in the comments, the use of open or URI.open is very dangerous, since it can not only access files but also process invocation by prefixing a pipe symbol (e.g. open("| ls")).
Kernel#open and URI.open enable not only file access but also process invocation by prefixing a pipe symbol (e.g., open("| ls")). So, it may lead to a serious security risk by using variable input to the argument of Kernel#open and URI.open. It would be better to use File.open, IO.popen or URI.parse#open explicitly.
Extracted from the Rubocop documentation: https://docs.rubocop.org/rubocop/1.8/cops_security.html#securityopen
So, a safer solution would be:
class User < ApplicationRecord
has_one_attached :avatar
before_save :grab_image
def grab_image
downloaded_image = URI.parse("http://www.example.com/image.jpg").open
avatar.attach(io: downloaded_image, filename: "foo.jpg")
end
end
using the down gem to avoid the security issues of using open-uri:
image = Down.download(image_url)
user.image.attach(io: image, filename: "image.jpg")
The simplest way to do this without having to enter filename explicitly is:
url = URI.parse("https://your-url.com/abc.mp3")
filename = File.basename(url.path)
file = URI.open(url)
user = User.first
user.avatar.attach(io: file, filename: filename)
This automatically saves the avatar against that particular user object.
In case you are using a remote service like S3 the URL can be retrieved by:
user.avatar.service_url

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

Paperclip dynamic url?

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

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