Discussion:
[ANN] file_upload 0.1
(too old to reply)
Sebastian Kanthak
2005-08-10 19:17:59 UTC
Permalink
Hi,

I've extracted code for handling file uploads and storing the file in a
model from one of my projects. I thought it might be useful to people,
as it avoids repeating yourself and has some nice features (especially
handling form-redisplays gracefully).

Here's a short list of features implemented so far:

Let's assume an model class named Entry, where we want to define the
"image" column
as a "file_upload" column. You can just call the handy "file_column" method:

require_dependency 'file_column'
class Entry < ActiveRecord::Base
include FileColumn

file_column :image
end

* every entry can have one uploaded file, the filename will be stored in
the "image" column

* Entry#image will return an absolute path to the uploaded file

* Entry#image= will handle uploaded files, so that you do not need
special code in your
controller

* files will be stored in "public/entry/image/#{entry.id}/filename.ext"

* Newly uploaded files will be stored in
"public/entry/tmp/<random>/filename.ext" so that
they can be reused in form redisplays (due to validation etc.)

* in a view, "<%= file_column_tag "entry", "image" %> will create a file
upload field as well
as a hidden field to recover files uploaded before in a case of a form
redisplay

* in a view "<%= url_for_file_column "entry", "image" %> will create an
URL to access the
uploaded file

For more information look at the documentation in the included files. If
people find this interesting, I can put up a web-site for this to
publish subsequent releases.

Any feedback is welcome!

Sebastian
Duane Johnson
2005-08-10 20:34:22 UTC
Permalink
Post by Sebastian Kanthak
Hi,
I've extracted code for handling file uploads and storing the file
in a model from one of my projects. I thought it might be useful to
people, as it avoids repeating yourself and has some nice features
(especially handling form-redisplays gracefully).
Wow Sebastian! Thanks for this excellent work. I'm eager to try it
out soon.

Duane Johnson
(canadaduane)
Simen Brekken
2005-08-10 20:59:45 UTC
Permalink
I think everyone who has worked with sites with images/video/flash files
have had problems and wanted something like this.

As discussed in a similar thread, this is definetly something that shoud
get into Rails itself, file uploads need to be as easy as the rest of rails.

SIMEN BREKKEN / born to synthesize.
Post by Duane Johnson
Hi,
I've extracted code for handling file uploads and storing the file in
a model from one of my projects. I thought it might be useful to
people, as it avoids repeating yourself and has some nice features
(especially handling form-redisplays gracefully).
Wow Sebastian! Thanks for this excellent work. I'm eager to try it
out soon.
Duane Johnson
(canadaduane)
Ezra Zygmuntowicz
2005-08-10 21:15:26 UTC
Permalink
This is awesome Sebastian! Thanks for writing this. I think file
uploads cause a lot of problems for people. I took me forever to get
it right but having this module will make things much easier. I would
definitely recommend putting this up on the rails wiki. Maybe submit
it as a patch as well. I will get a lot of use out of this module.

Thanks Again-
-Ezra
Post by Sebastian Kanthak
Hi,
I've extracted code for handling file uploads and storing the file
in a model from one of my projects. I thought it might be useful to
people, as it avoids repeating yourself and has some nice features
(especially handling form-redisplays gracefully).
Let's assume an model class named Entry, where we want to define
the "image" column
as a "file_upload" column. You can just call the handy
require_dependency 'file_column'
class Entry < ActiveRecord::Base
include FileColumn
file_column :image
end
* every entry can have one uploaded file, the filename will be
stored in the "image" column
* Entry#image will return an absolute path to the uploaded file
* Entry#image= will handle uploaded files, so that you do not need
special code in your
controller
* files will be stored in "public/entry/image/#{entry.id}/
filename.ext"
* Newly uploaded files will be stored in "public/entry/tmp/<random>/
filename.ext" so that
they can be reused in form redisplays (due to validation etc.)
* in a view, "<%= file_column_tag "entry", "image" %> will create a
file upload field as well
as a hidden field to recover files uploaded before in a case of a
form redisplay
* in a view "<%= url_for_file_column "entry", "image" %> will
create an URL to access the
uploaded file
For more information look at the documentation in the included
files. If people find this interesting, I can put up a web-site for
this to publish subsequent releases.
Any feedback is welcome!
Sebastian
require 'fileutils'
require 'tempfile'
module FileColumn
def self.append_features(base)
super
base.extend(ClassMethods)
end
# The FileColumn module allows you to easily handle file
uploads. You can designate
# one or more columns of your model's table as "file columns"
#
# class Entry < ActiveRecord::Base
# file_column :image
# end
#
# Now, by default, an uploaded file "test.png" for an entry
object with primary key 42 will
# be stored in in "public/entry/image/42/test.png". The
filename "test.png" will be stored
# in the record's +image+ column.
#
# == Generated Methods
#
# After calling "<tt>file_column :image</tt>" as in the example
above, a number of instance methods
#
# * <tt>Entry#image=(uploaded_file)</tt>: this will handle a
newly uploaded file (see below). Note that
# you can simply call your upload field "entry[image]" in
your view (or use the helper).
# * <tt>Entry#image</tt>: This will return an absolute path (as
a string) to the currently uploaded file
# or nil if no file has been uploaded
# * <tt>Entry#image_relative_path</tt>: This will return a path
relative to this file column's base
# directory
# as a string or nil if no file has been uploaded. This would
be "42/test.png" in the example.
# * <tt>Entry#image_just_uploaded?</tt>: Returns true if a new
file has been uploaded to this instance.
# You can use this in <tt>before_validation</tt> to resize
images on newly uploaded files, for example.
#
# == Storage of uploaded file
#
# For a model class +Entry+ and a column +image+, all files
will be stored under
# "public/entry/image". A sub-directory named after the primary
key of the object will
# be created, so that files can be stored using their real
filename. For example, a file
# "test.png" stored in an Entry object with id 42 will be
stored in
#
# public/entry/image/42/test.png
#
# Files will be moved to this location in an +after_save+
callback. They will be stored in
# a temporary location previously as explained in the next
section.
#
# == Handling of form redisplay
#
# Suppose you have a form for creating a new object where the
user can upload an image. The form may
# have to be re-displayed because of validation errors. The
uploaded file has to be stored somewhere so
# that the user does not have to upload it again. FileColumn
will store these in a temporary directory
# (called "tmp" and located under the column's base directory
by default) so that it can be moved to
# the final location if the object is successfully created. If
the form is never completed, though, you
# can easily remove all the images in this "tmp" directory once
per day or so.
#
# So in the example above, the image "test.png" would first be
stored in
# "public/entry/image/tmp/<some_random_key>/test.png" and be
moved to
# "public/entry/image/<primary_key>/test.png".
#
# This temporary location of newly uploaded files has another
advantage when updating objects. If the
# update fails for some reasons (e.g. due to validations), the
existing image will not be overwritten, so
# it has a kind of "transactional behaviour".
module ClassMethods
DEFAULT_OPTIONS = {
"root_path" => File.join(RAILS_ROOT, "public"),
"web_root" => ""
}.freeze
# handle one or more attributes as "file-upload" columns,
generating additional methods as explained
# above. You should pass the names of the attributes as
#
# file_column :image, :another_image
def file_column(*args)
options = DEFAULT_OPTIONS.dup
options.update(args.pop) if args.last.is_a?(Hash)
options["base_path"] ||= File.join(options
["root_path"], Inflector.underscore(self.name).to_s)
options["base_url"] ||= options["web_root"]
+"/"+Inflector.underscore(self.name).to_s+"/"
for attr in args
store_dir = File.join(options["base_path"], attr.to_s)
tmp_base_dir = File.join(store_dir, "tmp")
FileUtils.mkpath([store_dir,tmp_base_dir])
column_attr = attr.to_s
column_read_method = attr.to_sym
column_write_method = (attr.to_s+"=").to_sym
read_temp_method = "#{attr}_temp".to_sym
write_temp_method = "#{attr}_temp=".to_sym
column_relative_path_method = (attr.to_s
+"_relative_path").to_sym
column_options_method = "#{attr}_options".to_sym
just_uploaded_method = "#{attr}_just_uploaded?".to_sym
# symbols for callback methods
column_after_save_method = (attr.to_s
+"_after_save").to_sym
column_after_destroy_method = (attr.to_s
+"_after_destroy").to_sym
_just_uploaded".to_sym
define_method column_read_method do
relative_path = self.send
column_relative_path_method
return nil unless relative_path
File.join(store_dir, relative_path)
end
define_method column_relative_path_method do
filename = read_attribute column_attr
return nil unless filename
tmp_dir = instance_variable_get tmp_dir_attribute
if tmp_dir
File.join("tmp",tmp_dir,filename)
else
File.join(self.id.to_s,filename)
end
end
define_method column_write_method do |file|
if file.nil? and read_attribute(column_attr)
if (tmp_dir = instance_variable_get
tmp_dir_attribute)
# delete temporary image immediately
FileColumn.remove_file_with_dir
(File.join(tmp_base_dir,tmp_dir,
read_attribute(column_attr)))
remove_instance_variable tmp_dir_attribute
end
write_attribute column_attr, nil
end
return nil unless file and file.size > 0
tmp_dir = FileColumn.generate_temp_name
FileUtils.mkdir(File.join(tmp_base_dir, tmp_dir))
filename = FileColumn::sanitize_filename
(file.original_filename)
local_file_path = File.join
(tmp_base_dir,tmp_dir,filename)
# stored uploaded file into local_file_path
# If it was a Tempfile object, the temporary
file will be
# cleaned up automatically, so we do not have
to care for this
if file.respond_to?(:local_path) and
file.local_path and File.exists?(file.local_path)
FileUtils.copy_file(file.local_path,
local_file_path)
elsif file.respond_to?(:read)
File.open(local_file_path, "w") { |f|
f.write(file.read) }
else
raise ArgumentError.new("Do not know how to
handle #{file.inspect}")
end
# if there already was an old temporary file,
remove it
if (old_tmp_dir = instance_variable_get
tmp_dir_attribute)
FileColumn.remove_file_with_dir(File.join
(tmp_base_dir,old_tmp_dir,
read_attribute(column_attr)))
end
instance_variable_set tmp_dir_attribute, tmp_dir
write_attribute column_attr, filename
instance_variable_set just_uploaded_attribute,
true
end
define_method read_temp_method do
tmp_dir = instance_variable_get tmp_dir_attribute
return nil unless tmp_dir
File.join(tmp_dir, read_attribute(column_attr))
end
define_method write_temp_method do |temp_path|
return nil if temp_path == ""
raise ArgumentError.new("invalid format of '#
{temp_path}'") unless temp_path =~ %r{^((\d+\.)+\d+)/([^/].+)$}
tmp_dir, filename = $1,
FileColumn.sanitize_filename($3)
if instance_variable_get(tmp_dir_attribute).nil?
instance_variable_set tmp_dir_attribute,
tmp_dir
write_attribute column_attr, filename
else
# if tmp_dir_attribute is already set we
have already uploaded
# a new file via column=, which takes
precedence over the old
# temporary image. However, we can clean up
the old image right now
FileColumn.remove_file_with_dir(File.join
(tmp_base_dir,tmp_dir,filename))
end
end
define_method column_after_save_method do
if instance_variable_get tmp_dir_attribute
# we have a newly uploaded image, move it
to the correct location
# create a directory named after the
primary key, first
dir = File.join(store_dir,self.id.to_s)
FileUtils.mkdir(dir) unless File.exists?(dir)
# move the temporary file over
local_path = self.send(column_read_method)
FileUtils.mv local_path, dir
# remove all old files in the directory
# we do this _after_ moving the file to
avoid a short period of
# time where none of the two files is
available
filename = File.basename(local_path)
FileUtils.rm(
Dir.entries(dir).reject!{ |e|
[".",".."].include?(e) or e == filename }.
collect{ |e| File.join(dir,e) }
)
# cleanup temporary file
Dir.rmdir(File.dirname(local_path))
remove_instance_variable tmp_dir_attribute
elsif read_attribute(column_attr).nil?
# we do not have a file stored anymore,
make sure
# to remove it from disk if needed
FileUtils.rm_rf File.join(store_dir,
self.id.to_s)
end
end
after_save column_after_save_method
define_method column_after_destroy_method do
local_path = self.send(column_read_method)
FileColumn.remove_file_with_dir(local_path) if
local_path
end
after_destroy column_after_destroy_method
define_method just_uploaded_method do
instance_variable_get just_uploaded_attribute
end
define_method column_options_method do
options
end
private column_after_save_method,
column_after_destroy_method
end
end
end
private
def self.generate_temp_name
now = Time.now
"#{now.to_i}.#{now.usec}.#{Process.pid}"
end
def self.sanitize_filename(filename)
filename = File.basename(filename.gsub("\\", "/")) # work-
around for IE
filename.gsub(/[^a-zA-Z0-9\.\-\+_]/,"_")
filename = "_#{filename}" if filename =~ /^\.+$/
filename
end
def self.remove_file_with_dir(path)
FileUtils.rm_f path
dir = File.dirname(path)
Dir.rmdir(dir) if File.exists?(dir)
end
end
# This module contains helper methods for displaying and uploading
files
# for attributes created by +FileColumn+'s +file_column+ method.
module FileColumnHelper
# Use this helper to create an upload field for a file_column
attribute. This will generate
# an additional hidden field to keep uploaded files during form-
redisplays. For example,
# when called with
#
# <%= file_column_field("entry", "image") %>
#
# the following HTML will be generated (assuming the form is
redisplayed and something has
#
# <input type="hidden" name="entry[image_temp]" value="..." />
# <input type="file" name="entry[image]" />
#
# You can use the +option+ argument to pass additional options
to the file-field tag.
def file_column_field(object, method, options={})
result = ActionView::Helpers::InstanceTag.new(object,
method.to_s+"_temp", self).to_input_field_tag("hidden", {})
result << ActionView::Helpers::InstanceTag.new(object,
method, self).to_input_field_tag("file", options)
end
# Creates an URL where an uploaded file can be accessed. When
called for an Entry object with
# id 42 like this
#
# <%= url_for_file_column("entry", "image")
#
# the follwoing URL will be produced, assuming the file
"test.png" has been stored in
#
# /entry/image/42/test.png
#
# This will produce a valid URL even for temporary uploaded
files, e.g. files where the object
# they are belonging to has not been saved in the database yet.
def url_for_file_column(object_name, method)
url << object.send("#{method}_options")["base_url"]
url << "#{method}/"
url << object.send("#{method}_relative_path")
end
end
_______________________________________________
Rails mailing list
http://lists.rubyonrails.org/mailman/listinfo/rails
-Ezra Zygmuntowicz
Yakima Herald-Republic
WebMaster
509-577-7732
ezra-gdxLOakOTQ9oetBuM9ipNAC/***@public.gmane.org
Demetrius Nunes
2005-08-11 14:01:58 UTC
Permalink
Please, do turn this into a Rails patch. It's really useful.

Thanks a lot!

rgds
Dema
--
http://dema.ruby.com.br - Rails from a .NET perspective
Rick Olson
2005-08-11 14:18:19 UTC
Permalink
Post by Demetrius Nunes
Please, do turn this into a Rails patch. It's really useful.
I haven't looked closely at the File Upload module (it's not something
I need at the moment), but I have a tip for building rails extensions.
I have one available at
http://collaboa.techno-weenie.net/repository/browse/sentry/lib. You
can either drop that directory into your rails_app/lib directory or
install it as a gem. Either way, require 'my_lib' does the requiring,
and you can use it like it was apart of rails. Then one day if you
submit it for rails, you could take out the require line and be on
your way.

The code that allows this is at the bottom of sentry.rb:

begin
require 'active_record/sentry' # require your lib
ActiveRecord::Base.class_eval do
include ActiveRecord::Sentry # add your module's new methods
end
rescue NameError
nil # hmm, ActiveRecord is not available...
end

I've tried to structure my lib after other common ruby and rails
libraries I've come across.
--
rick
http://techno-weenie.net
rails-1W37MKcQCpIf0INCOvqR/
2005-08-11 15:33:39 UTC
Permalink
Well, that was just the trick. file_upload 0.1.1 is now in production
on my application. Works fine, except for the part below.

Thanks to Sebastian for FileUpload, and Rick for the code below.

Bye !
François
Post by Rick Olson
begin
require 'active_record/sentry' # require your lib
ActiveRecord::Base.class_eval do
include ActiveRecord::Sentry # add your module's new methods
end
rescue NameError
nil # hmm, ActiveRecord is not available...
end
Sebastian Kanthak
2005-08-13 08:51:38 UTC
Permalink
Post by Rick Olson
Post by Demetrius Nunes
Please, do turn this into a Rails patch. It's really useful.
I haven't looked closely at the File Upload module (it's not something
I need at the moment), but I have a tip for building rails extensions.
thank's for the tip. I did a minor 0.1.2 release that provides this kind
of extension. You just have to "require 'rails_file_column'" in your
"environment.rb" and file-column's method will be available in all
models and views without including the file_column modules explicitly.

Regarding turning this into a rails patch: I guess it should become a
little bit more mature (has anyone tested this on windows?) and get a
few more features (look at the TODO) file. I'm not sure, what the plans
for 1.0 are and if this can still get included. If some rails people say
"yes", I'll submit it as a patch as soon as possible.

Sebastian
François Beausoleil
2005-08-15 17:48:36 UTC
Permalink
Sebastian, as I said on 2005-08-11, yes I am using it in a production
environment. Also, I'm on Windows, but my production environment is on
DreamHost.

No changes were necessary to make FileColumn work on Windows. Good job !

Bye !
François
Post by Sebastian Kanthak
little bit more mature (has anyone tested this on windows?) and get a
Sebastian Kanthak
2005-08-16 09:47:36 UTC
Permalink
Post by François Beausoleil
Sebastian, as I said on 2005-08-11, yes I am using it in a production
environment. Also, I'm on Windows, but my production environment is
on DreamHost.
No changes were necessary to make FileColumn work on Windows. Good job !
wow, I'm surprised! Are you using the "url_for_file_column" helper
method in your view? I suspect it might return URLs containing a "\"
because I'm using File.join and this should emit backslashes on Windows,
which is okay for file paths but not for URLs. Could you give this a try
for me?

Sebastian
François Beausoleil
2005-08-18 13:39:32 UTC
Permalink
Hello Sebastian !
Post by Sebastian Kanthak
Post by François Beausoleil
No changes were necessary to make FileColumn work on Windows. Good job !
wow, I'm surprised! Are you using the "url_for_file_column" helper
method in your view? I suspect it might return URLs containing a "\"
because I'm using File.join and this should emit backslashes on Windows,
which is okay for file paths but not for URLs. Could you give this a try
for me?
Hi !

Nope, the returned URL is quite correct - forward slashes all the way.

The only problem I had was if there were no picture, I would get a
"cannot convert nil into String". I emit my img tag using this:
<%= image_tag(url_for_file_column('estimate', 'picture'), :class =>
'standalone house') %>

I had to guard against that nil by doing:
<% unless @estimate.picture.blank? -%>
...
<% end -%>

Thanks for your hard work.

Bye !
François
Sebastian Kanthak
2005-08-19 08:56:13 UTC
Permalink
Post by François Beausoleil
Post by Sebastian Kanthak
wow, I'm surprised! Are you using the "url_for_file_column" helper
method in your view? I suspect it might return URLs containing a "\"
because I'm using File.join and this should emit backslashes on Windows,
which is okay for file paths but not for URLs. Could you give this a try
for me?
Nope, the returned URL is quite correct - forward slashes all the way.
okay, it turns out File::SEPERATOR is "/" on windows as well and ruby
handles this correctly when opening files. I should probably not
depend on this, though.
Post by François Beausoleil
The only problem I had was if there were no picture, I would get a
<%= image_tag(url_for_file_column('estimate', 'picture'), :class =>
'standalone house') %>
...
<% end -%>
you could write this a little bit shorter like this:

<%= image_tag(url_for_file_column('estimate', 'picture'), :class =>
'standalone house') if @estimate.picture %>

I don't think you can avoid this guard even if url_for_file_column
returned an empty string or so, because you'd still have the image_tag
pointing to an empty string in your HTML.

BTW, I've released version 0.1.3 which contains some bug-fixes (see
CHANGELOG for details).

Sebastian
Keith Bingman
2005-08-22 08:10:12 UTC
Permalink
I have been trying to use file_column 0.1.3 on a Mac (10.4.2 using
Webrick), but somehow it is not working. It sets up the tmp folder,
but never copies the file into it. On Safari, the forms completes and
says "entry successfully updated", but does not copy the file name
into the database. Firefox returns an error: "undefined method
`original_filename' for 'q-software3.jpg':String".

I am doing something very wrong, though what, I am not sure. Then, I
don't really have much I idea about rails and am much the newbie...

Any help appreciated.

Keith Bingman
Sebastian Kanthak
2005-08-22 08:30:55 UTC
Permalink
Post by Keith Bingman
I have been trying to use file_column 0.1.3 on a Mac (10.4.2 using
Webrick), but somehow it is not working. It sets up the tmp folder,
but never copies the file into it. On Safari, the forms completes and
says "entry successfully updated", but does not copy the file name
into the database. Firefox returns an error: "undefined method
`original_filename' for 'q-software3.jpg':String".
do you set the attribute "enctype" attribute to "multipart/form-data"
in your form tag? If you use the form tag helper you can do it like
this:

<%= form_tag { :action => "foo" }, :multipart => true %>

Another thing you might want to check are the permissions of the
folders created. Do they give the user the web-server is running as
write access?

Sebastian
Keith Bingman
2005-08-22 13:10:38 UTC
Permalink
Post by Sebastian Kanthak
do you set the attribute "enctype" attribute to "multipart/form-data"
in your form tag? If you use the form tag helper you can do it like
<%= form_tag { :action => "foo" }, :multipart => true %>
This was the answer I needed. Thanks!

Keith

Stephen Caudill
2005-08-15 23:59:00 UTC
Permalink
Post by Sebastian Kanthak
thank's for the tip. I did a minor 0.1.2 release that provides this kind
of extension. You just have to "require 'rails_file_column'" in your
"environment.rb" and file-column's method will be available in all
models and views without including the file_column modules explicitly.
Regarding turning this into a rails patch: I guess it should become a
little bit more mature (has anyone tested this on windows?) and get a
few more features (look at the TODO) file. I'm not sure, what the plans
for 1.0 are and if this can still get included. If some rails people say
"yes", I'll submit it as a patch as soon as possible.
Sebastian,

Have you set up web space where you're tracking your progress with
this? You mention a release, but I haven't seen any indication of
where (or if) it's available. BIG thanks for the contribution, this
has been one of my biggest stumbling points so far.

Thanks,
Stephen
Sebastian Kanthak
2005-08-16 09:41:37 UTC
Permalink
Post by Stephen Caudill
Have you set up web space where you're tracking your progress with
this? You mention a release, but I haven't seen any indication of
where (or if) it's available. BIG thanks for the contribution, this
has been one of my biggest stumbling points so far.
I'm glad you like it. You can find releases at

http://www.kanthak.net/opensource/file_column/

Cheers
Sebastian
Continue reading on narkive:
Loading...