diff --git a/dev-resources/public/hello.json b/dev-resources/public/hello.json new file mode 100644 index 00000000..a1cc6c1a --- /dev/null +++ b/dev-resources/public/hello.json @@ -0,0 +1 @@ +{"hello": "file"} \ No newline at end of file diff --git a/dev-resources/public/hello.xml b/dev-resources/public/hello.xml new file mode 100644 index 00000000..d3653a88 --- /dev/null +++ b/dev-resources/public/hello.xml @@ -0,0 +1 @@ +file diff --git a/modules/reitit-ring/src/reitit/ring.cljc b/modules/reitit-ring/src/reitit/ring.cljc index 52da7a09..be7ba179 100644 --- a/modules/reitit-ring/src/reitit/ring.cljc +++ b/modules/reitit-ring/src/reitit/ring.cljc @@ -1,8 +1,11 @@ (ns reitit.ring (:require [meta-merge.core :refer [meta-merge]] [reitit.middleware :as middleware] + [reitit.ring.mime :as mime] [reitit.core :as r] - [reitit.impl :as impl])) + [reitit.impl :as impl] + #?(:clj + [clojure.java.io :as io]))) (def http-methods #{:get :head :post :put :delete :connect :options :trace :patch}) (defrecord Methods [get head post put delete connect options trace patch]) @@ -64,6 +67,48 @@ (respond (error-handler request))) (respond (not-found request))))))) +#?(:clj + (defn create-resource-handler + "A ring handler for handling classpath resources, + configured via options: + + | key | description | + | -------------|-------------| + | :parameter | optional name of the wildcard parameter, defaults to `:` + | :root | optional resource root, defaults to `public` + | :mime-types | optional extension->mime-type mapping, defaults to `reitit.ring.mime/default-types` + | :path | optional path to mount the handler to. Works only outside of a router + " + ([] + (create-resource-handler nil)) + ([{:keys [parameter root mime-types path] + :or {parameter (keyword "") + root "public" + mime-types mime/default-mime-types}}] + (let [response (fn [file] + {:status 200 + :body file + :headers {"Content-Type" (mime/ext-mime-type (.getName file) mime-types)}})] + (if path + (let [path-size (count path)] + (fn + ([req] + (let [uri (:uri req)] + (if (and (>= (count uri) path-size)) + (some->> (str root (subs uri path-size)) io/resource io/file response)))) + ([req respond _] + (let [uri (:uri req)] + (if (and (>= (count uri) path-size)) + (some->> (str root (subs uri path-size)) io/resource io/file response respond)))))) + (fn + ([req] + (or (some->> req :path-params parameter (str root "/") io/resource io/file response) + {:status 404})) + ([req respond _] + (respond + (or (some->> req :path-params parameter (str root "/") io/resource io/file response) + {:status 404}))))))))) + (defn ring-handler "Creates a ring-handler out of a ring-router. Supports both 1 (sync) and 3 (async) arities. diff --git a/modules/reitit-ring/src/reitit/ring/mime.cljc b/modules/reitit-ring/src/reitit/ring/mime.cljc new file mode 100644 index 00000000..3e7b6de3 --- /dev/null +++ b/modules/reitit-ring/src/reitit/ring/mime.cljc @@ -0,0 +1,99 @@ +(ns reitit.ring.mime + (:require [clojure.string :as str])) + +(def default-mime-types + "A map of file extensions to mime-types." + {"7z" "application/x-7z-compressed" + "aac" "audio/aac" + "ai" "application/postscript" + "appcache" "text/cache-manifest" + "asc" "text/plain" + "atom" "application/atom+xml" + "avi" "video/x-msvideo" + "bin" "application/octet-stream" + "bmp" "image/bmp" + "bz2" "application/x-bzip" + "class" "application/octet-stream" + "cer" "application/pkix-cert" + "crl" "application/pkix-crl" + "crt" "application/x-x509-ca-cert" + "css" "text/css" + "csv" "text/csv" + "deb" "application/x-deb" + "dart" "application/dart" + "dll" "application/octet-stream" + "dmg" "application/octet-stream" + "dms" "application/octet-stream" + "doc" "application/msword" + "dvi" "application/x-dvi" + "edn" "application/edn" + "eot" "application/vnd.ms-fontobject" + "eps" "application/postscript" + "etx" "text/x-setext" + "exe" "application/octet-stream" + "flv" "video/x-flv" + "flac" "audio/flac" + "gif" "image/gif" + "gz" "application/gzip" + "htm" "text/html" + "html" "text/html" + "ico" "image/x-icon" + "iso" "application/x-iso9660-image" + "jar" "application/java-archive" + "jpe" "image/jpeg" + "jpeg" "image/jpeg" + "jpg" "image/jpeg" + "js" "text/javascript" + "json" "application/json" + "lha" "application/octet-stream" + "lzh" "application/octet-stream" + "mov" "video/quicktime" + "m4v" "video/mp4" + "mp3" "audio/mpeg" + "mp4" "video/mp4" + "mpe" "video/mpeg" + "mpeg" "video/mpeg" + "mpg" "video/mpeg" + "oga" "audio/ogg" + "ogg" "audio/ogg" + "ogv" "video/ogg" + "pbm" "image/x-portable-bitmap" + "pdf" "application/pdf" + "pgm" "image/x-portable-graymap" + "png" "image/png" + "pnm" "image/x-portable-anymap" + "ppm" "image/x-portable-pixmap" + "ppt" "application/vnd.ms-powerpoint" + "ps" "application/postscript" + "qt" "video/quicktime" + "rar" "application/x-rar-compressed" + "ras" "image/x-cmu-raster" + "rb" "text/plain" + "rd" "text/plain" + "rss" "application/rss+xml" + "rtf" "application/rtf" + "sgm" "text/sgml" + "sgml" "text/sgml" + "svg" "image/svg+xml" + "swf" "application/x-shockwave-flash" + "tar" "application/x-tar" + "tif" "image/tiff" + "tiff" "image/tiff" + "ttf" "application/x-font-ttf" + "txt" "text/plain" + "webm" "video/webm" + "wmv" "video/x-ms-wmv" + "woff" "application/font-woff" + "xbm" "image/x-xbitmap" + "xls" "application/vnd.ms-excel" + "xml" "text/xml" + "xpm" "image/x-xpixmap" + "xwd" "image/x-xwindowdump" + "zip" "application/zip"}) + +(defn file-ext [name] + (if-let [i (str/last-index-of name ".")] + (subs name (inc i)))) + +(defn ext-mime-type [name mime-types] + (-> name file-ext mime-types)) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 8ab6273d..bc7a20c8 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -264,3 +264,41 @@ (let [app (create {::middleware/transform #(interleave % (repeat (middleware "debug")))})] (is (= {:status 200, :body [:olipa "debug" :kerran "debug" :avaruus "debug" :ok]} (app request))))))) + +#?(:clj + (deftest resource-handler-test + (doseq [[test app] [["inside a router" + (ring/ring-handler + (ring/router + [["/ping" (constantly {:status 200, :body "pong"})] + ["/files/*" (ring/create-resource-handler)]]) + (ring/create-default-handler))] + + ["outside of a router" + (ring/ring-handler + (ring/router + ["/ping" (constantly {:status 200, :body "pong"})]) + (ring/routes + (ring/create-resource-handler {:path "/files"}) + (ring/create-default-handler)))]]] + + (testing test + (testing "different file-types" + (let [response (app {:uri "/files/hello.json", :request-method :get})] + (is (= "application/json" (get-in response [:headers "Content-Type"]))) + (is (= "{\"hello\": \"file\"}" (slurp (:body response))))) + (let [response (app {:uri "/files/hello.xml", :request-method :get})] + (is (= "text/xml" (get-in response [:headers "Content-Type"]))) + (is (= "file\n" (slurp (:body response)))))) + + (testing "not found" + (let [response (app {:uri "/files/not-found", :request-method :get})] + (is (= 404 (:status response))))) + + (testing "3-arity" + (let [result (atom nil) + respond (partial reset! result) + raise ::not-called] + (app {:uri "/files/hello.xml", :request-method :get} respond raise) + (is (= "text/xml" (get-in @result [:headers "Content-Type"]))) + (is (= "file\n" (slurp (:body @result))))))))))