From fd0d7cc46c9b73f0f3b8dc6a3af715e0d63c3a41 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Sat, 9 May 2020 18:22:57 +0300 Subject: [PATCH] Add create-file-handler, fixes #395 --- CHANGELOG.md | 1 + modules/reitit-ring/src/reitit/ring.cljc | 97 +++++--- test/cljc/reitit/ring_test.cljc | 271 ++++++++++++----------- 3 files changed, 200 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b05c34a9..bad25715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ is called the first time, so that `rfe/push-state` and such can be called ### `reitit-ring` * `reitit.ring/routes` strips away `nil` routes, fixes [#394](https://github.com/metosin/reitit/issues/394) +* `reitit.ring/create-file-handler` to serve files from classpah, fixes [#395](https://github.com/metosin/reitit/issues/395) ## 0.4.2 (2020-01-17) diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index f919b40c..a9cd7d40 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -189,6 +189,48 @@ ;; TODO: ring.middleware.not-modified/wrap-not-modified ;; TODO: ring.middleware.head/wrap-head ;; TODO: handle etags + (defn -create-file-or-resource-handler + [response-fn {:keys [parameter root path loader allow-symlinks? index-files paths not-found-handler] + :or {parameter (keyword "") + root "public" + index-files ["index.html"] + paths (constantly nil) + not-found-handler (constantly {:status 404, :body "", :headers {}})}}] + (let [options {:root root + :loader loader + :index-files? false + :allow-symlinks? allow-symlinks?} + path-size (count path) + create (fn [handler] + (fn + ([request] (handler request)) + ([request respond _] (respond (handler request))))) + join-paths (fn [& paths] + (str/replace (str/replace (str/join "/" paths) #"([/]+)" "/") #"/$" "")) + response (fn [path] + (if-let [response (or (paths (join-paths "/" path)) + (response-fn path options))] + (response/content-type response (mime-type/ext-mime-type path)))) + path-or-index-response (fn [path uri] + (or (response path) + (loop [[file & files] index-files] + (if file + (if (response (join-paths path file)) + (response/redirect (join-paths uri file)) + (recur files)))))) + handler (if path + (fn [request] + (let [uri (:uri request)] + (if-let [path (if (>= (count uri) path-size) (subs uri path-size))] + (path-or-index-response path uri)))) + (fn [request] + (let [uri (:uri request) + path (-> request :path-params parameter)] + (or (path-or-index-response path uri) + (not-found-handler request)))))] + (create handler)))) + +#?(:clj (defn create-resource-handler "A ring handler for serving classpath resources, configured via options: @@ -202,42 +244,25 @@ | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)" ([] (create-resource-handler nil)) - ([{:keys [parameter root path loader allow-symlinks? index-files paths not-found-handler] - :or {parameter (keyword "") - root "public" - index-files ["index.html"] - paths (constantly nil) - not-found-handler (constantly {:status 404, :body "", :headers {}})}}] - (let [options {:root root, :loader loader, :allow-symlinks? allow-symlinks?} - path-size (count path) - create (fn [handler] - (fn - ([request] (handler request)) - ([request respond _] (respond (handler request))))) - join-paths (fn [& paths] - (str/replace (str/replace (str/join "/" paths) #"([/]+)" "/") #"/$" "")) - resource-response (fn [path] - (if-let [response (or (paths (join-paths "/" path)) - (response/resource-response path options))] - (response/content-type response (mime-type/ext-mime-type path)))) - path-or-index-response (fn [path uri] - (or (resource-response path) - (loop [[file & files] index-files] - (if file - (if (resource-response (join-paths path file)) - (response/redirect (join-paths uri file)) - (recur files)))))) - handler (if path - (fn [request] - (let [uri (:uri request)] - (if-let [path (if (>= (count uri) path-size) (subs uri path-size))] - (path-or-index-response path uri)))) - (fn [request] - (let [uri (:uri request) - path (-> request :path-params parameter)] - (or (path-or-index-response path uri) - (not-found-handler request)))))] - (create handler))))) + ([opts] + (-create-file-or-resource-handler response/resource-response opts)))) + +#?(:clj + (defn create-file-handler + "A ring handler for serving file resources, configured via options: + + | key | description | + | -------------------|-------------| + | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:` + | :root | optional resource root, defaults to `\"public\"` + | :path | optional path to mount the handler to. Works only if mounted outside of a router. + | :loader | optional class loader to resolve the resources + | :index-files | optional vector of index-files to look in a resource directory, defaults to `[\"index.html\"]` + | :not-found-handler | optional handler function to use if the requested resource is missing (404 Not Found)" + ([] + (create-file-handler nil)) + ([opts] + (-create-file-or-resource-handler response/file-response opts)))) (defn create-enrich-request [inject-match? inject-router?] (cond diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 8710f178..8cc1fb2b 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -454,158 +454,163 @@ (app request))))))) #?(:clj - (deftest resource-handler-test + (deftest file-resource-handler-test (let [redirect (fn [uri] {:status 302, :body "", :headers {"Location" uri}}) request (fn [uri] {:uri uri, :request-method :get})] - (testing "inside a router" - (testing "from root" - (let [app (ring/ring-handler - (ring/router - ["/*" (ring/create-resource-handler)]) - (ring/create-default-handler))] - (testing test - (testing "different file-types" - (let [response (app (request "/hello.json"))] - (is (= "application/json" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) - (let [response (app (request "/hello.xml"))] - (is (= "text/xml" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body response)))))) + (doseq [[name create] [["resource-handler" ring/create-resource-handler] + ["file-handler" #(ring/create-file-handler (assoc % :root "dev-resources/public"))]]] - (testing "index-files" - (let [response (app (request "/docs"))] - (is (= (redirect "/docs/index.html") response))) - (let [response (app (request "/docs/"))] - (is (= (redirect "/docs/index.html") response)))) + (testing (str "for " name) + (testing "inside a router" - (testing "not found" - (let [response (app (request "/not-found"))] - (is (= 404 (:status response))))) + (testing "from root" + (let [app (ring/ring-handler + (ring/router + ["/*" (create nil)]) + (ring/create-default-handler))] + (testing test + (testing "different file-types" + (let [response (app (request "/hello.json"))] + (is (= "application/json" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) + (let [response (app (request "/hello.xml"))] + (is (= "text/xml" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body response)))))) - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result) - raise ::not-called] - (app (request "/hello.xml") respond raise) - (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) - (is (get-in @result [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body @result))))))))) + (testing "index-files" + (let [response (app (request "/docs"))] + (is (= (redirect "/docs/index.html") response))) + (let [response (app (request "/docs/"))] + (is (= (redirect "/docs/index.html") response)))) - (testing "from path" - (let [app (ring/ring-handler - (ring/router - ["/files/*" (ring/create-resource-handler)]) - (ring/create-default-handler)) - request #(request (str "/files" %)) - redirect #(redirect (str "/files" %))] - (testing test - (testing "different file-types" - (let [response (app (request "/hello.json"))] - (is (= "application/json" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) - (let [response (app (request "/hello.xml"))] - (is (= "text/xml" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body response)))))) + (testing "not found" + (let [response (app (request "/not-found"))] + (is (= 404 (:status response))))) - (testing "index-files" - (let [response (app (request "/docs"))] - (is (= (redirect "/docs/index.html") response))) - (let [response (app (request "/docs/"))] - (is (= (redirect "/docs/index.html") response)))) + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result) + raise ::not-called] + (app (request "/hello.xml") respond raise) + (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) + (is (get-in @result [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body @result))))))))) - (testing "not found" - (let [response (app (request "/not-found"))] - (is (= 404 (:status response))))) + (testing "from path" + (let [app (ring/ring-handler + (ring/router + ["/files/*" (create nil)]) + (ring/create-default-handler)) + request #(request (str "/files" %)) + redirect #(redirect (str "/files" %))] + (testing test + (testing "different file-types" + (let [response (app (request "/hello.json"))] + (is (= "application/json" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) + (let [response (app (request "/hello.xml"))] + (is (= "text/xml" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body response)))))) - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result) - raise ::not-called] - (app (request "/hello.xml") respond raise) - (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) - (is (get-in @result [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body @result)))))))))) + (testing "index-files" + (let [response (app (request "/docs"))] + (is (= (redirect "/docs/index.html") response))) + (let [response (app (request "/docs/"))] + (is (= (redirect "/docs/index.html") response)))) - (testing "outside a router" + (testing "not found" + (let [response (app (request "/not-found"))] + (is (= 404 (:status response))))) - (testing "from root" - (let [app (ring/ring-handler - (ring/router []) - (ring/routes - (ring/create-resource-handler {:path "/"}) - (ring/create-default-handler)))] - (testing test - (testing "different file-types" - (let [response (app (request "/hello.json"))] - (is (= "application/json" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) - (let [response (app (request "/hello.xml"))] - (is (= "text/xml" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body response)))))) + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result) + raise ::not-called] + (app (request "/hello.xml") respond raise) + (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) + (is (get-in @result [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body @result)))))))))) - (testing "index-files" - (let [response (app (request "/docs"))] - (is (= (redirect "/docs/index.html") response))) - (let [response (app (request "/docs/"))] - (is (= (redirect "/docs/index.html") response)))) + (testing "outside a router" - (testing "not found" - (let [response (app (request "/not-found"))] - (is (= 404 (:status response))))) + (testing "from root" + (let [app (ring/ring-handler + (ring/router []) + (ring/routes + (create {:path "/"}) + (ring/create-default-handler)))] + (testing test + (testing "different file-types" + (let [response (app (request "/hello.json"))] + (is (= "application/json" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) + (let [response (app (request "/hello.xml"))] + (is (= "text/xml" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body response)))))) - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result) - raise ::not-called] - (app (request "/hello.xml") respond raise) - (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) - (is (get-in @result [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body @result))))))))) + (testing "index-files" + (let [response (app (request "/docs"))] + (is (= (redirect "/docs/index.html") response))) + (let [response (app (request "/docs/"))] + (is (= (redirect "/docs/index.html") response)))) - (testing "from path" - (let [app (ring/ring-handler - (ring/router []) - (ring/routes - (ring/create-resource-handler {:path "/files"}) - (ring/create-default-handler))) - request #(request (str "/files" %)) - redirect #(redirect (str "/files" %))] - (testing test - (testing "different file-types" - (let [response (app (request "/hello.json"))] - (is (= "application/json" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) - (let [response (app (request "/hello.xml"))] - (is (= "text/xml" (get-in response [:headers "Content-Type"]))) - (is (get-in response [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body response)))))) + (testing "not found" + (let [response (app (request "/not-found"))] + (is (= 404 (:status response))))) - (testing "index-files" - (let [response (app (request "/docs"))] - (is (= (redirect "/docs/index.html") response))) - (let [response (app (request "/docs/"))] - (is (= (redirect "/docs/index.html") response)))) + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result) + raise ::not-called] + (app (request "/hello.xml") respond raise) + (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) + (is (get-in @result [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body @result))))))))) - (testing "not found" - (let [response (app (request "/not-found"))] - (is (= 404 (:status response))))) + (testing "from path" + (let [app (ring/ring-handler + (ring/router []) + (ring/routes + (create {:path "/files"}) + (ring/create-default-handler))) + request #(request (str "/files" %)) + redirect #(redirect (str "/files" %))] + (testing test + (testing "different file-types" + (let [response (app (request "/hello.json"))] + (is (= "application/json" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) + (let [response (app (request "/hello.xml"))] + (is (= "text/xml" (get-in response [:headers "Content-Type"]))) + (is (get-in response [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body response)))))) - (testing "3-arity" - (let [result (atom nil) - respond (partial reset! result) - raise ::not-called] - (app (request "/hello.xml") respond raise) - (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) - (is (get-in @result [:headers "Last-Modified"])) - (is (= "file\n" (slurp (:body @result))))))))))))) + (testing "index-files" + (let [response (app (request "/docs"))] + (is (= (redirect "/docs/index.html") response))) + (let [response (app (request "/docs/"))] + (is (= (redirect "/docs/index.html") response)))) + + (testing "not found" + (let [response (app (request "/not-found"))] + (is (= 404 (:status response))))) + + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result) + raise ::not-called] + (app (request "/hello.xml") respond raise) + (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) + (is (get-in @result [:headers "Last-Modified"])) + (is (= "file\n" (slurp (:body @result))))))))))))))) (deftest router-available-in-default-branch (testing "1-arity"