Skip to content

Commit 7e06486

Browse files
committed
support large file upload
1 parent 97c8767 commit 7e06486

5 files changed

+78
-30
lines changed

config.namespaced-example.edn

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
:triangulum.handler/truncate-request false
2222
:triangulum.handler/private-request-keys #{:base64Image :plotFileBase64 :sampleFileBase64}
2323
:triangulum.handler/private-response-keys #{}
24+
:triangulum.handler/upload-max-size-mb 100
25+
:triangulum.handler/upload-max-file-count 10
2426

2527
;; workers (server)
2628
:triangulum.worker/workers [{:triangulum.worker/name "scheduler"

config.nested-example.edn

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
:truncate-request false
2222
:private-request-keys #{:base64Image :plotFileBase64 :sampleFileBase64}
2323
:private-response-keys #{}
24+
:upload-max-size-mb 100
25+
:upload-max-file-count 10
2426

2527
;; workers
2628
:workers {:scheduler {:start product-ns.jobs/start-scheduled-jobs!

src/triangulum/config_namespaced_spec.clj

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#(no-keys-of-ns? % "triangulum.server")
1919
:server-keys
2020
(s/keys :req [:triangulum.server/http-port
21-
:triangulum.server/handler]
21+
:triangulum.server/handler]
2222
:opt [:triangulum.server/https-port
2323
:triangulum.server/nrepl
2424
:triangulum.server/nrepl-port
@@ -37,6 +37,8 @@
3737
:triangulum.handler/private-request-keys
3838
:triangulum.handler/private-response-keys
3939
:triangulum.handler/bad-tokens
40+
:triangulum.handler/upload-max-size-mb
41+
:triangulum.handler/upload-max-file-count
4042
:triangulum.worker/workers
4143
:triangulum.response/response-type])))
4244

src/triangulum/config_nested_spec.clj

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
:triangulum.handler/truncate-request
2424
:triangulum.handler/private-request-keys
2525
:triangulum.handler/private-response-keys
26+
:triangulum.handler/upload-max-size-mb
27+
:triangulum.handler/upload-max-file-count
2628
:triangulum.worker/workers
2729
:triangulum.response/response-type]))
2830

src/triangulum/handler.clj

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
(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]]))
3031

3132
;; spec
3233

@@ -38,6 +39,14 @@
3839
(s/def ::truncate-request boolean?)
3940
(s/def ::private-request-keys (s/coll-of keyword :kind set?))
4041
(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)))
4150

4251
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4352
;; Routing Handler
@@ -173,6 +182,37 @@
173182
(cookie-store {:key (-> (random-string 16)
174183
(string-to-bytes))}))
175184

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+
176216
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
177217
;; Handler Stack
178218
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -195,7 +235,7 @@
195235
wrap-json-params
196236
wrap-edn-params
197237
wrap-nested-params
198-
wrap-multipart-params
238+
(wrap-multipart-params (make-upload-config))
199239
wrap-params
200240
(wrap-session {:store (get-cookie-store)})
201241
wrap-absolute-redirects

0 commit comments

Comments
 (0)