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. :-)
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.
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.
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.
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.
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
There is an awesome gem for this purpose if you are using carrierwave
https://github.com/lebedev-yury/carrierwave-base64
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With