/
multipart_params.clj
175 lines (153 loc) · 7.16 KB
/
multipart_params.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
(ns ring.middleware.multipart-params
"Middleware that parses multipart request bodies into parameters.
This middleware is necessary to handle file uploads from web browsers.
Ring comes with two different multipart storage engines included:
ring.middleware.multipart-params.byte-array/byte-array-store
ring.middleware.multipart-params.temp-file/temp-file-store"
(:require [ring.util.codec :refer [assoc-conj]]
[ring.util.request :as req]
[ring.util.parsing :refer [re-charset]])
(:import [org.apache.commons.fileupload UploadContext
FileItemIterator
FileItemStream
FileUpload
ProgressListener]
[org.apache.commons.io IOUtils]))
(defn- progress-listener
"Create a progress listener that calls the supplied function."
[request progress-fn]
(reify ProgressListener
(update [this bytes-read content-length item-count]
(progress-fn request bytes-read content-length item-count))))
(defn- multipart-form?
"Does a request have a multipart form?"
[request]
(= (req/content-type request) "multipart/form-data"))
(defn- request-context
"Create an UploadContext object from a request map."
{:tag UploadContext}
[request encoding]
(reify UploadContext
(getContentType [this] (get-in request [:headers "content-type"]))
(getContentLength [this] (or (req/content-length request) -1))
(contentLength [this] (or (req/content-length request) -1))
(getCharacterEncoding [this] encoding)
(getInputStream [this] (:body request))))
(defn- file-item-iterator-seq
"Create a lazy seq from a FileItemIterator instance."
[^FileItemIterator it]
(lazy-seq
(if (.hasNext it)
(cons (.next it) (file-item-iterator-seq it)))))
(defn- file-item-seq
"Create a seq of FileItem instances from a request context."
[request progress-fn context]
(let [upload (if progress-fn
(doto (FileUpload.)
(.setProgressListener (progress-listener request progress-fn)))
(FileUpload.))]
(file-item-iterator-seq
(.getItemIterator ^FileUpload upload context))))
(defn- parse-content-type-charset [^FileItemStream item]
(some->> (.getContentType item)
(re-find re-charset)
second))
(defn- parse-html5-charset [params]
(when-let [charset (->> params (filter #(= (first %) "_charset_")) first second :bytes)]
(String. ^bytes charset "US-ASCII")))
(defn- decode-string-values [fallback-encoding forced-encoding params]
(let [html5-encoding (parse-html5-charset params)]
(for [[k v field?] params]
[k (if field?
(String. ^bytes (:bytes v) (str (or forced-encoding
html5-encoding
(:encoding v)
fallback-encoding)))
v)])))
(defn- parse-file-item
"Parse a FileItemStream into a key-value pair. If the request is a file the
supplied store function is used to save it."
[^FileItemStream item store]
[(.getFieldName item)
(if (.isFormField item)
{:bytes (IOUtils/toByteArray (.openStream item))
:encoding (parse-content-type-charset item)}
(store {:filename (.getName item)
:content-type (.getContentType item)
:stream (.openStream item)}))
(.isFormField item)])
(defn- parse-multipart-params
"Parse a map of multipart parameters from the request."
[request fallback-encoding forced-encoding store progress-fn]
(->> (request-context request fallback-encoding)
(file-item-seq request progress-fn)
(map #(parse-file-item % store))
(decode-string-values fallback-encoding forced-encoding)
(reduce (fn [m [k v]] (assoc-conj m k v)) {})))
(defn- load-var
"Returns the var named by the supplied symbol, or nil if not found. Attempts
to load the var namespace on the fly if not already loaded."
[sym]
(require (symbol (namespace sym)))
(find-var sym))
(def ^:private default-store
(delay
(let [store 'ring.middleware.multipart-params.temp-file/temp-file-store
func (load-var store)]
(func))))
(defn multipart-params-request
"Adds :multipart-params and :params keys to request.
See: wrap-multipart-params."
{:added "1.2"}
([request]
(multipart-params-request request {}))
([request options]
(let [store (or (:store options) @default-store)
forced-encoding (:encoding options)
req-encoding (or forced-encoding
(:fallback-encoding options)
(req/character-encoding request)
"UTF-8")
progress (:progress-fn options)
params (if (multipart-form? request)
(parse-multipart-params request
req-encoding
forced-encoding
store
progress)
{})]
(merge-with merge request
{:multipart-params params}
{:params params}))))
(defn wrap-multipart-params
"Middleware to parse multipart parameters from a request. Adds the
following keys to the request map:
:multipart-params - a map of multipart parameters
:params - a merged map of all types of parameter
The following options are accepted
:encoding - character encoding to use for multipart parsing.
Overrides the encoding specified in the request. If not
specified, uses the encoding specified in a part named
\"_charset_\", or the content type for each part, or
request character encoding if the part has no encoding,
or \"UTF-8\" if no request character encoding is set.
:fallback-encoding - specifies the character encoding used in parsing if a
part of the request does not specify encoding in its
content type or no part named \"_charset_\" is present.
Has no effect if :encoding is also set.
:store - a function that stores a file upload. The function
should expect a map with :filename, :content-type and
:stream keys, and its return value will be used as the
value for the parameter in the multipart parameter map.
The default storage function is the temp-file-store.
:progress-fn - a function that gets called during uploads. The
function should expect four parameters: request,
bytes-read, content-length, and item-count."
([handler]
(wrap-multipart-params handler {}))
([handler options]
(fn
([request]
(handler (multipart-params-request request options)))
([request respond raise]
(handler (multipart-params-request request options) respond raise)))))