From da0c4c5c9695e2ebe8d98b391d901b598dd293a2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 22 May 2005 08:58:43 +0000 Subject: Deprecated all render_* methods in favor of consolidating all rendering behavior in Base#render(options). This enables more natural use of combining options, such as layouts git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1350 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/lib/action_controller.rb | 3 + actionpack/lib/action_controller/base.rb | 261 +++------------------ actionpack/lib/action_controller/benchmarking.rb | 6 +- actionpack/lib/action_controller/cookies.rb | 22 +- .../deprecated_renders_and_redirects.rb | 68 ++++++ actionpack/lib/action_controller/layout.rb | 21 +- actionpack/lib/action_controller/streaming.rb | 133 +++++++++++ 7 files changed, 261 insertions(+), 253 deletions(-) create mode 100644 actionpack/lib/action_controller/deprecated_renders_and_redirects.rb create mode 100644 actionpack/lib/action_controller/streaming.rb (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 517f740f14..70722a0e73 100755 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -32,6 +32,7 @@ rescue LoadError end require 'action_controller/base' +require 'action_controller/deprecated_renders_and_redirects' require 'action_controller/rescue' require 'action_controller/benchmarking' require 'action_controller/filters' @@ -47,6 +48,7 @@ require 'action_controller/cgi_process' require 'action_controller/caching' require 'action_controller/components' require 'action_controller/verification' +require 'action_controller/streaming' require 'action_view' ActionController::Base.template_class = ActionView::Base @@ -66,4 +68,5 @@ ActionController::Base.class_eval do include ActionController::Caching include ActionController::Components include ActionController::Verification + include ActionController::Streaming end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c449d33c22..e128b1434e 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -205,13 +205,6 @@ module ActionController #:nodoc: DEFAULT_RENDER_STATUS_CODE = "200 OK" - DEFAULT_SEND_FILE_OPTIONS = { - :type => 'application/octet-stream'.freeze, - :disposition => 'attachment'.freeze, - :stream => true, - :buffer_size => 4096 - }.freeze - # Determines whether the view has access to controller internals @request, @response, @session, and @template. # By default, it does. @@view_controller_internals = true @@ -439,236 +432,74 @@ module ActionController #:nodoc: end protected + # Renders the template specified by template_name, which defaults to the name of the current controller and action. + # So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or + # "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is + # shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK", + # but can be changed, such as by calling render("weblog/error", "500 Error"). + # A unified replacement for the individual renders (work-in-progress). - def r(options = {}, &block) + def render(options = {}, deprecated_status = nil) raise DoubleRenderError, "Can only render or redirect once per action" if performed? + + # Backwards compatibility + return render({ :template => options || default_template_name, :status => deprecated_status }) if !options.is_a?(Hash) + add_variables_to_assigns options[:status] = (options[:status] || DEFAULT_RENDER_STATUS_CODE).to_s if options[:text] @response.headers["Status"] = options[:status] - @response.body = block_given? ? block : options[:text] + @response.body = options[:text] @performed_render = true return options[:text] elsif options[:file] assert_existance_of_template_file(options[:file]) if options[:use_full_path] logger.info("Rendering #{options[:file]} (#{options[:status]})") unless logger.nil? - r(options.merge({ :text => @template.render_file(options[:file], options[:use_full_path])})) + render(options.merge({ :text => @template.render_file(options[:file], options[:use_full_path])})) elsif options[:template] - r(options.merge({ :file => options[:template], :use_full_path => true })) + render(options.merge({ :file => options[:template], :use_full_path => true })) elsif options[:inline] - r(options.merge({ :text => @template.render_template(options[:type] || :rhtml, options[:inline]) })) + render(options.merge({ :text => @template.render_template(options[:type] || :rhtml, options[:inline]) })) elsif options[:action] - r(options.merge({ :template => default_template_name(options[:action]) })) + render(options.merge({ :template => default_template_name(options[:action]) })) elsif options[:partial] && options[:collection] - r(options.merge({ + render(options.merge({ :text => ( @template.render_partial_collection( - options[:partial], options[:collection], options[:spacer_template], options[:local_assigns] + options[:partial], options[:collection], options[:spacer_template], options[:locals] ) || '' ) })) elsif options[:partial] - r(options.merge({ :text => @template.render_partial(options[:partial], options[:object], options[:local_assigns]) })) + render(options.merge({ :text => @template.render_partial(options[:partial], options[:object], options[:locals]) })) elsif options[:nothing] - r(options.merge({ :text => "" })) + render(options.merge({ :text => "" })) else - r(options.merge({ :template => default_template_name })) + render(options.merge({ :template => default_template_name })) end end - - # Renders the template specified by template_name, which defaults to the name of the current controller and action. - # So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or - # "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is - # shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK", - # but can be changed, such as by calling render("weblog/error", "500 Error"). - def render(template_name = nil, status = nil) #:doc: - render_file(template_name || default_template_name, status, true) - end - - # Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling - # render_action "show_many" in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or - # "#{template_root}/weblog/show_many.rxml". - def render_action(action_name, status = nil) #:doc: - render(default_template_name(action_name), status) - end - - # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be - # used like render_file "/Users/david/Code/Ruby/template" to render "/Users/david/Code/Ruby/template.rhtml" or - # "/Users/david/Code/Ruby/template.rxml". - def render_file(template_path, status = nil, use_full_path = false) #:doc: - assert_existance_of_template_file(template_path) if use_full_path - logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil? - - add_variables_to_assigns - render_text(@template.render_file(template_path, use_full_path), status) - end - - # Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So - # you'd call render_template "Hello, <%= @user.name %>" to greet the current user. Or if you want to render as Builder - # template, you could do render_template "xml.h1 @user.name", nil, "rxml". - def render_template(template, status = nil, type = "rhtml") #:doc: - add_variables_to_assigns - render_text(@template.render_template(type, template), status) - end - - # Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's - # considerably faster than rendering through the template engine. - # Use block for response body if provided (useful for deferred rendering or streaming output). - def render_text(text = nil, status = nil, &block) #:doc: - raise DoubleRenderError, "Can only render or redirect once per action" if performed? - add_variables_to_assigns - @response.headers["Status"] = (status || DEFAULT_RENDER_STATUS_CODE).to_s - @response.body = block_given? ? block : text - @performed_render = true - end - - # Renders an empty response that can be used when the request is only interested in triggering an effect. Do note that good - # HTTP manners mandate that you don't use GET requests to trigger data changes. - def render_nothing(status = nil) #:doc: - render_text "", status - end # Returns the result of the render as a string. - def render_to_string(template_name = default_template_name) #:doc: - add_variables_to_assigns - @template.render_file(template_name) + def render_to_string(options) #:doc: + result = render(options) + erase_render_results + return result end - + # Clears the rendered results, allowing for another render or redirect to be performed. def erase_render_results #:nodoc: @response.body = nil @performed_render = false end - - # Renders the partial specified by partial_path, which by default is the name of the action itself. Example: - # - # class WeblogController < ActionController::Base - # def show - # render_partial # renders "weblog/_show.r(xml|html)" - # end - # end - def render_partial(partial_path = default_template_name, object = nil, local_assigns = {}) #:doc: - add_variables_to_assigns - render_text(@template.render_partial(partial_path, object, local_assigns)) - end - - # Renders a collection of partials using partial_name to iterate over the +collection+. - def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})#:doc: - add_variables_to_assigns - render_text(@template.render_collection_of_partials(partial_name, collection, partial_spacer_template, local_assigns) || '') - end - - # Sends the file by streaming it 4096 bytes at a time. This way the - # whole file doesn't need to be read into memory at once. This makes - # it feasible to send even large files. - # - # Be careful to sanitize the path parameter if it coming from a web - # page. send_file(@params['path']) allows a malicious user to - # download any file on your server. - # - # Options: - # * :filename - suggests a filename for the browser to use. - # Defaults to File.basename(path). - # * :type - specifies an HTTP content type. - # Defaults to 'application/octet-stream'. - # * :disposition - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # * :streaming - whether to send the file to the user agent as it is read (true) - # or to read the entire file before sending (false). Defaults to true. - # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. - # Defaults to 4096. - # - # The default Content-Type and Content-Disposition headers are - # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have - # a variety of quirks (especially when downloading over SSL). - # - # Simple download: - # send_file '/path/to.zip' - # - # Show a JPEG in browser: - # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' - # - # Read about the other Content-* HTTP headers if you'd like to - # provide the user with more information (such as Content-Description). - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 - # - # Also be aware that the document may be cached by proxies and browsers. - # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See - # http://www.mnot.net/cache_docs/ for an overview of web caching and - # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 - # for the Cache-Control header spec. - def send_file(path, options = {}) #:doc: - raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) - - options[:length] ||= File.size(path) - options[:filename] ||= File.basename(path) - send_file_headers! options - - @performed_render = false - - if options[:stream] - render_text do - logger.info "Streaming file #{path}" unless logger.nil? - len = options[:buffer_size] || 4096 - File.open(path, 'rb') do |file| - if $stdout.respond_to?(:syswrite) - begin - while true - $stdout.syswrite file.sysread(len) - end - rescue EOFError - end - else - while buf = file.read(len) - $stdout.write buf - end - end - end - end - else - logger.info "Sending file #{path}" unless logger.nil? - File.open(path, 'rb') { |file| render_text file.read } - end - end - - # Send binary data to the user as a file download. May set content type, apparent file name, - # and specify whether to show data inline or download as an attachment. - # - # Options: - # * :filename - Suggests a filename for the browser to use. - # * :type - specifies an HTTP content type. - # Defaults to 'application/octet-stream'. - # * :disposition - specifies whether the file will be shown inline or downloaded. - # Valid values are 'inline' and 'attachment' (default). - # - # Generic data download: - # send_data buffer - # - # Download a dynamically-generated tarball: - # send_data generate_tgz('dir'), :filename => 'dir.tgz' - # - # Display an image Active Record in the browser: - # send_data image.data, :type => image.content_type, :disposition => 'inline' - # - # See +send_file+ for more information on HTTP Content-* headers and caching. - def send_data(data, options = {}) #:doc: - logger.info "Sending data #{options[:filename]}" unless logger.nil? - send_file_headers! options.merge(:length => data.size) - @performed_render = false - render_text data - end def rewrite_options(options) if defaults = default_url_options(options) @@ -724,19 +555,6 @@ module ActionController #:nodoc: end end end - - # Deprecated in favor of calling redirect_to directly with the path. - def redirect_to_path(path) #:doc: - redirect_to(path) - end - - # Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass - # true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through - # just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to. - def redirect_to_url(url, permanently = false) #:doc: - headers["Status"] = "301 Moved Permanently" if permanently - redirect_to(url) - end # Resets the session by clearing out all the objects stored within and initializing a new session object. def reset_session #:doc: @@ -745,11 +563,6 @@ module ActionController #:nodoc: @response.session = @session end - # Deprecated cookie writer method - def cookie(*options) - @response.headers["cookie"] << CGI::Cookie.new(*options) - end - private def initialize_template_class(response) begin @@ -800,7 +613,8 @@ module ActionController #:nodoc: def action_methods @action_methods ||= (self.class.public_instance_methods - self.class.hidden_actions) end - + + def add_variables_to_assigns add_instance_variables_to_assigns add_class_variables_to_assigns if view_controller_internals @@ -828,6 +642,7 @@ module ActionController #:nodoc: end end + def request_origin "#{@request.remote_ip} at #{Time.now.to_s}" end @@ -835,6 +650,7 @@ module ActionController #:nodoc: def close_session @session.close unless @session.nil? || Hash === @session end + def template_exists?(template_name = default_template_name) @template.file_exists?(template_name) @@ -852,23 +668,6 @@ module ActionController #:nodoc: end end - def send_file_headers!(options) - options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) - [:length, :type, :disposition].each do |arg| - raise ArgumentError, ":#{arg} option required" if options[arg].nil? - end - - disposition = options[:disposition].dup || 'attachment' - disposition <<= %(; filename="#{options[:filename]}") if options[:filename] - - @headers.update( - 'Content-Length' => options[:length], - 'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers - 'Content-Disposition' => disposition, - 'Content-Transfer-Encoding' => 'binary' - ); - end - def default_template_name(default_action_name = action_name) "#{self.class.controller_path}/#{default_action_name}" end diff --git a/actionpack/lib/action_controller/benchmarking.rb b/actionpack/lib/action_controller/benchmarking.rb index 25d9f50e74..6a78ad5dac 100644 --- a/actionpack/lib/action_controller/benchmarking.rb +++ b/actionpack/lib/action_controller/benchmarking.rb @@ -15,12 +15,12 @@ module ActionController #:nodoc: } end - def render_with_benchmark(template_name = default_template_name, status = "200 OK") + def render_with_benchmark(options = {}, deprecated_status = nil) if logger.nil? - render_without_benchmark(template_name, status) + render_without_benchmark(options, deprecated_status) else db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? - @rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real + @rendering_runtime = Benchmark::measure{ render_without_benchmark(options, deprecated_status) }.real if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected? @db_rt_before_render = db_runtime diff --git a/actionpack/lib/action_controller/cookies.rb b/actionpack/lib/action_controller/cookies.rb index 81f176ad32..ac96ed0e0c 100644 --- a/actionpack/lib/action_controller/cookies.rb +++ b/actionpack/lib/action_controller/cookies.rb @@ -3,17 +3,17 @@ module ActionController #:nodoc: # the cookies being written is what will be sent out will the response. Cookies are read by value (so you won't get the cookie object # itself back -- just the value it holds). Examples for writing: # - # cookies["user_name"] = "david" # => Will set a simple session cookie - # cookies["login"] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour + # cookies[:user_name] = "david" # => Will set a simple session cookie + # cookies[:login] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour # # Examples for reading: # - # cookies["user_name"] # => "david" + # cookies[:user_name] # => "david" # cookies.size # => 2 # # Example for deleting: # - # cookies.delete "user_name" + # cookies.delete :user_name # # All the option symbols for setting cookies are: # @@ -24,10 +24,16 @@ module ActionController #:nodoc: # * secure - whether this cookie is a secure cookie or not (default to false). # Secure cookies are only transmitted to HTTPS servers. module Cookies - # Returns the cookie container, which operates as described above. - def cookies - CookieJar.new(self) - end + protected + # Returns the cookie container, which operates as described above. + def cookies + CookieJar.new(self) + end + + # Deprecated cookie writer method + def cookie(*options) + @response.headers["cookie"] << CGI::Cookie.new(*options) + end end class CookieJar < Hash #:nodoc: diff --git a/actionpack/lib/action_controller/deprecated_renders_and_redirects.rb b/actionpack/lib/action_controller/deprecated_renders_and_redirects.rb new file mode 100644 index 0000000000..a511656c1e --- /dev/null +++ b/actionpack/lib/action_controller/deprecated_renders_and_redirects.rb @@ -0,0 +1,68 @@ +module ActionController + class Base + protected + # Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling + # render_action "show_many" in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or + # "#{template_root}/weblog/show_many.rxml". + def render_action(action_name, status = nil) #:doc: + render :action => action_name, :status => status + end + + # Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be + # used like render_file "/Users/david/Code/Ruby/template" to render "/Users/david/Code/Ruby/template.rhtml" or + # "/Users/david/Code/Ruby/template.rxml". + def render_file(template_path, status = nil, use_full_path = false) #:doc: + render :file => template_path, :status => status, :use_full_path => use_full_path + end + + # Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So + # you'd call render_template "Hello, <%= @user.name %>" to greet the current user. Or if you want to render as Builder + # template, you could do render_template "xml.h1 @user.name", nil, "rxml". + def render_template(template, status = nil, type = "rhtml") #:doc: + render :inline => template, :status => status, :type => type + end + + # Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's + # considerably faster than rendering through the template engine. + # Use block for response body if provided (useful for deferred rendering or streaming output). + def render_text(text = nil, status = nil) #:doc: + render(:text => text, :status => status) { yield } + end + + # Renders an empty response that can be used when the request is only interested in triggering an effect. Do note that good + # HTTP manners mandate that you don't use GET requests to trigger data changes. + def render_nothing(status = nil) #:doc: + render :nothing => true, :status => status + end + + # Renders the partial specified by partial_path, which by default is the name of the action itself. Example: + # + # class WeblogController < ActionController::Base + # def show + # render_partial # renders "weblog/_show.r(xml|html)" + # end + # end + def render_partial(partial_path = default_template_name, object = nil, local_assigns = {}) #:doc: + render :partial => partial_path, :object => object, :locals => local_assigns + end + + # Renders a collection of partials using partial_name to iterate over the +collection+. + def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})#:doc: + render :partial => partial_name, :collection => collection, :spacer_template => partial_spacer_template, :locals => local_assigns + end + + + # Deprecated in favor of calling redirect_to directly with the path. + def redirect_to_path(path) #:doc: + redirect_to(path) + end + + # Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass + # true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through + # just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to. + def redirect_to_url(url, permanently = false) #:doc: + headers["Status"] = "301 Moved Permanently" if permanently + redirect_to(url) + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb index 37dba36186..d6423fdbff 100644 --- a/actionpack/lib/action_controller/layout.rb +++ b/actionpack/lib/action_controller/layout.rb @@ -6,9 +6,6 @@ module ActionController #:nodoc: alias_method :render_without_layout, :render alias_method :render, :render_with_layout - alias_method :r_without_layout, :r - alias_method :r, :r_with_layout - class << self alias_method :inherited_without_layout, :inherited end @@ -205,7 +202,7 @@ module ActionController #:nodoc: active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout end - def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc: + def xrender_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc: if layout ||= active_layout and action_has_layout? add_variables_to_assigns logger.info("Rendering #{template_name} within #{layout}") unless logger.nil? @@ -216,23 +213,25 @@ module ActionController #:nodoc: end end - def r_with_layout(options = {}) - if (layout = active_layout_for_r(options)) && options[:text] + def render_with_layout(options = {}, deprecated_status = nil, deprecated_layout = nil) + if (layout = active_layout_for_r(options, deprecated_layout)) && options[:text] add_variables_to_assigns logger.info("Rendering #{template_name} within #{layout}") unless logger.nil? - @content_for_layout = r_without_layout(options) + @content_for_layout = render_without_layout(options) add_variables_to_assigns erase_render_results - r_without_layout(options.merge({ :text => @template.render_file(layout, true)})) + render_without_layout(options.merge({ :text => @template.render_file(layout, true), :status => options[:status] || deprecated_status })) else - r_without_layout(options) + render_without_layout(options, deprecated_status) end end private - def active_layout_for_r(options = {}) + def active_layout_for_r(options = {}, deprecated_layout = nil) + return deprecated_layout unless deprecated_layout.nil? + case options[:layout] when FalseClass nil @@ -244,7 +243,7 @@ module ActionController #:nodoc: end def action_has_layout? - conditions = self.class.layout_conditions + conditions = self.class.layout_conditions || {} case when conditions[:only] conditions[:only].include?(action_name) diff --git a/actionpack/lib/action_controller/streaming.rb b/actionpack/lib/action_controller/streaming.rb new file mode 100644 index 0000000000..8118fff1c9 --- /dev/null +++ b/actionpack/lib/action_controller/streaming.rb @@ -0,0 +1,133 @@ +module ActionController #:nodoc: + # Methods for sending files and streams to the browser instead of rendering. + module Streaming + DEFAULT_SEND_FILE_OPTIONS = { + :type => 'application/octet-stream'.freeze, + :disposition => 'attachment'.freeze, + :stream => true, + :buffer_size => 4096 + }.freeze + + protected + # Sends the file by streaming it 4096 bytes at a time. This way the + # whole file doesn't need to be read into memory at once. This makes + # it feasible to send even large files. + # + # Be careful to sanitize the path parameter if it coming from a web + # page. send_file(@params['path']) allows a malicious user to + # download any file on your server. + # + # Options: + # * :filename - suggests a filename for the browser to use. + # Defaults to File.basename(path). + # * :type - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # * :streaming - whether to send the file to the user agent as it is read (true) + # or to read the entire file before sending (false). Defaults to true. + # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. + # Defaults to 4096. + # + # The default Content-Type and Content-Disposition headers are + # set to download arbitrary binary files in as many browsers as + # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # a variety of quirks (especially when downloading over SSL). + # + # Simple download: + # send_file '/path/to.zip' + # + # Show a JPEG in browser: + # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' + # + # Read about the other Content-* HTTP headers if you'd like to + # provide the user with more information (such as Content-Description). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 + # + # Also be aware that the document may be cached by proxies and browsers. + # The Pragma and Cache-Control headers declare how the file may be cached + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See + # http://www.mnot.net/cache_docs/ for an overview of web caching and + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + # for the Cache-Control header spec. + def send_file(path, options = {}) #:doc: + raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) + + options[:length] ||= File.size(path) + options[:filename] ||= File.basename(path) + send_file_headers! options + + @performed_render = false + + if options[:stream] + render :text => Proc.new { + logger.info "Streaming file #{path}" unless logger.nil? + len = options[:buffer_size] || 4096 + File.open(path, 'rb') do |file| + if $stdout.respond_to?(:syswrite) + begin + while true + $stdout.syswrite file.sysread(len) + end + rescue EOFError + end + else + while buf = file.read(len) + $stdout.write buf + end + end + end + } + else + logger.info "Sending file #{path}" unless logger.nil? + File.open(path, 'rb') { |file| render :text => file.read } + end + end + + # Send binary data to the user as a file download. May set content type, apparent file name, + # and specify whether to show data inline or download as an attachment. + # + # Options: + # * :filename - Suggests a filename for the browser to use. + # * :type - specifies an HTTP content type. + # Defaults to 'application/octet-stream'. + # * :disposition - specifies whether the file will be shown inline or downloaded. + # Valid values are 'inline' and 'attachment' (default). + # + # Generic data download: + # send_data buffer + # + # Download a dynamically-generated tarball: + # send_data generate_tgz('dir'), :filename => 'dir.tgz' + # + # Display an image Active Record in the browser: + # send_data image.data, :type => image.content_type, :disposition => 'inline' + # + # See +send_file+ for more information on HTTP Content-* headers and caching. + def send_data(data, options = {}) #:doc: + logger.info "Sending data #{options[:filename]}" unless logger.nil? + send_file_headers! options.merge(:length => data.size) + @performed_render = false + render :text => data + end + + private + def send_file_headers!(options) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) + [:length, :type, :disposition].each do |arg| + raise ArgumentError, ":#{arg} option required" if options[arg].nil? + end + + disposition = options[:disposition].dup || 'attachment' + disposition <<= %(; filename="#{options[:filename]}") if options[:filename] + + @headers.update( + 'Content-Length' => options[:length], + 'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers + 'Content-Disposition' => disposition, + 'Content-Transfer-Encoding' => 'binary' + ); + end + end +end \ No newline at end of file -- cgit v1.2.3