user3467734
user3467734

Reputation:

Rails 4.2.x - zero size uploaded files

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 view renders like this: enter image description here

index view: enter image description here

and Failed to load image example: enter image description here

and prove that all files are zero bytes: enter image description here

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

+ enter image description here

=========================================== 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

Answers (2)

Sherlock_HJ
Sherlock_HJ

Reputation: 236

It looks like you're mixing up two different methods of storing files:

  1. Storing as a Binary Blob in your Database (ex: the content column in your DB)
  2. Saving the file to the filesystem (ex: /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.

Method 1: Storing Binary Blob in Database

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.

Method 2 – Storing File in Filesystem

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

Pardeep Saini
Pardeep Saini

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

Related Questions