3.2.1. Conditional save to file system

You only want to save the file to the filesystem if the rest of the record is valid and gets saved too.

Use the after_save callback to handle copying the uploaded file only after the record has been validate and saved.

The model
class Content < ActiveRecord::Base
  def file=(incomming_file)
    @temp_file = incomming_file
    @filename = incomming_file.original_filename
    @content_type = incomming_file.content_type
  end

  def after_save
    if @temp_file
      logger.debug("saving '#{RAILS_ROOT}/media/#{@filename}'")
      File.open("#{RAILS_ROOT}/media/#{@filename}", "wb") do |f| 
        f.write(@temp_file.read)
      end
    end
  end
 end
The view
<h1>New </h1>

<%= error_messages_for 'content' %>
<!--[form:content]-->
<%= start_form_tag( {:action => 'create'}, :multipart => true) %>
    <%= hidden_field 'content', 'id' %>
    <p><label for="content_name">Content</label><br /><%= text_field 'content', 'name'  %></p>
    <input type="file" id="content_file"  name="content[file]"/><br />
    <input type="submit" value="Create" />
<%= end_form_tag %>
<!--[eoform:content]-->

<%= link_to 'Back', :action => 'list' %>
the controller
class ContentsController < ApplicationController
  ...
  def create
    @content = Content.new(@params['content'])
    if @content.save
      flash['notice'] = 'Content was successfully created.'
      redirect_to :action => 'list'
    else
      render_action 'new'
    end
  end
  ...
end

The model uses a setter method(file=) to collect the name and content-type of the uploaded file in the same manner seen ealier in this chapter.

set aside

The setter also assign the uploaded file to a class variable, @temp_file.
@temp_file = incomming_file

@temp_file is used as a temporary holding place where we’ll keep the uploaded file until the model is ready to move the file to its real destination. @temp_file is not a field in our table.

tell them to call back

The code that actually copies our tempfile to it’s desired location is inside a callback method, after_save. You get one guess as to when that method gets run.

If the record is not valid, it won’t get saved and this method will not run. That means we are not saving any orphaned files.

The only logic in after_save is a lone if statement.
    if @temp_file
It’s there because not every record save involves a new file. It ensures we only try to copy if a file is there.

disconnected?

One of the freebies we get when using any of the callbacks is that we’re inside a transaction block, which means that if something goes astray any uncommited changes to the DB will get rolled back and we won’t be left with half a record in the database.

cleaning up after ourselves

Since we’re so concerned with leaving poor orphaned files behind, we should also consider the other end of the models lifespan, when it gets destroyed. We don’t want to leave files around that don’t belong to any records.

Well surprise surprise, another callback to the rescue:
class Content < ActiveRecord::Base
  ...
  def after_destroy
    File.delete("#{RAILS_ROOT}/media/#{@filename}") if File.exist?("#{RAILS_ROOT}/media/#{@filename}")
  end
end

What’s missing

  • validation
    • filesize (too large?, to small?)
    • content-type (images only?, just PDFs?)
  • tests
    • does the destination directory even exists?
    • is it writeable?