Sending files to a Rails JSON API - ruby-on-rails

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. :-)

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

Related

When you upload multiple files using ActiveStorage, is it possible to determine what was just uploaded?

I have a model (let's call it Message) that's using Active Storage with has_many_attached to store multiple uploads through a Javascript routine. I'd like to know the URL of the uploads as soon as these are received and processed by the Update action in my controller. Ideally, the response would be in the form of a JSON string, containing the URL of the file that is just uploaded by the Javascript routine.
Other than having to loop through all the uploads, is there a way to obtain this information as they're uploaded?
I'm not using the DirectUpload javascript suggested in the Active Storage guide, in case you're wondering. The Javascript I'm using sends the files to a custom PATCH action /message/:id/new_upload (where :id is the ID of the Message record), with the BLOB file as its only parameter.
I've tried to use something like this:
def new_upload
#message.uploads.attach(params[:uploads])
respond_to do |format|
format.json {render json: { location: "The URL would go here"} }
end
end
But the attach line at the beginning doesn't return anything (obviously), all it is is doing is saving the attachments. Is there anything I can use to figure out what was just uploaded?
UPDATE
I've managed to make it "work" by doing something like this:
def new_upload
#message.uploads.attach(params[:uploads])
upload_url = rails_blob_url(#message.uploads.blobs.last)
respond_to do |format|
format.json {render json: { location: upload_url} }
end
end
However, this only works for one attachment (the last). Thankfully I only require to upload one file at a time, but I'd prefer to infer the URL by something other than checking the last upload.

Rails: JSON.pretty_generate(obj) in the controller does not produce pretty output

I need a pretty output of the JSON for an activerecord object in the rails controller. Based on the answer to this question by jpatokal, I tried the following:
respond_to do |format|
format.json { render :json => JSON.pretty_generate(record) }
end
where
record
is an activerecord object. It does not produce a prettified output. However, when I try outputting a hash using the same code, viz,
respond_to do |format|
format.json { render :json => JSON.pretty_generate({"abc" => "1", "def" => "2"}) }
end
it does produce a prettified output (so pretty_generate is working, and so is my browser).
How do I use pretty_generate to produce a pretty output of an activerecord object?
To get pretty output of JSON from an active record object you have to first request the object as JSON.
record.as_json
The above code will do this for you, the short way to render out pretty JSON from the controller is:
render json: JSON.pretty_generate(record.as_json)
This also solves the "only generation of JSON objects or arrays allowed" error you can get trying to convert AR objects to pretty_generate JSON.
EDIT: I forgot to mention this can all be done in rails 4.1.8 and above (possibly even earlier) using the standard json and multi-json gems packaged with rails project.

Ruby On Rails: upload a file to a specific folder

I am using this file uploader example for Ruby on Rail.
Now the files are saved by their IDs into: public->system->uploads->uploads->000->000.
I need it to be saved into such system of folders: public->system->Files->Types(JPG OR PDF)
The files have to be saved not according to their IDs but according to their types.
Example:
All jpg files should be saved into:
public->system->Files->JPG
All PDF files should be saved into:
public->system->Files->PDF
I have already created the folders, I need only to specify a path where the files have to be saved.
That is a code from uploads_controller.rb
def create
p_attr=params[:upload]
p_attr[:upload] = params[:upload][:upload].first if params[:upload][:upload].class == Array
#upload = Upload.new(p_attr)
respond_to do |format|
if #upload.save
format.html {
render :json => [#upload.to_jq_upload].to_json,
:content_type => 'text/html',
:layout => false
}
format.json { render json: [#upload.to_jq_upload].to_json, status: :created, location: #upload }
else
format.html { render action: "new" }
format.json{ render json: {name:(#upload.upload_file_name).split(".").first ,error: #upload.errors.messages[:upload_file_name]}, :status =>422}
end
end
end
Thanks in advance
I'd recommend switching to carrierwave uploader. There are config options , which are going to help you in separating files by type . I mean , in this configuration in carrierwave uploader :
def store_dir
"uploads/#{Rails.env}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
you can alter the 'model.id' part with a method , naming the image by type .
You can manually do this using regex. So it'd be something like:
type = p_attr.filename.match(/^*\w{3,}$/).to_s
filename being whatever its called within p_attr. So that would give you the file type ending as a string. According to the paperclip gem you can set a different path and url by setting them as a param:
path = "#{Rails.env}/public/system/FILE/#{type}"
#upload = Upload.new(p.attrs, path: path, url: path)
Not 100% sure on the upload line, but something like that should be able to override the paperclip default.
For more information check the paperclip readme section about storage. It explains about overriding default path.
Changing Paperclip File Storage Location

Sending an image through JSON data

noobie here hope you guys don't mind! Im trying to query my user/id/pictures.json but all it returns are attributes cus i did a generic format.json {render :json => #photo.to_json()}. My question is how can i create and encapsulate the actual data from the images, so my client can turn that data in to an image? And also what do i need to create(attribute wise) besides the path of image(say you only had useless attributes eg: height content_type, description, thumbnail file_name)?
this is what im trying in my index.json.erb so far
}
<% #photos.each do |photo|%>
data: <%= StringIO.new(Base64.encode64(photo.public_filename(:large))) %>
<%end%>
}
i am getting back
{
data: #<StringIO:0x1058a6cd0>
}
which is not the IMGdata im looking for
looking for
Have a look at Data-URIs.
They essentially are Base64-encoded entities (documents) formatted as a URI
[{ "name":"red dot", "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="}, ...]
[UPDATE]
You need to read the file and encode it as Base64 (you also need to strip the newlines away in rails 2.3.x)
data = ActiveSupport::Base64.encode64(File.read("/images/image1.png")).gsub("\n", '')
uri = "data:image/png;base64,#{data}"
I think you are using Ruby on Rails, aren't you?
Then there are some steps needed to download an image (e.g. a png):
Create a mime type
Go to config/initializers/mime_types.rb and insert Mime::Type.register "image/png", :png at the end.
Create an image
For example, you could use the gem Chunky_PNG to create an image, see at http://rubygems.org/gems/chunky_png and https://github.com/wvanbergen/chunky_png/wiki
Prepare your controller
You have to tell your controller, that it can accept pngs. Modify your controller the following way
class UsersController < ApplicationController
respond_to :json, :png
def show
# your own stuff
# ...
respond_with(response) do |format|
format.json
format.png do
send_data ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT), :type =>"image/png", :disposition => 'inline'
end
end
end
end
This will create a fully transparent image. If you want to draw something in this, look at the Chunky PNG docs.
It's up to the client how to render it really. This works for me, maybe worth a try.
render json: #thumbnail, type: :jpeg, content_type: 'image/jpeg'

respond_to with custom format aliasing json format, is this possible?

I have a situation where I'm returning json objects to my application which are built from YML files. Because to parse the yml file and return it as json I always have to do something like this:
format.json { render json: YAML.load(render_to_string :file => File.join(Rails.root,'app','views','home','icons.yml.erb'), :layout => false ) }
I would like to make this operation shorter, by creating a custom format that (however) result in a json, so I don't want to create a new mime type.
My idea is to write:
format.myformat
Which will automatically search for myaction.myformat.erb inside views/mycontroller directory, and will automatically parse the yaml file returning it as a json object.
Is this possible? If yes, how can I eventually do this?
Edit 1:
I found an important suggestion in config/mime_types.rb:
Mime::Type.register_alias "text/html", :iphone
So I can alias a mime type, now the biggest problem is: how to define the default render action for a given format, like format.html does?
If I write
format.html
current_action.html.erb will be automatically rendered, how can I choose the correct method to render a custom format?
Edit 2:
I managed to create this code (inside a controller, through some helper methods I built):
def icons
respond_to do |format|
format.extjson { render_to_extjson }
end
end
Is possible to make rails understand that if I write:
def icons
respond_to do |format|
format.extjson
end
end
it has to do:
format.extjson { render_to_extjson }
by default?
You could do something like:
respond_to do |format|
format.html { #foo = Foo.all(:limit => 10) }
format.any(:atom, :rss) { #foo = Foo.all }
end
A longer post going into the guts of the render actions can be found here: http://ryanbigg.com/2009/04/how-rails-works-2-mime-types-respond_to/
While Josh's answer is a valid one, I would rather see your parsing code wrapped up into an object. If you put that object into app/models, it'll be testable and you can always verify any change to the logic with a test suite.
Another upside to this is that you can re-use the format.json call and make you controller that much simpler.
This is not doable at the moment, I read a lot of rails sources and there isn't a way to access that method, so isn't possible to customize it.
I'll write a rails plugin to support this and eventually I'll post it here, but for sure the answer actually is: this can't be done.

Resources