~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/streaming.rb

  • Committer: Richard Lee (Canonical)
  • Date: 2010-10-15 15:17:58 UTC
  • mfrom: (190.1.3 use-case-mapper)
  • Revision ID: richard.lee@canonical.com-20101015151758-wcvmfxrexsongf9d
Merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
require 'active_support/core_ext/string/bytesize'
2
 
 
3
 
module ActionController #:nodoc:
4
 
  # Methods for sending arbitrary data and for streaming files to the browser,
5
 
  # instead of rendering.
6
 
  module Streaming
7
 
    DEFAULT_SEND_FILE_OPTIONS = {
8
 
      :type         => 'application/octet-stream'.freeze,
9
 
      :disposition  => 'attachment'.freeze,
10
 
      :stream       => true,
11
 
      :buffer_size  => 4096,
12
 
      :x_sendfile   => false
13
 
    }.freeze
14
 
 
15
 
    X_SENDFILE_HEADER = 'X-Sendfile'.freeze
16
 
 
17
 
    protected
18
 
      # Sends the file, by default streaming it 4096 bytes at a time. This way the
19
 
      # whole file doesn't need to be read into memory at once. This makes it
20
 
      # feasible to send even large files. You can optionally turn off streaming
21
 
      # and send the whole file at once.
22
 
      #
23
 
      # Be careful to sanitize the path parameter if it is coming from a web
24
 
      # page. <tt>send_file(params[:path])</tt> allows a malicious user to
25
 
      # download any file on your server.
26
 
      #
27
 
      # Options:
28
 
      # * <tt>:filename</tt> - suggests a filename for the browser to use.
29
 
      #   Defaults to <tt>File.basename(path)</tt>.
30
 
      # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
31
 
      #   either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
32
 
      # * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
33
 
      #   is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
34
 
      # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
35
 
      #   Valid values are 'inline' and 'attachment' (default).
36
 
      # * <tt>:stream</tt> - whether to send the file to the user agent as it is read (+true+)
37
 
      #   or to read the entire file before sending (+false+). Defaults to +true+.
38
 
      # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
39
 
      #   Defaults to 4096.
40
 
      # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
41
 
      # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
42
 
      #   the URL, which is necessary for i18n filenames on certain browsers
43
 
      #   (setting <tt>:filename</tt> overrides this option).
44
 
      # * <tt>:x_sendfile</tt> - uses X-Sendfile to send the file when set to +true+. This is currently
45
 
      #   only available with Lighttpd/Apache2 and specific modules installed and activated. Since this
46
 
      #   uses the web server to send the file, this may lower memory consumption on your server and
47
 
      #   it will not block your application for further requests.
48
 
      #   See http://blog.lighttpd.net/articles/2006/07/02/x-sendfile and
49
 
      #   http://tn123.ath.cx/mod_xsendfile/ for details. Defaults to +false+.
50
 
      #
51
 
      # The default Content-Type and Content-Disposition headers are
52
 
      # set to download arbitrary binary files in as many browsers as
53
 
      # possible.  IE versions 4, 5, 5.5, and 6 are all known to have
54
 
      # a variety of quirks (especially when downloading over SSL).
55
 
      #
56
 
      # Simple download:
57
 
      #
58
 
      #   send_file '/path/to.zip'
59
 
      #
60
 
      # Show a JPEG in the browser:
61
 
      #
62
 
      #   send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
63
 
      #
64
 
      # Show a 404 page in the browser:
65
 
      #
66
 
      #   send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404
67
 
      #
68
 
      # Read about the other Content-* HTTP headers if you'd like to
69
 
      # provide the user with more information (such as Content-Description) in
70
 
      # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
71
 
      #
72
 
      # Also be aware that the document may be cached by proxies and browsers.
73
 
      # The Pragma and Cache-Control headers declare how the file may be cached
74
 
      # by intermediaries.  They default to require clients to validate with
75
 
      # the server before releasing cached responses.  See
76
 
      # http://www.mnot.net/cache_docs/ for an overview of web caching and
77
 
      # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
78
 
      # for the Cache-Control header spec.
79
 
      def send_file(path, options = {}) #:doc:
80
 
        raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
81
 
 
82
 
        options[:length]   ||= File.size(path)
83
 
        options[:filename] ||= File.basename(path) unless options[:url_based_filename]
84
 
        send_file_headers! options
85
 
 
86
 
        @performed_render = false
87
 
 
88
 
        if options[:x_sendfile]
89
 
          logger.info "Sending #{X_SENDFILE_HEADER} header #{path}" if logger
90
 
          head options[:status], X_SENDFILE_HEADER => path
91
 
        else
92
 
          if options[:stream]
93
 
            render :status => options[:status], :text => Proc.new { |response, output|
94
 
              logger.info "Streaming file #{path}" unless logger.nil?
95
 
              len = options[:buffer_size] || 4096
96
 
              File.open(path, 'rb') do |file|
97
 
                while buf = file.read(len)
98
 
                  output.write(buf)
99
 
                end
100
 
              end
101
 
            }
102
 
          else
103
 
            logger.info "Sending file #{path}" unless logger.nil?
104
 
            File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
105
 
          end
106
 
        end
107
 
      end
108
 
 
109
 
      # Sends the given binary data to the browser. This method is similar to
110
 
      # <tt>render :text => data</tt>, but also allows you to specify whether
111
 
      # the browser should display the response as a file attachment (i.e. in a
112
 
      # download dialog) or as inline data. You may also set the content type,
113
 
      # the apparent file name, and other things.
114
 
      #
115
 
      # Options:
116
 
      # * <tt>:filename</tt> - suggests a filename for the browser to use.
117
 
      # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
118
 
      #   either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
119
 
      # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
120
 
      #   Valid values are 'inline' and 'attachment' (default).
121
 
      # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
122
 
      #
123
 
      # Generic data download:
124
 
      #
125
 
      #   send_data buffer
126
 
      #
127
 
      # Download a dynamically-generated tarball:
128
 
      #
129
 
      #   send_data generate_tgz('dir'), :filename => 'dir.tgz'
130
 
      #
131
 
      # Display an image Active Record in the browser:
132
 
      #
133
 
      #   send_data image.data, :type => image.content_type, :disposition => 'inline'
134
 
      #
135
 
      # See +send_file+ for more information on HTTP Content-* headers and caching.
136
 
      #
137
 
      # <b>Tip:</b> if you want to stream large amounts of on-the-fly generated
138
 
      # data to the browser, then use <tt>render :text => proc { ... }</tt>
139
 
      # instead. See ActionController::Base#render for more information.
140
 
      def send_data(data, options = {}) #:doc:
141
 
        logger.info "Sending data #{options[:filename]}" if logger
142
 
        send_file_headers! options.merge(:length => data.bytesize)
143
 
        @performed_render = false
144
 
        render :status => options[:status], :text => data
145
 
      end
146
 
 
147
 
    private
148
 
      def send_file_headers!(options)
149
 
        options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
150
 
        [:length, :type, :disposition].each do |arg|
151
 
          raise ArgumentError, ":#{arg} option required" if options[arg].nil?
152
 
        end
153
 
 
154
 
        disposition = options[:disposition].dup || 'attachment'
155
 
 
156
 
        disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
157
 
 
158
 
        content_type = options[:type]
159
 
        if content_type.is_a?(Symbol)
160
 
          raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.has_key?(content_type.to_s)
161
 
          content_type = Mime::Type.lookup_by_extension(content_type.to_s)
162
 
        end
163
 
        content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
164
 
 
165
 
        headers.merge!(
166
 
          'Content-Length'            => options[:length].to_s,
167
 
          'Content-Type'              => content_type,
168
 
          'Content-Disposition'       => disposition,
169
 
          'Content-Transfer-Encoding' => 'binary'
170
 
        )
171
 
 
172
 
        # Fix a problem with IE 6.0 on opening downloaded files:
173
 
        # If Cache-Control: no-cache is set (which Rails does by default),
174
 
        # IE removes the file it just downloaded from its cache immediately
175
 
        # after it displays the "open/save" dialog, which means that if you
176
 
        # hit "open" the file isn't there anymore when the application that
177
 
        # is called for handling the download is run, so let's workaround that
178
 
        headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache'
179
 
      end
180
 
  end
181
 
end