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 _______________________________________________ Rails mailing list Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org http://lists.rubyonrails.org/mailman/listinfo/rails
On Aug 10, 2005, at 1:17 PM, Sebastian Kanthak wrote:> 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: >Wow Sebastian! Thanks for this excellent work. I''m eager to try it out soon. Duane Johnson (canadaduane)
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. Duane Johnson wrote:> > On Aug 10, 2005, at 1:17 PM, Sebastian Kanthak wrote: > >> 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: >> > Wow Sebastian! Thanks for this excellent work. I''m eager to try it > out soon. > > Duane Johnson > (canadaduane)
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 On Aug 10, 2005, at 12:17 PM, Sebastian Kanthak wrote:> 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 > > 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" > like this: > # > # 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 > # will automatically be generated, all prefixed by "image_": > # > # * <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 > symbols, like this: > # > # 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 > > tmp_dir_attribute = "@#{attr}_temp".to_sym > just_uploaded_attribute = "@#{attr} > _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 > # already been uploaded): > # > # <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 > # the "image"-column: > # > # /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) > object = instance_variable_get("@#{object_name.to_s}") > url = @request.relative_url_root.to_s > url << object.send("#{method}_options")["base_url"] > url << "#{method}/" > url << object.send("#{method}_relative_path") > end > end > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails >-Ezra Zygmuntowicz Yakima Herald-Republic WebMaster 509-577-7732 ezra-gdxLOakOTQ9oetBuM9ipNAC/G2K4zDHf@public.gmane.org
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
On 8/11/05, Demetrius Nunes <demetrius-fDpYTK8McCzCdMRJFJuMdgh0onu2mTI+@public.gmane.org> wrote:> 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/iCwEArCW2h5@public.gmane.org
2005-Aug-11 15:33 UTC
Re: Re: [ANN] file_upload 0.1
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 Rick Olson said the following on 2005-08-11 10:18:> 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
Rick Olson wrote:>On 8/11/05, Demetrius Nunes <demetrius-fDpYTK8McCzCdMRJFJuMdgh0onu2mTI+@public.gmane.org> wrote: > > >>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
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 Sebastian Kanthak said the following on 2005-08-13 04:51:> little bit more mature (has anyone tested this on windows?) and get a
On 8/13/05, Sebastian Kanthak <sebastian.kanthak-ZS8b95Whz3sUSW6y5lq3GQ@public.gmane.org> wrote:> 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
Stephen Caudill wrote:>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
François Beausoleil wrote:> 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
Hello Sebastian ! Sebastian Kanthak said the following on 2005-08-16 05:47:> François Beausoleil wrote: >> 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
On 8/18/05, François Beausoleil <fbeausoleil-IQIa899fVSs@public.gmane.org> wrote:> Sebastian Kanthak said the following on 2005-08-16 05:47: > > 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.> 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 -%>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
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
On 8/22/05, Keith Bingman <keith-TLlEnfWeLheZ/nFHiwjLOg@public.gmane.org> wrote:> 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
On Aug 22, 2005, at 10:30 AM, Sebastian Kanthak wrote:> 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 %>This was the answer I needed. Thanks! Keith _______________________________________________ Rails mailing list Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org http://lists.rubyonrails.org/mailman/listinfo/rails