3.2.1. Conditional save to file system
3.1 Problem
You only want to save the file to the filesystem if the rest of the record is valid and gets saved too.
3.2 Solution
Use the after_save callback to handle copying the uploaded file only after the record has been validate and saved.
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
3.3 Discussion
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 inafter_save is a lone if statement.
if @temp_fileIt’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?