Reputation:
I am having a problem with displaying uploaded images in views. IMO due to zero size for each uploaded file in /public/uploads
.
Currently trying to get a grasp in uploading and serving files without any ruby/rails gem.
For this I ran a primitive app with basic scaffold just for testing upload/save cycle:
schema.rb:
ActiveRecord::Schema.define(version: 20151104043709) do
create_table "uploads", force: :cascade do |t|
t.string "filename"
t.string "content_type"
t.binary "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
UploadsController#create:
def create
@upload = Upload.new(upload_params)
data = params[:upload][:file]
File.open(Rails.root.join('public', 'uploads', data.original_filename), 'wb') do |f|
f.write(data.read)
end
respond_to do |format|
if @upload.save
format.html { redirect_to @upload, notice: 'Upload was successfully created.' }
format.json { render :show, status: :created, location: @upload }
else
format.html { render :new }
format.json { render json: @upload.errors, status: :unprocessable_entity }
end
end
end
model Upload.rb :
class Upload < ActiveRecord::Base
include ActionView::Helpers::NumberHelper
attr_accessor :upload
def file=(upload_data)
self.filename = upload_data.original_filename
self.content_type = upload_data.content_type
self.content = upload_data.read
end
def filename=(new_filename)
write_attribute("filename", sanitize_filename(new_filename))
end
private
def sanitize_filename(filename)
just_filename = File.basename(filename)
just_filename.gsub(/[^\w\.\-]/, '_')
end
end
uploads#show view:
<p id="notice"><%= notice %></p>
<p>
<strong>Filename:</strong>
<%= @upload.filename %>
</p>
<p>
<strong>Content type:</strong>
<%= @upload.content_type %>
</p>
<p>
<%= image_tag "/public/uploads/#{@upload.filename}" %>
</p>
<%= link_to 'Edit', edit_upload_path(@upload) %> |
<%= link_to 'Back', uploads_path %> |
<%= link_to 'Download', download_path(id: @upload) %>
PROBLEM: what is not going well is that all the files get 0 bytes in /public/uploads
dir.
and Failed to load image example:
and prove that all files are zero bytes:
NOTICE: binary should work correctly, because it is possible to dwnl each of the files with send_data
:
def download
send_data(@upload.content,
filename: @upload.filename,
content_type: @upload.content_type,
)
end
=========================================== Question: what is not OK with the app and how to fix it to display image or images in view? Thanks in advance for any help!
Upvotes: 1
Views: 1084
Reputation: 236
It looks like you're mixing up two different methods of storing files:
content
column in your DB)/public/uploads/
)Your implementation does a little bit of both: You're definitely saving the file as binary in the content
column of the uploads
table, and you're saving something to the filesystem. You need to pick one or the other.
If you want to use Method 2 (Filesystem), it's gonna take a bit more work to get it going. If this is what you really want to do, let me know and I can edit my answer with a solution.
However, you're already pretty much there with Method 1 (Database). We just need to put the pieces together in a slightly different way.
You mentioned that send_data
was working for you—that's exactly what we're going to use to fix your problem.
First, add a method to UploadsController
def show_image
@upload = Upload.find(params[:id])
send_data @upload.content, type: @upload.content_type, disposition: 'inline'
end
Next, add a corresponding route to routes.rb
get 'show_image', to: 'uploads#show_image'
And finally, in your view, /uploads/show.html.erb
, swap out your image_tag
for this one
<%= image_tag url_for(controller: "uploads", action: "show_image", id: @upload.id) %>
And there we go. That should be enough for it to work.
We basically defined a controller action to grab the binary blob from the database, which then uses send_data
with the 'inline'
disposition to return the file to your view with the correct MIME type so that the link_tag
in your view knows what to do with it.
You'll notice that you can delete almost everything in the create
method of your UploadsController
. All you need is the standard boilerplate code:
def create
@upload = Upload.new(upload_params) # <-- All you need
respond_to do |format|
...
end
You can also remove the /public/uploads
directory because you're not using it. The file is stored completely in the database with this method and will not appear in your filesystem.
Hope this was helpful! Let me know if this works for you.
Note about Method 2 (Filesystem):
I'm guessing you're doing this as a learning experience, which is great, but if you're ever trying to store uploaded files in the filesystem on a "real" app, you really should use a gem to handle it. Paperclip make things really easy and is a useful tool even in production applications where you're ultimately storing files somewhere else like S3, so it's definitely not a waste to learn it.
Okay, so it turns out that your code for the filesystem work was actually pretty close to working. The main problem was that in the file=(upload_data)
method of upload.rb
, you called upload_data.read
. Then, in UploadsController#create
, you call f.write(data.read)
. The important thing to note here is that data
and upload_data
are the same exact file stream. Why is that important?
Well, the thing about read
in Ruby (and most other file-reading methods) is that there is hidden mutable state that keeps track of the current "position" in the contents of the file stream. (This is useful because most file-reading is done in batches, like foreach
which reads files one line at a time.) So what file.read
really does is that it "reads the rest of the file". If your "position" is at the beginning, then "the rest of the file" is the entire file. But if you're halfway through, then "the rest" is just the second half. Because of this, the "current position" always end up at the very end of the file when it's done read
-ing, meaning that (unless we "rewind") any future calls to read
will return the empty string because there is no more "rest of the file". Example:
myfile = File.open(some_path_to_file, 'r')
myfile.read
# => "lorem ipsum..." # (file contents from beginning)
myfile.read
# => "" # (empty string)
myfile.rewind
# => 0 (resets 'position' to position 0, the beginning)
myfile.read
# => "lorem ipsum..." # (file contents from beginning)
(Okay, so that's enough information for you to crack the problem if you want to give it a try before reading the answer. If not, or when you're ready, read on...)
Solution
In upload.rb
, make the following change file=(upload_data)
def file=(upload_data)
self.filename = upload_data.original_filename
self.content_type = upload_data.content_type
self.content = upload_data.read # <-- Either Delete this line, OR
upload_data.rewind # <-- Add this line
end
Obviously in a real app, we'd delete the self.content
line because we're trying to implement Method 2, using the filesystem, but since this is a test app, you can totally have both working at the same time. To do that, just add the upload_data.rewind
line at the very end.
So now the create
method should be successfully adding the files to your /public/uploads
directory (they won't be empty anymore) so check to make sure that's working. Next, we just have to make sure the views are set up properly.
In show.rb
the image tag should look like this
<%= image_tag "/uploads/#{@upload.filename}" %>
You had /public/uploads/...
instead of /uploads/...
. For security reasons, image_tag
can only look inside /public
, so you don't have to specify '/public' in the path—it will get added automatically.
And, we're done! Let me know if it works for you.
More
There are a few more things you probably should do. Like modify the create
action slightly:
File.open(Rails.root.join('public', 'uploads', data.original_filename), 'wb') do |f|
f.write(data.read)
f.close # <-- add this line. You should always explicitly close file streams after you open them.
end
#### something else here
Also, instead of hard-coding the paths like we did, store the path in the database in a file_path
column and then we can just pass @upload.file_path
to our image_tag
.
rails g migration AddFilePathToUploads file_path:string
rake db:migrate
And then make sure our create
action manually sets the path before saving. So plug the following in instead of "something else here"
@upload.file_path = ['/uploads', data.original_filename].join '/'
Then change the image tag in show.rb
<%= image_tag @upload.file_path %>
Alright, now we're done. I just want to make the recommendation once again to check out the Paperclip or CarrierWave gems, since they're what you'll likely be using for any real project.
I hope you learned something and that this was helpful to you. I had fun with it too. Let me know when you get it working!
Cheers
Upvotes: 1
Reputation: 2102
Try this, i think this should help.
original_filename = data.original_filename.to_s
tmp = data.tempfile
file = File.join("public/uploads", original_filename)
FileUtils.cp tmp.path, file
Upvotes: 1