Multi step form with image uploader - ruby-on-rails

I want to build 3 step user registration with avatar uploading on 2nd step. So i follow Ryan Bates's guide http://railscasts.com/episodes/217-multistep-forms . I'm using CarrierWave gem to handle uploads. But it seems like i can't store uploaded file info in user session (i'm getting can't dump File error). I use following technique in controller
if params[:user][:img_path]
#uploader = FirmImgUploader.new
#uploader.store!(params[:user][:img_path])
session[:img] = #uploader
params[:user].delete(:img_path)
end
It actually helps. But when i upload forbidden file type everything's crashes on this line
#uploader.store!(params[:user][:img_path])
with this error
CarrierWave::IntegrityError in UsersController#create
You are not allowed to upload "docx" files, allowed types: ["jpg", "jpeg", "gif", "png"]
instead of normal form validation error.
How can i solve this problem ? Thanks !

Actually I solved my problem. Here's working code for multistep forms with file uploading using carrierwave
if params[:user][:img_path]
#uploaded = params[:user][:img_path]
params[:user].delete(:img_path)
end
session[:user_data].deep_merge!(params[:user]) if params[:user]
#user = User.new(session[:user_data])
if #uploaded
# here how validation will work
#user.img_path = #uploaded
end
#user.current_stage = session[:register_stage]
if #user.valid?
if #user.last_stage?
#user.img_path = session[:img] if #user.last_stage?
#user.save
else
#user.next_stage
end
# now we can store carrierwave object in session
session[:img] = #user.img_path
session[:register_stage] = #user.current_stage
end

This may be a little late for the OP, but hopefully this helps someone. I needed to store an uploaded image in the user's session (again for a multi-step form), and I too started with Ryan's Railscast #217, but the app quickly evolved beyond that. Note that my environment was Rails 4 on Ruby 2, using Carrierwave and MiniMagick, as well as activerecord-session_store, which I'll explain below.
I believe the problem that both OP and I had was that we were trying to add all of the POST params to the user's session, but with a file upload, one of the params was an actual UploadedFile object, which is way to big for that. The approach described below is another solution to that problem.
Disclaimer: As is widely noted, it's not ideal to store complex objects in a user's session, better to store record identifiers or other identifier data (e.g. an image's path) and look up that data when it's needed. Two major reasons for this keeping the session and model/database data in sync (a non-trivial task), and the default Rails session store (using cookies) is limited to 4kb.
My Model (submission.rb):
class Submission < ActiveRecord::Base
mount_uploader :image_original, ImageUploader
# ...
end
Controller (submissions_controller.rb):
def create
# If the submission POST contains an image, set it as an instance variable,
# because we're going to remove it from the params
if params[:submission] && params[:submission][:image_original] && !params[:submission][:image_original].is_a?(String)
# Store the UploadedFile object as an instance variable
#image = params[:submission][:image_original]
# Remove the uploaded object from the submission POST params, since we
# don't want to merge the whole object into the user's session
params[:submission].delete(:image_original)
end
# Merge existing session with POST params
session[:submission_params].deep_merge!(params[:submission]) if params[:submission]
# Instantiate model from newly merged session/params
#submission = Submission.new(session[:submission_params])
# Increment the current step in the session form
#submission.current_step = session[:submission_step]
# ... other steps in the form
# After deep_merge, bring back the image
if #image
# This adds the image back to the Carrierwave mounted uploader (which
# re-runs any processing/versions specified in the uploader class):
#submission.image_original = #image
# The mounted uploader now has the image stored in the Carrierwave cache,
# and provides us with the cache identifier, which is what we will save
# in our session:
session[:submission_params][:image_original] = #submission.image_original_cache
session[:image_processed_cached] = #submission.image_original.url(:image_processed)
end
# ... other steps in the form
# If we're on the last step of the form, fetch the image and save the model
if #submission.last_step?
# Re-populate the Carrierwave uploader's cache with the cache identifier
# saved in the session
#submission.image_original_cache = session[:submission_params][:image_original]
# Save the model
#submission.save
# ... render/redirect_to ...
end
end
My uploader file was mostly stock with some custom processing.
Note: to beef up sessions, I'm using activerecord-session_store, which is a gem that was extracted from the Rails core in v4 that provides a database-backed session store (thus increasing the 4kb session limit). Follow the documentation for installation instructions, but in my case it was pretty quick and painless to set it and forget it. Note for high-traffic users: the leftover session records don't seem to be purged by the gem, so if you get enough traffic this table could potentially balloon to untold numbers of rows.

Related

How to find the model an ActiveStorage attachment belongs to using only the blob_path?

To prevent anyone with a url accessing restricted files, I need to check user role permissions to view those ActiveStorage model attachments beforehand. I've created an ActiveStorage::BaseController with a before action that does all the needed checks.
I currently find the original parent model that the attachment belongs to by using the signed_id in the params:
signed_id = params[:signed_id].presence || params[:signed_blob_id].presence
blob_id = ActiveStorage::Blob.find_signed(signed_id)
attachment = ActiveStorage::Attachment.find_by(blob_id: blob_id)
record = attachment.record
# record.some_model method_or_check etc.
This works really well and I'm able to do all the stuff I want to do, however, sometimes the signed_id is not in the params and instead I just get:
{"encoded_key"=>
"[LONG KEY HERE]",
"filename"=>"sample",
"format"=>"pdf"}
I have no idea why this happens unpredictably as I'm always using rails_blob_path(model.attachment, disposition: 'attachment') or rails_blob_path(model.attachment, disposition: 'preview') everywhere in the code and testing under the same conditions.
Is it possible to ensure the signed_id is always present or alternatively, is there a way to get back to the parent model using the encoded_key as rails obviously does this to display the attachment anyway?
I am not sure you need it
If you're worried that someone might view the file if he/she knows URL, you can make time expiration limit for that URL
# config/initializers/active_storage.rb
Rails.application.config.active_storage.service_urls_expire_in = 1.minute
It is unlikely that an attacker will be able to generate URL in such a way as to be able to view that file

Delayed job to check if a carrierwave_direct upload has been completed and attached to a model

I'm using carrierwave_direct and delayed_job with user uploaded images. The process is:
User drag & drops or selects a file.
File is uploaded to S3 directly using carrierwave_direct to do all the form signing etc.
User is presented with a dialog to crop their image, the key (from carrierwave_direct) is passed in through a hidden field. At this point no record exists in the database, but a file is stored on S3. Should the user now move away from the page, we will have an abandoned / unattached file on S3 that does not belong to any model.
If user completes the process, a record will be created and processing (cropping and creating several thumbnail versions) is done by a worker queue.
I had to make a delayed job that is put into the queue as soon as the file is uploaded. It will have a delay of several hours (or days). This job checks to see if the file exists, then checks through all recently created image records in the database to see if any are attached to that file. If not, it deletes the file.
Lastly, I wanted to make use of Carrierwave classes to handle all the Fog stuff, since I was using it anyway.
I could not find this anywhere, so here is my version. Leaving this here for people to come across in the future.
This is how I did it.
def new
# STEP 1: Show upload dialog that user can drop an image unto
#image = current_user.portfolio.images.new.images
# Status 201 returns an XML
#image.success_action_status = "201"
end
def crop
# STEP 2: A file is now on Amazon S3, but no record exists in the database yet. Make the user crop the image and enter a title.
# Meanwhile, also setup a delayed job that will later check if this step has been completed.
# Note: the crop view gets retrieved and inserted using ajax in images.js.coffee
#image = Image.new(key: params[:key])
Delayed::Job.enqueue ImageDeleteIfUnattachedJob.new(params[:key]), 0, 15.minute.from_now.getutc
render :partial => "images/crop.html.erb", :object => #image
end
ImageDeleteIfUnattachedJob = Struct.new(:key) do
def perform
# Do any of the images created in the last week match the key that was passed in?
# If not, the user probably went through the upload process, which then either went wrong or was cancelled.
unless Image.where("created_at > ?", 1.week.ago).order('created_at DESC').any? { |image| key == image.images.path }
# We spawn these to let Carrierwave handle the Fog stuff.
#uploader = ImageUploader.new
#storage = CarrierWave::Storage::Fog.new(#uploader)
#file = CarrierWave::Storage::Fog::File.new(#uploader, #storage, key)
if #file.exists?
# Indeed, the file is still there. Destroy it.
#file.delete
else
return true
end
end
end
def max_attempts
3
end
end

Rails-RMagick handle single image

I'm building a rails app where a user uploads an image, then it gets sent to rmagick to be modified and then gets rendered. Since the user only handles one image, I was initially thinking that I could store it in memory instead of the database, but that seems to not be feasible. The model name is AppImage, so I then thought about displaying AppImage.last and deleting all previous AppImages right before rendering it, but I'm wondering if that would cause problems with multiple users.
Is the best solution to have each user get a user profile according to their IP, and have one AppImage per user? Should I be thinking about session hashes?
Edit: I am currently using paperclip, but just am not sure how to structure the program.
if an user uploads an image, you could set a session variable to true, and check in your uploads controller if the session variable is set or not..dependent on that you allow the user to upload an image, or not. you can set the session store to db, further you can define a range how long the session is saved.
controller:
def new
#upload = YourUploadModel.new
session[:image_uploaded] ||= true
end
def create
if session[:image_uploaded] && session[:image_uploaded] == true
redirect_to root_path, :notice => "Already uploaded an image today!"
else
# create your upload..
end
end
app/config/initializers/session_store.rb:
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "rails generate session_migration")
YourAppname::Application.config.session_store :active_record_store, {
expire_after: 1.days
}

Rails 3.1 Deny editing other users images

I have a web app which uses Devise for authentication. It is a site which allows the user to upload images so the url would be /images/2. There is a separate image controller.
I have found that a user could edit an image they didn't upload by changing then URL, e.g. /images/4/edit. Is there a way to block them from editing other users images and only allowing them to edit their own?
The images controller, model, etc was created using rails g scaffold if that helps.
There are many different solutions for this, but the simplest is when running edit/update/destroy load the image from the current user instead of from all images:
def edit
#image = current_user.images.find( params[:id] )
end
def update
#image = current_user.images.find( params[:id] )
# do whatever has to be done
end
def destroy
#image = current_user.images.find( params[:id] )
# do whatever has to be done
end
Also, using scaffolds is a really bad practice, you should just write our own code, it's simpler, more productive and will lead you to understand how the framework is supposed to work.
You could also use nested controllers for this, but you would have to research a bit to understand how they work and why they might be a better solution.

Paperclip - delete a file from Amazon S3?

I need to be able to delete files from S3 that are stored by users, such as profile photos. Just calling #user.logo.destroy doesn't seem to do the trick - I get [paperclip] Saving attachments. in the logs and the file stays right there in the S3 bucket.
How can the file itself be removed?
This are the methods from Paperclip that can be used to remove the attachments:
# Clears out the attachment. Has the same effect as previously assigning
# nil to the attachment. Does NOT save. If you wish to clear AND save,
# use #destroy.
def clear(*styles_to_clear)
if styles_to_clear.any?
queue_some_for_delete(*styles_to_clear)
else
queue_all_for_delete
#queued_for_write = {}
#errors = {}
end
end
# Destroys the attachment. Has the same effect as previously assigning
# nil to the attachment *and saving*. This is permanent. If you wish to
# wipe out the existing attachment but not save, use #clear.
def destroy
clear
save
end
So you see, destroy only removes the attachment if no error occurs. I have tried it with my own setup against S3 so I know that destroy works.
Could the problem in your case possible be that you have any validations that cancels the save? I.e validates_attachment_presence or something similar?
I think one way to find out would be to try #user.logo.destroy and then check the content of #user.errors to see if it reports any error messages.
This seems like an answer to your question, although I don't totally understand their distinction between destroy and clear (I don't know which model has_attached_file, page or image):
Rails Paperclip how to delete attachment?

Resources