Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending files to a Rails JSON API

I know there are questions similar to this one, but I've not found a good answer yet. What I need to do is send a description of an object to one of my create methods, which includes some different attributes including one called :image, a paperclip attachment:

has_attached_file :image

Now I've read that sending the image could be done straight in JSON by encoding and decoding the image as base64, but that feels like a dirty solution to me. There must be better ways.

Another solution is sending a multipart/form-data request, much like the one LEEjava describes here. The problem with that one is that the request params are not interpreted correctly in Rails 3.2.2, and JSON.parse spits out an error when it tries to parse the params, or perhaps it is Rails that is misinterpreting something.

Started POST "/api/v1/somemodel.json?token=ZoipX7yhcGfrWauoGyog" for 127.0.0.1 at 2012-03-18 15:53:30 +0200 Processing by Api::V1::SomeController#create as JSON Parameters: {"{\n
\"parentmodel\": {\n \"superparent_id\": 1,\n
\"description\": \"Enjoy the flower\",\n \"\": "=>{"\n
{\n \"someattribute\": 1,\n
\"someotherattribute\": 2,\n \"image\": \"image1\"\n
}\n "=>{"\n }\n}"=>nil}}, "token"=>"ZoipX7yhcGfrWauoGyog"}

It is quite hard to read that, sorry. JSON.parse(params[:parentmodel]) is not possible here, and I can't JSON.parse(params) either because of the token attribute, JSON.parse(params) throws this error:

TypeError (can't convert ActiveSupport::HashWithIndifferentAccess into String)

Which leads me to believe I'm either approaching this problem totally wrong, or I'm just doing something. Either way, we can be sure that I'm wrong about something. :)

Is there a better way to do this? Can someone point me to any guide/tutorial, or write an answer describing how I should approach this?

Thank you in advance

UPDATE: So I've actually got it working now, but only in tests. I'm not totally sure how this works, but perhaps someone can fill in the gaps for me? This is part of the test code (the image: fixture_file_upload(...) is the important part).

parts_of_the_object = { someattribute: 0, someotherattribute: 0, image: fixture_file_upload('/images/plot.jpg', 'image/jpg') }

My params[] looks like a normal HTML form was submitted, which is strange (and awesome):

Parameters: {"superparentid"=>"1", "plots"=>[{"someattribute"=>"0", "someotherattribute"=>"0", "image"=>#<ActionDispatch::Http::UploadedFile:0x007f812eab00e8 @original_filename="plot.jpg", @content_type="image/jpg", @headers="Content-Disposition: form-data; name=\"plots[][image]\"; filename=\"plot.jpg\"\r\nContent-Type: image/jpg\r\nContent-Length: 51818\r\n", @tempfile=#<File:/var/folders/45/rcdbb3p50bl2rgjzqp3f0grw0000gn/T/RackMultipart20120318-1242-1cn036o>>}], "token"=>"4L5LszuXQMY6rExfifio"}

The request is made just like and post request is made with rspec:

post "/api/v1/mycontroller.json?token=#{@token}", thefull_object

So I've got it all working. I just don't know how exactly it works! I want to be able to create a response like this by myself too, not only from RSpec. :-)

like image 628
Emil Ahlbäck Avatar asked Mar 18 '12 13:03

Emil Ahlbäck


People also ask

Can you send a file over API?

Transferring Files with APIs RESTful HTTP based APIs are the current 'go-to' approach for designing applications and file upload and download is a common business requirement for many applications. Files can be streamed attachments or links to the actual content.

What is REST API in Rails?

REST stands for REpresentational State Transfer and describes resources (in our case URLs) on which we can perform actions. CRUD , which stands for Create, Read, Update, Delete, are the actions that we perform. Although, in Rails, REST and CRUD are bestest buddies, the two can work fine on their own.

What is API in Ruby on Rails?

As perfectly explained on Ruby on Rails guides, when people say they use Rails as an “API”, it means developers are using Rails to build a back-end that is shared between their web application and other native applications.


3 Answers

I was actually having a terrible time with this question yesterday to do something very similar. In fact, I wrote the question: Base64 upload from Android/Java to RoR Carrierwave

What it came down to was creating that uploaded image object in the controller and then injecting it back into the params.

For this specific example, we are taking a base64 file (which I assume you have, as JSON doesn't support embeded files) and saving it as a temp file in the system then we are creating that UploadedFile object and finally reinjecting it into the params.

What my json/params looks like:

picture {:user_id => "1", :folder_id => 1, etc., :picture_path {:file => "base64 awesomeness", :original_filename => "my file name", :filename => "my file name"}}

Here is what my controller looks like now:

  # POST /pictures
  # POST /pictures.json
  def create

    #check if file is within picture_path
    if params[:picture][:picture_path]["file"]
         picture_path_params = params[:picture][:picture_path]
         #create a new tempfile named fileupload
         tempfile = Tempfile.new("fileupload")
         tempfile.binmode
         #get the file and decode it with base64 then write it to the tempfile
         tempfile.write(Base64.decode64(picture_path_params["file"]))

         #create a new uploaded file
         uploaded_file = ActionDispatch::Http::UploadedFile.new(:tempfile => tempfile, :filename => picture_path_params["filename"], :original_filename => picture_path_params["original_filename"]) 

         #replace picture_path with the new uploaded file
         params[:picture][:picture_path] =  uploaded_file

    end

    @picture = Picture.new(params[:picture])

    respond_to do |format|
      if @picture.save
        format.html { redirect_to @picture, notice: 'Picture was successfully created.' }
        format.json { render json: @picture, status: :created, location: @picture }
      else
        format.html { render action: "new" }
        format.json { render json: @picture.errors, status: :unprocessable_entity }
      end
    end
  end

The only thing left to do at this point is to delete the tempfile, which I believe can be done with tempfile.delete

I hope this helps with your question! I spent all day looking for a solution yesterday, and everything I have seen is a dead end. This, however, works on my test cases.

like image 77
TomJ Avatar answered Nov 04 '22 00:11

TomJ


TomJ gave a good answer, but at least in Rails 3/Ruby 1.9 there are some minor holes.

First, don't attempt to call [] on what might be an UploadedFile object in your params object. Make sure you check that it .is_a?(Hash) first, for example.

Also, make sure you tempfile.rewind() after you write, otherwise you'll get files with 0 length.

The :original_filename key in the parameters to the constructor of UploadedFile is unnecessary/unused. On the other hand, you may want to provide a :type key. An easy way to find the value for type is mime_type = Mime::Type.lookup_by_extension(File.extname(original_filename)[1..-1]).to_s

Here is a version with the changes applied:

# POST /pictures
# POST /pictures.json
def create

  #check if file is within picture_path
  if params[:picture][:picture_path].is_a?(Hash)
    picture_path_params = params[:picture][:picture_path]
    #create a new tempfile named fileupload
    tempfile = Tempfile.new("fileupload")
    tempfile.binmode
    #get the file and decode it with base64 then write it to the tempfile
    tempfile.write(Base64.decode64(picture_path_params["file"]))
    tempfile.rewind()
 
    original_filename = picture_path_params["original_filename"]
    mime_type = Mime::Type.lookup_by_extension(File.extname(original_filename)[1..-1]).to_s
    #create a new uploaded file
    uploaded_file = ActionDispatch::Http::UploadedFile.new(
      :tempfile => tempfile,
      :filename => picture_path_params["filename"],
      :type => mime_type) 
       
    #replace picture_path with the new uploaded file
    params[:picture][:picture_path] =  uploaded_file
  end
    
  @picture = Picture.new(params[:picture])
  respond_to do |format|
    if @picture.save
      format.html { redirect_to @picture, notice: 'Picture was successfully created.' }
      format.json { render json: @picture, status: :created, location: @picture }
    else
     format.html { render action: "new" }
     format.json { render json: @picture.errors, status: :unprocessable_entity }
   end
 end

end

like image 37
Shannon Avatar answered Nov 04 '22 00:11

Shannon


There is an awesome gem for this purpose if you are using carrierwave

https://github.com/lebedev-yury/carrierwave-base64

like image 5
peeyush singla Avatar answered Nov 04 '22 00:11

peeyush singla