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