Displaying paperclip attachments stored in a non-public folder - ruby-on-rails

My conundrum is how to embed in an html page an image whose source is not available to the Internet at large.
Let's say I have, in a Rails/Paperclip setup, the following model:
class Figure < ActiveRecord::Base
has_attached_file :image
...
end
class User < ActiveRecord::Base
... (authentication code here)
has_many :figures
end
In the controller:
class FiguresController < ActionController::Base
def show
# users must be authenticated, and they can only access their own figures
#figure = current_user.figures.find(params[:id])
end
end
In the view:
<%= image_tag(#figure.image.url) %>
The problem with this, of course, is that with the default Paperclip settings images are stored in the public directory, and anyone with the link can access the stored image bypassing authentication/authorization.
Now, if we tell Paperclip to store attachments at a private locations:
class Figure < ActiveRecord::Base
has_attached_file :image, path: ":rails_root/private/:class/:attachment/:id_partition/:style/:filename",
url: ":rails_root/private/:class/:attachment/:id_partition/:style/:filename"
...
end
Then it's easy to control who the image gets served to:
class FiguresController < ActionController::Base
def show
#figure = current_user.figures.find(params[:id])
send_file #figure.image.path, type: 'image/jpeg', disposition: 'inline'
end
end
The effect of this action is to display the image in its own browser window/tab.
On the other hand, image_tag(#figure.image.url) will understandably produce a routing error, because the source cannot be accessed!
Thus, is there a way to display the image via image_tag in a regular HTML page, while still restricting access to it?

You need to change the :url option passed to has_attached_file so that it matches the route for your figures controller.
For example, if the correct url is /figures/123 for the figure with is 123 then the url you pass to has_attached_file should be
'/figures/:id'
Or even
'/:class/:id'
Since the :class segment will be interpolated to the pluralized lowercase underscore form of the name. You could also append the extension or the filename if you wanted (but you would then have to change the controller code slightly to extract the id)

Related

Rails Active Storage: Get relative disk service path for attachment

I'm switching to Rails Active Storage to handle upload and storage of images locally (using the disk service) for a product catalog, and I'm having trouble getting a usable url to the image to feed into an <img> tag. I'm using React on the frontend, so I can't (easily) use Rails helpers to generate the tag.
ActiveStorage puts the files in /public/images. I can hardcode relative links to the files (i.e. http://localhost:3000/images/Ab/CD/AbCDEfGhIjkL) and it works fine.
Relevant snippet from Product.rb:
class Product < ApplicationRecord
attr_accessor :image_url
has_one_attached :image
def as_json(options)
h = super(options)
if self.image.attached?
h[:image_url] = ActiveStorage::Blob.service.service_url(self.image.key)
end
h
end
end
as_json produces a JSON object to feed to React that has an entry image_url that is used for the <img>'s src attribute. With the code above, image_url contains the full path to the file (i.e. http://localhost:3000/srv/www/rails_app/public/images/Ab/CD/AbCDEfGhIjkL). Using url_for in the view produces the same result. I want it to only contain the path relative to rails root.
I could manipulate the string to remove everything before the relative path, but I foresee this causing bugs in the future if anything ever changes, so I'd much rather find a way to get ActiveStorage to just generate an appropriate string for me.
Thanks!
You need to use the routes helper to build of a URL to your Rails app.
https://guides.rubyonrails.org/active_storage_overview.html#linking-to-files
class Product < ApplicationRecord
attr_accessor :image_url
has_one_attached :image
def as_json(options)
h = super(options)
if self.image.attached?
h[:image_url] = Rails.application.routes.url_helpers.rails_blob_path(self.image)
end
h
end
end

creating an alias_attribute for a paperclip image in Rails

I'm currently building an app which has a model Post. I'm using the paperclip gem to upload images, and everything is going well.
class Post < ActiveRecord::Base
has_attached_file :headerimage, styles: {banner => "400x300#"}
end
As you can see by my class above, if I were to get a Post object, I could get the banner image with the following in my view:
image = Post.first.headerimage(:banner)
Alias
However, in my app, it must have the image attribute image refer to the thumbnail image. So, in my models class, I wrote
class Post < ActiveRecord::Base
has_attached_file :headerimage, styles: {banner => "400x300#"}
alias_attribute :image, :headerimage
end
which allows me to get an image by calling the following:
image = Post.first.image
This is what I want - however, it gets the original image from paperclip, so it is equivalent to writing the following:
image = Post.first.headerimage instead of image = Post.first.headerimage(:banner)
How can I set up a proper alias_attribute to access the paperclip thumnail? I can't seem to find an answer anywhere else, and I am unsure how paperclip is actually working.
I thought I might logically be able to do something like
alias_attribute :image, :headerimage(:banner)
but that does not work.
You can just try this - basically call the original with arguments since aliases won't take parameter but we know the fixed parameter to pass:
alias :image, :headerimage
def headerimage(name=nil)
headerimage(name || :thumb)
end

Scraping images from user input URL using MetaInspector (Rails)

I'm trying to create an app where a user can submit a URL link, a title and description, and it'll create a post with the title, description and an image. I want to be able to scrape the best or main image from directly from the URL path that the user submitted and display it on the show page using MetaInspector. (The reason I didn't use Nokogiri or Mechanize is because I didn't understand it all that well and MetaInspector seems alot less daunting)
The problem is I'm very new to rails and I'm having a hard time following most tutorials.
Is anyone able to explain to me step by step how to do this or show me a source that's very detailed and noob friendly?
I have a Post model that contains the link, and should also save the scraped image as a Paperclip attachment:
class Post < ActiveRecord::Base
belongs_to :user
has_attached_file :image
end
# == Schema Information
#
# Table name: posts
#
# id :integer not null, primary key
# title :string
# link :string
# description :text
# created_at :datetime
# updated_at :datetime
# user_id :integer
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
The full code of my app is available at github.com/johnnyji/wanderful.
I really appreciate any help at all! Thank you
Let's walk through this step by step.
First, add the MetaInspector gem to your Gemfile
gem 'metainspector'
and run the bundle command.
We need another bit of code: open-uri. With it, we can read remote files from URLs as if they were local files. It is part of Rubys standard library, so it's already built in, but we still need to require it at the top of your post.rb:
require 'open-uri'
class Post < ActiveRecord::Base
belongs_to :user
has_attached_file :image
end
We want to grab an image whenever a Posts link changes, so we make a before_save callback that triggers whenever that happens:
class Post < ActiveRecord::Base
belongs_to :user
has_attached_file :image
before_save :get_image_from_link,
if: ->(post) { post.link_changed? }
end
you can find more about before_save and other callbacks in the ActiveRecord::Callbacks guide.
the link_changed? method is part of the "dirty tracking" functionality ActiveModel::Dirty provides
that if: ->(post) thing is called a "stabby lambda" - it's basically just a Ruby function that is called with the current post as an argument. If it returns true, the before_action is run. It could also be written as if: Proc.new { |post| post.link_changed? }
Now we need our get_image_from_link method. Since it's only supposed to be called from within the Post model itself and not from the outside (say, Post.find(5).get_image_from_link), we make it a private method:
class Post < ActiveRecord::Base
belongs_to :user
has_attached_file :image
before_save :get_image_from_link,
if: ->(post) { post.link_changed? }
private
def get_image_from_link
end
end
Reading MetaInspectors README, it has a cool method called page.images.best that does the hard work for us selecting the right image from that page. So we are going to
parse the link with MetaInspector
open the image it selected as best with open-uri as a File-like object
give that File-like object to Paperclip to save as an attachment
So:
def get_image_from_link
# `link` here is `self.link` = the current post.
# At least when reading attributes, `self` is implicit
# in Ruby
page = MetaInspector.new(link)
# maybe the page didn't have images?
return unless page.images.best.present?
# when you use IO resources such as files, you need
# to take care that you `.close` everything you open.
# Using the block form takes care of that automatically.
open(page.images.best) do |file|
# when writing/assigning a value, `self` is not
# implicit, because when you write `something = 5`,
# Ruby cannot know whether you want to assign to
# `self.something` or create a new local variable
# called `something`
self.image = file
end
end
This is far from perfect, because it lacks some error handling (what if MetaInspector fails to open the page? Or open-uri cannot read the image URL?). Also, this has the drawback that all that parsing, downloading and so on takes place right when the user submits or updates her post, so when she clicks on the save button, she'll have to wait for all this to complete.
For the next iteration, look into doing things like these asynchronously, for example with a job queue. Rails' new Active Job system might be a good starting point.

How can I access current host and port on a model in Rails?

I am facing the following problem:
I am doing a Rails 4 webapp and we are using paperclip for profile images. If the user does not upload an image we provide a default one (like the facebook silhouette placeholder). So as paperclip eases handling default images, we are doing the following in the Profile model:
class Profile < ActiveRecord::Base
belongs_to :user
has_attached_file :image, :styles => { :medium => "300x300", :thumb => "100x100" }, :default_url => "assets/profiles/:style/placeholder.gif"
end
The big problem is that I need the complete URL of the image and NOT only the path so I am struggling to get the host and port before that path. Using action view helpers there did not help (asset_url helper)
I was thinking in initializing some constant or configuration or environment variable per environment. Will it be correct? Any other suggestions?
EDIT: I forgot to mention this: The resource (Profile) may have a custom picture or a default one. When it has a custom image, we store it in Amazon S3 and in that case profile.image.url returns full URL. In the other case, when it has not a custom picture it has a default image stored in app/assets/images and in that case profile.image.url returns just the path. I would like that the method image.url consistently return full URLs. – flyer88 just now edit
If, as you mention in your comment, you are providing an API endpoint, it might make more sense to determine the host, port, etc. in the controller. Something like this:
# routes.rb
get "/profile/:id" => "api#profile"
# profile.rb
def image_url_or_default request
if image
"#{request.protocol}#{request.host_with_port}#{image.url}"
else
"http://s3.amazon.com/my_bucket/default.jpg"
end
end
# api_controller.rb
def profile
profile = Profile.find params[:id]
render text:profile.image_url_or_default(request)
end
profile.image.url will be the full URL of the image.

Paperclip image name changed after updating any attribute

This is my model
class Technology < ActiveRecord::Base
attr_accessible :name #etc ....
has_attached_file :logo, :path => ":rails_root/public/technologies/logos/:normalized_input_file_name"
Paperclip.interpolates :normalized_input_file_name do |attachment, style|
attachment.instance.normalized_input_file_name
end
def normalized_input_file_name
name = "#{self.name}".downcase
"#{self.tuid}_"+name.gsub(/[^a-zA-Z0-9]{2,}/,' ').strip.gsub(/\W/,'_').gsub(/\A_/,'').gsub(/_\z/,'')+"_150x"+".png"
end
end
When I create any technology, I upload a logo for it and the image stored in public directory with the new name as I want using the method "normalized_input_file_name".
e.g.technology name is HTML5 and file name becomes id_html5_150x.png
But when I need to update name the image path also changed.
e.g. HTML 5 file name becomes id_html_5_150x.png Here actual image file name is not updated
But path is updated. So I can't find the image.
Use a before_save hook to download and store the image again if you recognize that the name attribute going to change.
This code is totally untested but should give you an idea:
before_save do
if self.name_changed?
self.logo = logo.uploaded_file
end
end

Resources