I'm developing a api applicacion with Rails 5. From client side I have an Anuglar app.
I have the following requirements to do:
Async upload of images. (Note that implies that the images have no attached to a model at the first time).
The model to be create can have N images attached.
For each uploaded image, create M specified thumbnails. (Note that also that thumbnails should be attached to the model.)
I have read about CarrierWave and paperclip but I don't found how can I do all of these requirements.
I will really appreciate any suggestion, lib, gem, etc that covers all of these requirements.
I have implemented something similar with CarrierWave gem. Assuming you want images attached to a model Post.
in the model:
class Post < ActiveRecord::Base
has_many :photos, inverse_of: :post
end
you can then have a photo model:
class Photo < ActiveRecord::Base
belongs_to :post
mount_uploader :post_image, PostImageUploader
end
Then you can add this to app/uploaders/post_image_uploader.rb
class PostImageUploader < CarrierWave::Uploader::Base
# Create different versions of your uploaded files:
version :standard do
process resize_to_fit: [800, 800]
end
version :thumb do
process resize_to_fit: [100, 100]
end
end
since you are creating the images via an api, you'll have to convert the image to Base64 string and send as a param. on this example the base64 string is in param [:photo][:photo_data] before sending it to the server. in photos_controller.rb have this:
def create
#photo = Photo.new(photo_params)
#photo.post_image = decode_photo_data(params[:photo][:photo_data])
if #photo.save
render json: #photo, status: :created, location: #photo
else
render json: #photo.errors, status: :unprocessable_entity
end
end
#decode base64 data to an jpg image:
def decode_photo_data(photo_data)
data = StringIO.new(Base64.decode64(photo_data))
data.class.class_eval { attr_accessor :original_filename, :content_type }
data.original_filename = "upload.jpg"
data.content_type = "image/jpg"
# return decoded data
data
end
def photo_params
params.require(:photo).permit(:caption, :post_id)
end
With this, when you make a post request to your create photo api endpoint with a body like this
{"photo": {"caption": "an image", "post_id": 1, "photo_data":<your image base64 string>}}
it will create a photo with both a standard and a thumb version for the post with an id of 1.
Related
I have created an app using a Rails API with a React front-end and ActiveAdmin as the CMS back-end, which is based off of this tutorial. I am adding on a model which includes an image using ActiveStorage for file uploads, as well as S3 for storage on production.
app/models/photo.rb
class Photo < ApplicationRecord
has_one_attached :image
end
app/controllers/photos_controller.rb
class PhotosController < ApplicationController
include ActionController::Serialization
before_action :set_photo, only: [:show, :update, :destroy]
# GET /photos
def index
#photos = Photo.all
render json: #photos.to_json
end
...
def photo_params
params.require(:photo).permit(:image, :location_name, :region, :url)
end
end
app/serializers/photo_seralizer.rb
class PhotoSerializer < ActiveModel::Serializer
attributes :id, :image, :location_name, :region, :url
def image
rails_blob_path(object.image, only_path: true) if object.image.attached?
end
end
When I view the API endpoint, the attached image is not showing in the photo object, here is an example return. Is there something I'm missing in the Serializer, that isn't adding in the related image? Any help would be greatly appreciated, thanks!
[{
"id":1,
"location_name":"Acreage Ciderhouse",
"region":"Northern Metro",
"url":"https://example.com/acreage-ciderhouse/",
"created_at":"2020-01-09T15:18:33.298-07:00",
"updated_at":"2020-01-09T15:27:40.594-07:00"
}]
The activestorage saves data different, I think your index is calling data from your model table, but images are saved in relationship to other tables. So following is way to check if image is really saved use rails console to check.
rails c
#photo = Photo.find(1)
puts #photo.image.attached?
If you get true that image is saving properly now you can do following to add all image urls in your request
def index
#photos = Photo.all
#photos.each do |p|
if #photo.present?
image_url = { :image => (url_for p.image) }
p.attributes.merge(image_url)
end
end
render json: #photos.to_json
end
I did not test above code, just wrote, but I used it a lot for my apis.
Controller
Save the object
How to Use This Controller to insert image in any folder and image url store in database
def create_json
#user = User.new(userFirstName: params[:userFirstName], userLastName: params[:userLastName], userEmail: params[:userEmail], password: encrypted_password, userImage: params[:userImage])
if #user.save #if save succeeds, redirect to the index action
redirect_to(:action => 'show', id: User.last.id)
else
#if not succeeds, redirect to the index action
redirect_to(:action => 'new')
end end
User(Model) mount_uploader :userImage, AvatarUploader
UsersController -> #user = User.new(user_params)
if #user.save
redirect_to(:action => 'show', id: User.last.id)
else
render :json => data_hash2, :content_type => 'application/json'
end
class AvatarUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def extension_white_list
%w(jpg jpeg gif png)
end end
If You Using Web Service
<form action="http://ruby/controllername/create" lass="new_user" id="new_user" enctype="multipart/form-data" accept-charset="UTF-8" method="post">
View following method for uploading files to app/assets/images/people_profile_images folder
First upload Image like this:
#user =User.new(user_params)
#this will create user form paramaters given
uploaded_file = params[:user][:photo]
#this will add uploaded image or file in uplaoded_fie
if (uploaded_file)
File.open(Rails.root.join('app/assets', 'images/people_profile_images', uploaded_file.original_filename), 'wb') do |file|
file.write(uploaded_file.read)
#user.photo = "/assets/people_profile_images/"+uploaded_file.original_filename
end
else
#user.photo = "/assets/people_profile_images/default_avatar.png"
end
then save user
#user.save
I don't know of an approach that can cover both: image uploading and then saving the url on the database.
You can do one or the other:
1) Use a gem called paperclip that helps upload image files.
See this links:
https://github.com/thoughtbot/paperclip#ruby-and-rails
https://teamtreehouse.com/library/image-uploads-in-ruby-on-rails-41 (paperclip, carrierwave, and dragonfly)
I have used paperclip and it has worked well for me.
OR:
2) Include it as an additional field on your filename.html.erb for your image_url
One caveat, of using this though is that this can be changed and you have no control of the image that might show on your site. Thus, the image_url should be from a reputable CDN or at least from a reputable user uploading to your site.
Create a column url:string in your model. Add it as a hidden field in your form. Set its value to the same as your remote_url using .js
var urladdy = input.remoteurl.val()
input.url.val(urladdy)
This will upload image with carrier wave and also save its url as string.
I have the model that uses paperclip like this
has_attached_file :image, styles: { :medium => "50x50>" }
validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/
def image_url
image.url(:medium)
end
I need it Json, So in my controller,
respond_to do |format|
format.json { render json: #celebrity.to_json(:methods => [:image_url])}
end
And the result is
"image_url":"/system/celebrities/images/000/000/003/medium/Designed___developed_by_wd_ApS.png?1430926991"
but, I need to include the domain name, localhost:3000 ,
So what I have to do here
Try this.
Create module:
module WithDomain
def domain
#domain
end
def domain=(val)
#domain = val
end
def domain?
#domain.present?
end
end
Change you model accordingly:
class Celebtiry < ActiveRecord::Base
include WithDomain
# existing staff
def image_url
if domain?
URI.join(domain, image.url(:medium)).to_s
else
image.url(:medium)
end
end
end
and in your controller:
respond_to do |format|
format.json {
#celebrity.domain = request.base_url
render json: #celebrity.to_json(:methods => [:image_url])
}
end
Solution 1: (with existing code)
You can use asset_url from ActionView::Helpers::AssetUrlHelper module which will give you the absolute url of your image. Just include ActionView::Helpers::AssetUrlHelper this in your model so that asset_url becomes available inside your model.
So, your method inside the model would be:
include ActionView::Helpers::AssetUrlHelper
def image_url
asset_url(image.url(:medium))
end
This is the easiest solution for you with your current code.
Solution 2: (inside the controller)
In your controller request is available, so you can do:
URI.join(request.url, #celebrity.image.url(:medium))
which will give you the absolute url of the image. This will be an URI object, which can be converted to a String with .to_s method.
Here is the issue from paperclip from where this solution is derived. Hope this helps.
I am using paperclip with s3 in a rails 4 app. It is working fine everywhere, but I have a specific use case that is requiring some special behavior.
I need to upload an image as an avatar, and have it resize to all the thumbnail sizes, but then I need to be able to update only the original image, while preserving all the thumbnail links.
Currently, I'm using a Proc to determine attachment sizes based on a class variable. This is causing image 1 to be uploaded and resized, then I am setting image 2 with no styles. I was hoping this would create all the thumbs, then replace the original. Unfortunately, it is updating the URLs for each size, but they are empty.
tl;dr - I need to have avatars resized, but I need to be able to update ONLY the original and leave the rest alone.
Controller
class StudentsController < ApplicationController
# POST /students/:id/avatar
def new_avatar
current_student.avatar = params[:avatar]
current_student.set_orginial_only TRUE
current_student.avatar = params[:avatar_orig]
if current_student.save
render json: current_student, serializer: StudentAvatarSerializer, status: 200
else
render json: ErrorSerializer.new(current_student), status: 400
end
end
# DELETE /students/:id/avatar
def destroy_avatar
current_student.avatar.destroy
if current_student.save
render json: {success: true}, status: 200
else
render json: ErrorSerializer.new(current_student), status: 400
end
end
private
# find student by id and cache
def current_student
#student ||= Student.find(params[:id])
end
end
Model
class Student < ActiveRecord::Base
##orginial_only = FALSE
def set_orginial_only value
##orginial_only = value
end
# Paperclip attachements
has_attached_file :avatar, :styles => Proc.new { |clip| clip.instance.attachment_sizes },
path: "/:class/:attachment/:id/:content_type_extension/:style/:filename",
:default_url => "/images/:style/missing.png"
validates_attachment_content_type :avatar, :content_type => /image/
validates_attachment_size :avatar, :in => 0..2.megabytes
def attachment_sizes
if ##orginial_only
styles = {}
else
styles = {
tiny: '50x50#',
tiny_retina: '100x100#',
small: '60x60#',
small_retina: '120x120#',
medium: '108x108#',
medium_retina: '216x216#',
large: '205x205#',
large_retina: '410x410#'
}
end
styles
end
end
Is there a way to only update the original image while keeping all my thumbs?
Solution: I ended up just using paperclip for the resized image, and aws-sdk to manually replace the original image in the paperclip assigned path.
I'm writing some image upload code for Ruby on Rails with Paperclip, and I've got a working solution but it's very hacky so I'd really appreciate advice on how to better implement it. I have an 'Asset' class containing information about the uploaded images including the Paperclip attachment, and a 'Generator' class that encapsulates sizing information. Each 'Project' has multiple assets and generators; all Assets should be resized according to the sizes specified by each generator; each Project therefore has a certain set of sizes that all of its assets should have.
Generator model:
class Generator < ActiveRecord::Base
attr_accessible :height, :width
belongs_to :project
def sym
"#{self.width}x#{self.height}".to_sym
end
end
Asset model:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
# this is utterly horrendous
def styles
s = {}
if #generators == nil
#generators = self.project.generators
end
#generators.each do |g|
s[g.sym] = "#{g.width}x#{g.height}"
end
s
end
end
Asset controller create method:
def create
#project = Project.find(params[:project_id])
#asset = Asset.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.project = #project
#asset.uploaded_by = current_user
respond_to do |format|
if #asset.save_(current_user)
#project.last_asset = #asset
#project.save
format.html { redirect_to project_asset_url(#asset.project, #asset), notice: 'Asset was successfully created.' }
format.json { render json: #asset, status: :created, location: #asset }
else
format.html { render action: "new" }
format.json { render json: #asset.errors, status: :unprocessable_entity }
end
end
end
The problem I am having is a chicken-egg issue: the newly created Asset doesn't know which generators (size specifications) to use until after it's been instantiated properly. I tried using #project.assets.build, but then the Paperclip code is still executed before the Asset gets its project association set and nils out on me.
The 'if #generators == nil' hack is so the update method will work without further hacking in the controller.
All in all it feels pretty bad. Can anyone suggest how to write this in a more sensible way, or even an approach to take for this kind of thing?
Thanks in advance! :)
I ran into the same Paperclip chicken/egg issue on a project trying to use dynamic styles based on the associated model with a polymorphic relationship. I've adapted my solution to your existing code. An explanation follows:
class Asset < ActiveRecord::Base
attr_accessible :image, :deferred_image
attr_writer :deferred_image
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
after_save :assign_deferred_image
def styles
project.generators.each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
private
def assign_deferred_image
if #deferred_image
self.image = #deferred_image
#deferred_image = nil
save!
end
end
end
Basically, to get around the issue of Paperclip trying to retrieve the dynamic styles before the project relation information has been propagated, you can assign all of the image attributes to a non-Paperclip attribute (in this instance, I have name it deferred_image). The after_save hook assigns the value of #deferred_image to self.image, which kicks off all the Paperclip jazz.
Your controller becomes:
# AssetsController
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.build(params[:asset])
#asset.uploaded_by = current_user
respond_to do |format|
# all this is unrelated and can stay the same
end
end
And the view:
<%= form_for #asset do |f| %>
<%# other asset attributes %>
<%= f.label :deferred_upload %>
<%= f.file_field :deferred_upload %>
<%= f.submit %>
<% end %>
This solution also allows using accepts_nested_attributes for the assets relation in the Project model (which is currently how I'm using it - to upload assets as part of creating/editing a Project).
There are some downsides to this approach (ex. validating the Paperclip image in relation to the validity of the Asset instance gets tricky), but it's the best I could come up with short of monkey patching Paperclip to somehow defer execution of the style method until after the association information had been populated.
I'll be keeping an eye on this question to see if anyone has a better solution to this problem!
At the very least, if you choose to keep using your same solution, you can make the following stylistic improvement to your Asset#styles method:
def styles
(#generators || project.generators).each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
Does the exact same thing as your existing method, but more succinctly.
While I really like Cade's solution, just a suggestion. It seems like the 'styles' belong to a project...so why aren't you calculating the generators there?
For example:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.project.styles }
end
class Project < ActiveRecord::Base
....
def styles
#generators ||= self.generators.inject {} do |hash, g|
hash[g.sym] = "#{g.width}x#{g.height}"
end
end
end
EDIT: Try changing your controller to (assuming the project has many assets):
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.uploaded_by = current_user
end
I've just solved a similar problem that I had.
In my "styles" lambda I am returning a different style depending on the value of a "category" attribute. The problem though is that Image.new(attrs), and image.update_attributes(attrs) doesn't set the attributes in a predictable order, and thus I can't be guaranteed that image.category will have a value before my styles lambda is called. My solution was to override attributes=() in my Image model as follows:
class Image
...
has_attached_file :image, :styles => my_lambda, ...
...
def attributes=(new_attributes, guard_protected_attributes = true)
return unless new_attributes.is_a?(Hash)
if new_attributes.key?("image")
only_attached_file = {
"image" => new_attributes["image"]
}
without_attached_file = new_attributes
without_attached_file.delete("image")
# set the non-paperclip attributes first
super(without_attached_file, guard_protected_attributes)
# set the paperclip attribute(s) after
super(only_attached_file, guard_protected_attributes)
else
super(new_attributes, guard_protected_attributes)
end
end
...
end
This ensures that the paperclip attribute is set after the other attributes and can thus use them in a :style lambda.
It clearly won't help in situations where the paperclip attribute is "manually" set. However in those circumstances you can help yourself by specifying a sensible order. In my case I could write:
image = Image.new
image.category = "some category"
image.image = File.open("/somefile") # styles lambda can use the "category" attribute
image.save!
(Paperclip 2.7.4, rails 3, ruby 1.8.7)