module Ramaze module Helper ## # Helper module for handling file uploads. File uploads are mostly handled # by Rack, but this helper adds some conveniance methods for handling # and saving the uploaded files. # # @example # class MyController < Ramaze::Controller # # Use upload helper # helper :upload # # # This action will handle *all* uploaded files # def handleupload1 # # Iterate over uploaded files and save them in the # # '/uploads/myapp' directory # get_uploaded_files.each_pair do |k, v| # v.save( # File.join('/uploads/myapp', v.filename), # :allow_overwrite => true # ) # # if v.saved? # Ramaze::Log.info( # "Saved uploaded file named #{k} to #{v.path}." # ) # else # Ramaze::Log.warn("Failed to save file named #{k}.") # end # end # end # # # This action will handle uploaded files beginning with 'up' # def handleupload2 # # Iterate over uploaded files and save them in the # # '/uploads/myapp' directory # get_uploaded_files(/^up.*/).each_pair do |k, v| # v.save( # File.join('/uploads/myapp', v.filename), # :allow_overwrite => true # ) # # if v.saved? # Ramaze::Log.info( # "Saved uploaded file named #{k} to #{v.path}." # ) # else # Ramaze::Log.warn("Failed to save file named #{k}.") # end # end # end # end # # @author Lars Olsson # @since 04-08-2011 # module Upload include Ramaze::Traited ## # This method will iterate through all request parameters and convert # those parameters which represents uploaded files to # Ramaze::Helper::Upload::UploadedFile objects. The matched parameters # will then be removed from the request parameter hash. # # Use this method if you want to decide whether to handle file uploads # in your action at runtime. For automatic handling, use # Ramaze::Helper::Upload::ClassMethods#handle_all_uploads or # Ramaze::Helper::Upload::ClassMethods#handle_uploads_for instead. # # @author Lars Olsson # @since 04-08-2011 # @param [Regexp] pattern If set, only those request parameters which # has a name matching the Regexp will be checked for file uploads. # @return [Array] The uploaded files. # @see Ramaze::Helper::Upload::ClassMethods#handle_all_uploads # @see Ramaze::Helper::Upload::ClassMethods#handle_uploads_for # def get_uploaded_files(pattern = nil) uploaded_files = {} # Iterate over all request parameters request.params.each_pair do |k, v| # If we use a pattern, check that it matches if pattern.nil? or pattern =~ k # Rack supports request parameters with either a single value or # an array of values. To support both, we need to check if the # current parameter is an array or not. if v.is_a?(Array) # Got an array. Iterate through it and check for uploaded files file_indices = [] v.each_with_index do |elem, idx| file_indices.push(idx) if is_uploaded_file?(elem) end # Convert found uploaded files to # Ramaze::Helper::Upload::UploadedFile objects file_elems = [] file_indices.each do |fi| file_elems << Ramaze::Helper::Upload::UploadedFile.new( v[fi][:filename], v[fi][:type], v[fi][:tempfile], ancestral_trait[:upload_options] || Ramaze::Helper::Upload::ClassMethods.trait[ :default_upload_options ] ) end # Remove uploaded files from current request param file_indices.reverse_each do |fi| v.delete_at(fi) end # If the request parameter contained at least one file upload, # add upload(s) to the list of uploaded files uploaded_files[k] = file_elems unless file_elems.empty? # Delete parameter from request parameter array if it doesn't # contain any other elements. request.params.delete(k) if v.empty? else # Got a single value. Check if it is an uploaded file if is_uploaded_file?(v) # The current parameter represents an uploaded file. # Convert the parameter to a # Ramaze::Helper::Upload::UploadedFile object uploaded_files[k] = Ramaze::Helper::Upload::UploadedFile.new( v[:filename], v[:type], v[:tempfile], ancestral_trait[:upload_options] || Ramaze::Helper::Upload::ClassMethods.trait[ :default_upload_options ] ) # Delete parameter from request parameter array request.params.delete(k) end end end end # If at least one file upload matched, override the uploaded_files # method with a singleton method that returns the list of uploaded # files. Doing things this way allows us to store the list of uploaded # files without using an instance variable. unless uploaded_files.empty? @_ramaze_uploaded_files = uploaded_files # Save uploaded files if autosave is set to true if ancestral_trait[:upload_options] and ancestral_trait[:upload_options][:autosave] uploaded_files().each_value do |uf| uf.save end end end # The () is required, otherwise the name would collide with the variable # "uploaded_files". return uploaded_files() end ## # Adds some class method to the controller whenever the helper # is included. # def self.included(mod) mod.extend(ClassMethods) end ## # Returns list of currently handled file uploads. # # Both single and array parameters are supported. If you give # your file upload fields the same name (for instance upload[]) Rack will # merge them into a single parameter. The upload helper will keep this # structure so that whenever the request parameter contains an array, the # uploaded_files method will also return an array of # Ramaze::Helper::Upload::UploadedFile objects for the same key. # # @return [Hash] Currently uploaded files. The keys in the hash # corresponds to the names of the request parameters that contained file # uploads and the values consist of Ramaze::Helper::Upload::UploadedFile # objects. # def uploaded_files return @_ramaze_uploaded_files || {} end private # Returns whether +param+ is considered an uploaded file # A parameter is considered to be an uploaded file if it is # a hash and contains all parameters that Rack assigns to an # uploaded file # # @param [Hash] param A request parameter # @return [Boolean] def is_uploaded_file?(param) if param.respond_to?(:has_key?) [:filename, :type, :name, :tempfile, :head].each do |k| return false if !param.has_key?(k) end return true else return false end end # Helper class methods. Methods in this module will be available # in your controller *class* (not your controller instance). module ClassMethods include Ramaze::Traited # Default options for uploaded files. You can change these options # by using the uploads_options method trait :default_upload_options => { :allow_overwrite => false, :autosave => false, :default_upload_dir => nil, :unlink_tempfile => false }.freeze ## # This method will activate automatic handling of uploaded files # for specified actions in the controller. # # @example # class MyController < Ramaze::Controller # # # Use upload helper # helper :upload # # # Handle all uploads for the foo and bar actions # handle_uploads_for :foo, :bar # # # Handle all uploads for the baz action and uploads beginning with # # 'up' for the qux action # handle_uploads_for :baz, [:qux, /^up.*/] # end # # @param [Array] args An arbitrary long list of arguments with action # names (and optionally patterns) that should handle file uploads # automatically. Each argument can either be a symbol or a two-element # array consisting of a symbol and a reqexp. # @see #handle_all_uploads # @see Ramaze::Helper::Upload#get_uploaded_files def handle_uploads_for(*args) args.each do |arg| if arg.respond_to?(:first) and arg.respond_to?(:last) before(arg.first.to_sym) do get_uploaded_files(arg.last) end else before(arg.to_sym) do get_uploaded_files end end end end ## # Sets options for file uploads in the controller. # # @example # # This controller will handle all file uploads automatically. # # All uploaded files are saved automatically in '/uploads/myapp' # # and old files are overwritten. # # # class MyController < Ramaze::Controller # # # Use upload helper # helper :upload # # handle_all_uploads # upload_options :allow_overwrite => true, # :autosave => true, # :default_upload_dir => '/uploads/myapp', # :unlink_tempfile => true # end # # # This controller will handle all file uploads automatically. # # All uploaded files are saved automatically, but the exact location # # is depending on a session variable. Old files are overwritten. # # # class MyController2 < Ramaze::Controller # # # Use upload helper # helper :upload # # # Proc to use for save directory calculation # calculate_dir = lambda { File.join('/uploads', session['user']) } # # handle_all_uploads # upload_options :allow_overwrite => true, # :autosave => true, # :default_upload_dir => calculate_dir, # :unlink_tempfile => true # end # @param [Hash] options Options controlling how file uploads # are handled. # @option options [Boolean] :allow_overwrite If set to *true*, uploaded # files are allowed to overwrite existing ones. This option is set to # *false* by default. # @option options [Boolean] :autosave If set to *true*, # Ramaze::Helper::Upload::UploadedFile#save will be called on all # matched file uploads # automatically. You can use this option to automatically save files # at a preset location, but please note that you will need to set the # :default_upload_dir (and possibly :allow_overwrite) options as well # in order for this to work correctly. This option is set to *false* # by default. # @option options [String|Proc] :default_upload_dir If set to a string # (representing a path in the file system) this option will allow you # to save uploaded files without specifying a path. If you intend to # call Ramaze::Helper::Upload::UploadedFile#save with a path you don't # need to set this option at all. If you need to delay the calculation # of the directory, you can also set this option to a proc. The proc # should accept zero arguments and return a string. This comes in handy # when you want to use different directory paths for different users # etc. This option is set to *nil* by default. # @option options [Boolean] :unlink_tempfile If set to *true*, this # option will automatically unlink the temporary file created by Rack # immediately after Ramaze::Helper::Upload::UploadedFile#save is done # saving the uploaded file. This is probably not needed in most cases, # but if you don't want to expose your uploaded files in a shared # tempdir longer than necessary this option might be for you. This # option is set to *false* by default. # @see Ramaze::Helper::Upload::UploadedFile#initialize # @see Ramaze::Helper::Upload::UploadedFile#save # def upload_options(options) trait( :upload_options => Ramaze::Helper::Upload::ClassMethods.trait[ :default_upload_options ].merge(options) ) end end # ClassMethods ## # This class represents an uploaded file. # # @author Lars Olsson # @since 18-08-2011 # class UploadedFile include Ramaze::Traited # Suggested file name # @return [String] attr_reader :filename # MIME-type # @return [String] attr_reader :type ## # Initializes a new Ramaze::Helper::Upload::UploadedFile object. # # @param [String] filename Suggested file name # @param [String] type MIME-type # @param [File] tempfile temporary file # @param [Hash] options Options for uploaded files. Options supported # match those available to # Ramaze::Helper::Upload::ClassMethods#upload_options # @return [Ramaze::Helper::Upload::UploadedFile] A new # Ramaze::Helper::Upload::UploadedFile object # @see #save # @see Ramaze::Helper::Upload::ClassMethods#upload_options def initialize(filename, type, tempfile, options) @filename = File.basename(filename) @type = type @tempfile = tempfile @realfile = nil trait :options => options end ## # Changes the suggested filename of this # Ramaze::Helper::Upload::UploadedFile. +name+ should be a string # representing the filename (only the filename, not a complete path), # but if you provide a complete path this method it will try to identify # the filename and use that instead. # # @param [String] name The new suggested filename. # def filename=(name) @filename = File.basename(name) end ## # Returns the path of the Ramaze::Helper::Upload::UploadedFile object. # The method will always return *nil* before *save* has been called # on the Ramaze::Helper::Upload::UploadedFile object. # # @return [String|nil] # def path return self.saved? ? @realfile.path : nil end ## # Saves the Ramaze::Helper::Upload::UploadedFile. # # If +path+ is not set, the method checks whether there exists default # options for the path and tries to use that instead. # # If you need to override any options set in the controller (using # upload_options) you can set the corresponding option in +options+ to # override the behavior for this particular # Ramaze::Helper::Upload::UploadedFile object. # # @param [String] path Path where the # Ramaze::Helper::Upload::UploadedFile will be saved # @param [Hash] options Options for uploaded files. Options supported # match those available to # Ramaze::Helper::Upload::ClassMethods#upload_options # @raise [StandardError] Will be raised if the save operation fails. # @see #initialize # @see Ramaze::Helper::Upload::ClassMethods#upload_options # def save(path = nil, options = {}) # Merge options opts = trait[:options].merge(options) unless path # No path was provided, use info stored elsewhere to try to build # the path unless opts[:default_upload_dir] raise StandardError.new('Unable to save file, no dirname given') end unless @filename raise StandardError.new('Unable to save file, no filename given') end # Check to see if a proc or a string was used for the # default_upload_dir parameter. If it was a proc, call the proc and # use the result as the directory part of the path. If a string was # used, use the string directly as the directory part of the path. dn = opts[:default_upload_dir] if dn.respond_to?(:call) dn = dn.call end path = File.join(dn, @filename) end path = File.expand_path(path) # Abort if file altready exists and overwrites are not allowed if File.exists?(path) and !opts[:allow_overwrite] raise StandardError.new('Unable to overwrite existing file') end # Confirm that we can read source file unless File.readable?(@tempfile.path) raise StandardError.new('Unable to read temporary file') end # Confirm that we can write to the destination file unless (File.exists?(path) and File.writable?(path)) \ or (File.exists?(File.dirname(path)) \ and File.writable?(File.dirname(path))) raise StandardError.new( "Unable to save file to #{path}. Path is not writable" ) end # If supported, use IO,copy_stream. If not, require fileutils # and use the same method from there if IO.respond_to?(:copy_stream) IO.copy_stream(@tempfile, path) else require 'fileutils' File.open(@tempfile.path, 'rb') do |src| File.open(path, 'wb') do |dest| FileUtils.copy_stream(src, dest) end end end # Update the realfile property, indicating that the file has been # saved @realfile = File.new(path) # But no need to keep it open @realfile.close # If the unlink_tempfile option is set to true, delete the temporary # file created by Rack unlink_tempfile if opts[:unlink_tempfile] end ## # Returns whether the Ramaze::Helper::Upload::UploadedFile has been # saved or not. # # @return [Boolean] # def saved? return !@realfile.nil? end ## # Deletes the temporary file associated with this # Ramaze::Helper::Upload::UploadedFile immediately. # def unlink_tempfile File.unlink(@tempfile.path) @tempfile = nil end end # UploadedFile end # Upload end # Helper end # Ramaze