diff --git a/lib/sinatra.rb b/lib/sinatra.rb index 84bf694f..7bf29287 100644 --- a/lib/sinatra.rb +++ b/lib/sinatra.rb @@ -2,6 +2,7 @@ require 'rubygems' require 'metaid' require 'uri' require 'thread' +require 'time' if ENV['SWIFT'] require 'swiftcore/swiftiplied_mongrel' @@ -251,6 +252,11 @@ module Sinatra # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. # Defaults to 4096. # * :status - specifies the status code to send with the response. Defaults to '200 OK'. + # * :last_modified - an optional RFC 2616 formatted date value (See Time#httpdate) + # indicating the last modified time of the file. If the request includes an + # If-Modified-Since header that matches this value exactly, a 304 Not Modified response + # is sent instead of the file. Defaults to the file's last modified + # time. # # The default Content-Type and Content-Disposition headers are # set to download arbitrary binary files in as many browsers as @@ -283,6 +289,7 @@ module Sinatra options[:length] ||= File.size(path) options[:filename] ||= File.basename(path) options[:type] ||= Rack::File::MIME_TYPES[File.extname(options[:filename])[1..-1]] || 'text/plain' + options[:last_modified] ||= File.mtime(path).httpdate send_file_headers! options if options[:stream] @@ -302,6 +309,10 @@ module Sinatra # * :disposition - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * :status - specifies the status code to send with the response. Defaults to '200 OK'. + # * :last_modified - an optional RFC 2616 formatted date value (See Time#httpdate) + # indicating the last modified time of the response entity. If the request includes an + # If-Modified-Since header that matches this value exactly, a 304 Not Modified response + # is sent instead of the data. # # Generic data download: # send_data buffer @@ -325,6 +336,13 @@ module Sinatra raise ArgumentError, ":#{arg} option required" if options[arg].nil? end + # Send a "304 Not Modified" if the last_modified option is provided and matches + # the If-Modified-Since request header value. + if last_modified = options[:last_modified] + header 'Last-Modified' => last_modified + throw :halt, [ 304, '' ] if last_modified == request.env['HTTP_IF_MODIFIED_SINCE'] + end + disposition = options[:disposition].dup || 'attachment' disposition <<= %(; filename="#{options[:filename]}") if options[:filename] diff --git a/test/streaming_test.rb b/test/streaming_test.rb index 325eb645..197ac1b5 100644 --- a/test/streaming_test.rb +++ b/test/streaming_test.rb @@ -54,6 +54,22 @@ context "Static files (by default)" do body.should.equal "\n" end + specify "include a Last-Modified header" do + last_modified = File.mtime(Sinatra.application.options.public + '/foo.xml') + get_it('/foo.xml') + should.be.ok + body.should.not.be.empty + headers['Last-Modified'].should.equal last_modified.httpdate + end + + specify "are not served when If-Modified-Since matches" do + last_modified = File.mtime(Sinatra.application.options.public + '/foo.xml') + @request = Rack::MockRequest.new(Sinatra.application) + @response = @request.get('/foo.xml', 'HTTP_IF_MODIFIED_SINCE' => last_modified.httpdate) + status.should.equal 304 + body.should.be.empty + end + end context "SendData" do