13

I'm working on implementing Ajax-Upload for uploading photos in my Rails 3 app. The documentation says:

  1. For IE6-8, Opera, older versions of other browsers you get the file as you normally do with regular form-base uploads.

  2. For browsers which upload file with progress bar, you will need to get the raw post data and write it to the file.

So, how can I receive the raw post data in my controller and write it to a tmp file so my controller can then process it? (In my case the controller is doing some image manipulation and saving to S3.)

Some additional info:

As I'm configured right now the post is passing these parameters:

Parameters:
{"authenticity_token"=>"...", "qqfile"=>"IMG_0064.jpg"}

... and the CREATE action looks like this:

def create
    @attachment = Attachment.new
    @attachment.user = current_user
    @attachment.file = params[:qqfile]
    if @attachment.save!
        respond_to do |format|
            format.js { render :text => '{"success":true}' }
        end
    end
end

... but I get this error:

ActiveRecord::RecordInvalid (Validation failed: File file name must be set.):
  app/controllers/attachments_controller.rb:7:in `create'
edgerunner
  • 14,873
  • 2
  • 57
  • 69
Andrew
  • 42,517
  • 51
  • 181
  • 281
  • I just released an example of ajax upload in rails 3 + Uploadify here: https://github.com/apneadiving/Pic-upload---Crop-in-Ajax . Hope it may help – apneadiving Jan 16 '11 at 11:06

4 Answers4

27

That's because params[:qqfile] isn't a UploadedFile object but a String containing the file name. The content of the file is stored in the body of the request (accessible by using request.body.read). Ofcourse, you can't forget backward compatibility so you still have to support UploadedFile.

So before you can process the file in a uniform way you have to catch both cases:

def create
  ajax_upload = params[:qqfile].is_a?(String)
  filename = ajax_upload  ? params[:qqfile] : params[:qqfile].original_filename
  extension = filename.split('.').last
  # Creating a temp file
  tmp_file = "#{Rails.root}/tmp/uploaded.#{extension}"
  id = 0
  while File.exists?(tmp_file) do
    tmp_file = "#{Rails.root}/tmp/uploaded-#{id}.#{extension}"        
    id += 1
  end
  # Save to temp file
  File.open(tmp_file, 'wb') do |f|
    if ajax_upload
      f.write  request.body.read
    else
      f.write params[:qqfile].read
    end
  end
  # Now you can do your own stuff
end
Stefaan Colman
  • 3,715
  • 2
  • 22
  • 11
  • 4
    Thanks! This worked great! As a comment, I found (after you set me on the right track) that in my case at least `tmp_file = Tempfile.new(filename)` works equally well while being a little cleaner than your code under the `# Creating a temp file` note. Both ways work fine. Thanks! – Andrew Jan 16 '11 at 16:18
  • `f.write params[:qqfile].read` - this should be `f.write params[:qqfile].tempfile.read` otherwise no file is stored – Toshe Mar 22 '14 at 14:14
  • No it shouldn't, UploadedFile#read is a shortcut for UploadedFile#tempfile.read so the code does the same in both cases – Stefaan Colman Mar 24 '14 at 10:14
  • This is a better way to naming the tmp file `id = 0 file_template = "#{Rails.root}/tmp/uploaded-%s.#{extension}" tmp_file = file_template % id while File.exists?(tmp_file) id += 1 tmp_file = file_template % id end` – juliangonzalez Feb 20 '17 at 14:39
5

try it, add lib/qq_file.rb:

# encoding: utf-8
require 'digest/sha1'
require 'mime/types'

# Usage (paperclip example)
# @asset.data = QqFile.new(params[:qqfile], request)
class QqFile < ::Tempfile

  def initialize(filename, request, tmpdir = Dir::tmpdir)
    @original_filename  = filename
    @request = request

    super Digest::SHA1.hexdigest(filename), tmpdir
    fetch
  end

  def self.parse(*args)
    return args.first unless args.first.is_a?(String)
    new(*args)
  end

  def fetch
    self.write @request.raw_post
    self.rewind
    self
  end

  def original_filename
    @original_filename
  end

  def content_type
    types = MIME::Types.type_for(@request.content_type)
      types.empty? ? @request.content_type : types.first.to_s
  end
end

in assets_controller type this:

def create
  @asset ||= Asset.new(params[:asset])

  @asset.assetable_type = params[:assetable_type]
  @asset.assetable_id = params[:assetable_id] || 0
  @asset.guid = params[:guid]
  @asset.data = QqFile.parse(params[:qqfile], request)
  @asset.user_id = 0
  @success = @asset.save

  respond_with(@asset) do |format|
    format.html { render :text => "{'success':#{@success}}" }
    format.xml { render :xml => @asset.to_xml }
    format.js { render :text => "{'success':#{@success}}"}
    format.json { render :json => {:success => @success} }
  end
end

javascript:

var photo_uploader = new qq.FileUploader({
  element: document.getElementById('photo-button'),
  multiple: true,
  action: '/assets',
  allowedExtensions: ['png', 'gif', 'jpg', 'jpeg'],
  sizeLimit: 2097152,
  params: {guid: $('#idea_guid').val(), assetable_type: 'Idea', klass: 'Picture', collection: true}
});
super_p
  • 720
  • 9
  • 11
1

Another solution is:

gem 'rack-raw-upload', :git => 'git://github.com/tb/rack-raw-upload.git'

and in config.ru:

 require 'rack/raw_upload'
 use Rack::RawUpload

and use params[:file] in controller.

tomaszbak
  • 8,247
  • 4
  • 44
  • 37
0

Rather than creating some clutter temp file, you can use StringIO. See my answer about CarrierWave, here: https://stackoverflow.com/a/8812976/478354

Community
  • 1
  • 1
Peter Ehrlich
  • 6,969
  • 4
  • 49
  • 65