Skip to content
Browse files

Added simple if-modified-since / 304 Not Modified support

  • Loading branch information...
1 parent 11a170b commit 849c1e6896960255361d3bd4325f4965dce7f1e1 @andrewvc andrewvc committed with weavejester May 29, 2010
Showing with 74 additions and 13 deletions.
  1. +38 −7 ring-core/src/ring/middleware/file_info.clj
  2. +36 −6 ring-core/test/ring/middleware/file_info_test.clj
View
45 ring-core/src/ring/middleware/file_info.clj
@@ -1,7 +1,9 @@
(ns ring.middleware.file-info
"Augment Ring File responses."
(:use [clojure.contrib.def :only (defvar-)])
- (:import java.io.File))
+ (:import java.io.File)
+ (:import java.text.SimpleDateFormat)
+ (:import java.util.SimpleTimeZone))
(defvar- base-mime-types
{"ai" "application/postscript"
@@ -70,18 +72,47 @@
[#^File file mime-types]
(get mime-types (get-extension file) "application/octet-stream"))
+(defvar- http-date-formatter
+ ;"A SimpleDateFormat instance, set to format using RFC 822/1123"
+ (let [formatter (SimpleDateFormat. "EEE, dd MMM yyyy HH:mm:ss ZZZ")]
+ (do
+ ; We use GMT because it makes testing much easier
+ (.setTimeZone formatter (SimpleTimeZone. 0 "GMT"))
+ formatter)))
+
+(defn- http-date
+ "Takes a Date or Long, returns a String in HTTP Date (RFC 822/1123) format"
+ [date]
+ (.format http-date-formatter date))
+
(defn wrap-file-info
"Wrap an app such that responses with a file a body will have
- corresponding Content-Type and Content-Length headers added if they can be
- determined from the file.
+ corresponding Content-Type, Content-Length, and Last Modified headers added
+ if they can be determined from the file.
If two arguments are given, the second is taken to be a map of file extensions
- to content types that will supplement the default, built-in map."
+ to content types that will supplement the default, built-in map.
+ If the request specifies If-Modified-Since in its header, and it is a literal
+ match of the string returned as Last-Modified, a 304 with no body will be
+ sent instead."
[app & [custom-mime-types]]
(let [mime-types (merge base-mime-types custom-mime-types)]
(fn [req]
(let [{:keys [headers body] :as response} (app req)]
(if (instance? File body)
- (assoc response :headers
- (assoc headers "Content-Length" (str (.length #^File body))
- "Content-Type" (guess-mime-type body mime-types)))
+ (let [
+ file-size (str (.length #^File body))
+ content-type (guess-mime-type body mime-types)
+ server-lmodified (http-date (.lastModified body))
+ client-lmodified (get (:headers req) "if-modified-since")
+ ;it'd be nice to have a real date comparison at some point
+ not-modified (= client-lmodified server-lmodified)]
+ (if not-modified
+ (assoc response :status 304 :body "" :headers
+ (assoc headers "Content-Length" "0"
+ "Content-Type" content-type
+ "Last-Modified" server-lmodified))
+ (assoc response :headers
+ (assoc headers "Content-Length" file-size
+ "Content-Type" content-type
+ "Last-Modified" server-lmodified))))
response)))))
View
42 ring-core/test/ring/middleware/file_info_test.clj
@@ -11,6 +11,17 @@
(def unknown-file (File. "test/ring/assets/random.xyz"))
(def unknown-file-app (wrap-file-info (constantly {:headers {} :body unknown-file})))
+(defmacro with-custom-last-modified [file new-time form]
+ "Lets us use a known file modification time for tests, without permanently changing
+ the file's modification time"
+ `(let [old-time# (.lastModified ~file)]
+ (do
+ (.setLastModified ~file (* 1000 ~new-time));use seconds, not millis
+ (let [result# ~form]
+ (do
+ (.setLastModified ~file old-time#)
+ result#)))))
+
(def custom-type-app
(wrap-file-info
(constantly {:headers {} :body known-file})
@@ -20,16 +31,35 @@
(is (= {:headers {} :body "body"} (non-file-app {}))))
(deftest wrap-file-info-known-file-response
- (is (= {:headers {"Content-Type" "text/plain" "Content-Length" "6"}
- :body known-file}
- (known-file-app {}))))
+ (with-custom-last-modified known-file 1263506400
+ (is (= {:headers {"Content-Type" "text/plain" "Content-Length" "6"
+ "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"}
+ :body known-file}
+ (known-file-app {})))))
(deftest wrap-file-info-unknown-file-response
(is (= {:headers {"Content-Type" "application/octet-stream" "Content-Length" "7"}
:body unknown-file}
(unknown-file-app {}))))
(deftest wrap-file-info-custom-mime-types
- (is (= {:headers {"Content-Type" "custom/type" "Content-Length" "6"}
- :body known-file}
- (custom-type-app {}))))
+ (with-custom-last-modified known-file 0
+ (is (= {:headers {"Content-Type" "custom/type" "Content-Length" "6"
+ "Last-Modified" "Thu, 01 Jan 1970 00:00:00 +0000"}
+ :body known-file}
+ (custom-type-app {})))))
+
+(deftest wrap-file-info-if-modified-since-hit
+ (with-custom-last-modified known-file 1263506400
+ (is (= {:status 304
+ :headers {"Content-Type" "text/plain" "Content-Length" "0"
+ "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"}
+ :body ""}
+ (known-file-app {:headers {"if-modified-since" "Thu, 14 Jan 2010 22:00:00 +0000" }})))))
+
+(deftest wrap-file-info-if-modified-miss
+ (with-custom-last-modified known-file 1263506400
+ (is (= {:headers {"Content-Type" "text/plain" "Content-Length" "6"
+ "Last-Modified" "Thu, 14 Jan 2010 22:00:00 +0000"}
+ :body known-file}
+ (known-file-app {:headers {"if-modified-since" "Wed, 13 Jan 2010 22:00:00 +0000"}})))))

0 comments on commit 849c1e6

Please sign in to comment.
Something went wrong with that request. Please try again.