|
1 | 1 | (ns triangulum.handler
|
2 |
| - (:require [clojure.data.json :as json] |
3 |
| - [clojure.edn :as edn] |
4 |
| - [clojure.spec.alpha :as s] |
5 |
| - [clojure.string :as str] |
6 |
| - [ring.middleware.absolute-redirects :refer [wrap-absolute-redirects]] |
7 |
| - [ring.middleware.content-type :refer [wrap-content-type]] |
8 |
| - [ring.middleware.default-charset :refer [wrap-default-charset]] |
9 |
| - [ring.middleware.gzip :refer [wrap-gzip]] |
10 |
| - [ring.middleware.json :refer [wrap-json-params]] |
11 |
| - [ring.middleware.keyword-params :refer [wrap-keyword-params]] |
12 |
| - [ring.middleware.multipart-params :refer [wrap-multipart-params]] |
13 |
| - [ring.middleware.nested-params :refer [wrap-nested-params]] |
14 |
| - [ring.middleware.not-modified :refer [wrap-not-modified]] |
15 |
| - [ring.middleware.params :refer [wrap-params]] |
16 |
| - [ring.middleware.reload :refer [wrap-reload]] |
17 |
| - [ring.middleware.resource :refer [wrap-resource]] |
18 |
| - [ring.middleware.session :refer [wrap-session]] |
19 |
| - [ring.middleware.session.cookie :refer [cookie-store]] |
20 |
| - [ring.middleware.ssl :refer [wrap-ssl-redirect]] |
21 |
| - [ring.util.codec :refer [url-decode]] |
22 |
| - [ring.middleware.x-headers :refer [wrap-content-type-options |
23 |
| - wrap-frame-options |
24 |
| - wrap-xss-protection]] |
25 |
| - [triangulum.config :as config :refer [get-config]] |
26 |
| - [triangulum.logging :refer [log log-str]] |
27 |
| - [triangulum.errors :refer [nil-on-error]] |
28 |
| - [triangulum.utils :refer [resolve-foreign-symbol]] |
29 |
| - [triangulum.response :refer [forbidden-response data-response]])) |
| 2 | + (:require [clojure.data.json :as json] |
| 3 | + [clojure.edn :as edn] |
| 4 | + [clojure.spec.alpha :as s] |
| 5 | + [clojure.string :as str] |
| 6 | + [ring.middleware.absolute-redirects :refer [wrap-absolute-redirects]] |
| 7 | + [ring.middleware.content-type :refer [wrap-content-type]] |
| 8 | + [ring.middleware.default-charset :refer [wrap-default-charset]] |
| 9 | + [ring.middleware.gzip :refer [wrap-gzip]] |
| 10 | + [ring.middleware.json :refer [wrap-json-params]] |
| 11 | + [ring.middleware.keyword-params :refer [wrap-keyword-params]] |
| 12 | + [ring.middleware.multipart-params :refer [wrap-multipart-params]] |
| 13 | + [ring.middleware.multipart-params.temp-file :refer [temp-file-store]] |
| 14 | + [ring.middleware.nested-params :refer [wrap-nested-params]] |
| 15 | + [ring.middleware.not-modified :refer [wrap-not-modified]] |
| 16 | + [ring.middleware.params :refer [wrap-params]] |
| 17 | + [ring.middleware.reload :refer [wrap-reload]] |
| 18 | + [ring.middleware.resource :refer [wrap-resource]] |
| 19 | + [ring.middleware.session :refer [wrap-session]] |
| 20 | + [ring.middleware.session.cookie :refer [cookie-store]] |
| 21 | + [ring.middleware.ssl :refer [wrap-ssl-redirect]] |
| 22 | + [ring.util.codec :refer [url-decode]] |
| 23 | + [ring.middleware.x-headers :refer [wrap-content-type-options |
| 24 | + wrap-frame-options |
| 25 | + wrap-xss-protection]] |
| 26 | + [triangulum.config :as config :refer [get-config]] |
| 27 | + [triangulum.logging :refer [log log-str]] |
| 28 | + [triangulum.errors :refer [nil-on-error]] |
| 29 | + [triangulum.utils :refer [resolve-foreign-symbol]] |
| 30 | + [triangulum.response :refer [forbidden-response data-response]])) |
30 | 31 |
|
31 | 32 | ;; spec
|
32 | 33 |
|
|
38 | 39 | (s/def ::truncate-request boolean?)
|
39 | 40 | (s/def ::private-request-keys (s/coll-of keyword :kind set?))
|
40 | 41 | (s/def ::private-response-keys (s/coll-of keyword :kind set?))
|
| 42 | +(s/def ::upload-max-size-mb |
| 43 | + ^{:doc "Maximum allowed file size in megabytes for uploads. |
| 44 | + Must be a positive integer not exceeding 1000MB (1GB)."} |
| 45 | + (s/and pos-int? #(<= % 1000))) |
| 46 | +(s/def ::upload-max-file-count |
| 47 | + ^{:doc "Maximum number of files allowed in a single upload request. |
| 48 | + Must be a positive integer not exceeding 100 files."} |
| 49 | + (s/and pos-int? #(<= % 100))) |
41 | 50 |
|
42 | 51 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
43 | 52 | ;; Routing Handler
|
|
173 | 182 | (cookie-store {:key (-> (random-string 16)
|
174 | 183 | (string-to-bytes))}))
|
175 | 184 |
|
| 185 | +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 186 | +;; Upload Configuration |
| 187 | +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
| 188 | + |
| 189 | +(def ^:private mb-to-bytes |
| 190 | + "Convert megabytes to bytes." |
| 191 | + (partial * 1024 1024)) |
| 192 | + |
| 193 | +(defn- calculate-progress-interval |
| 194 | + "Calculate bytes between progress updates, aiming for ~10 updates." |
| 195 | + [max-size-mb] |
| 196 | + (mb-to-bytes (max 1 (quot max-size-mb 10)))) |
| 197 | + |
| 198 | +(defn- make-upload-config |
| 199 | + "Creates upload configuration from config.edn or defaults." |
| 200 | + [] |
| 201 | + (let [max-size-mb (or (get-config ::upload-max-size-mb) 100) |
| 202 | + max-file-count (or (get-config ::upload-max-file-count) 10)] |
| 203 | + {:encoding "UTF-8" |
| 204 | + :error-handler (fn [_] |
| 205 | + (data-response |
| 206 | + (str "File upload exceeded limits. Maximum file size is " max-size-mb "MB.") |
| 207 | + {:status 413})) |
| 208 | + :max-file-count max-file-count |
| 209 | + :max-file-size (mb-to-bytes max-size-mb) |
| 210 | + :progress-fn (fn [_ bytes-read content-length item-count] |
| 211 | + (when (zero? (mod bytes-read (calculate-progress-interval max-size-mb))) |
| 212 | + (log (str "Upload progress: " bytes-read "/" content-length |
| 213 | + " bytes, items: " item-count)))) |
| 214 | + :store (temp-file-store)})) |
| 215 | + |
176 | 216 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
177 | 217 | ;; Handler Stack
|
178 | 218 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
195 | 235 | wrap-json-params
|
196 | 236 | wrap-edn-params
|
197 | 237 | wrap-nested-params
|
198 |
| - wrap-multipart-params |
| 238 | + (wrap-multipart-params (make-upload-config)) |
199 | 239 | wrap-params
|
200 | 240 | (wrap-session {:store (get-cookie-store)})
|
201 | 241 | wrap-absolute-redirects
|
|
0 commit comments