liquidsoap-prettier 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-formatting.yml +32 -0
- package/README.md +31 -5
- package/package.json +1 -1
- package/src/cli.js +104 -9
- package/tests/liq/audio.liq +460 -0
- package/tests/liq/autocue.liq +1081 -0
- package/tests/liq/clock.liq +14 -0
- package/tests/liq/cron.liq +74 -0
- package/tests/liq/error.liq +48 -0
- package/tests/liq/extra/audio.liq +677 -0
- package/tests/liq/extra/audioscrobbler.liq +482 -0
- package/tests/liq/extra/deprecations.liq +976 -0
- package/tests/liq/extra/externals.liq +196 -0
- package/tests/liq/extra/fades.liq +260 -0
- package/tests/liq/extra/file.liq +66 -0
- package/tests/liq/extra/http.liq +160 -0
- package/tests/liq/extra/interactive.liq +917 -0
- package/tests/liq/extra/metadata.liq +75 -0
- package/tests/liq/extra/native.liq +201 -0
- package/tests/liq/extra/openai.liq +150 -0
- package/tests/liq/extra/server.liq +177 -0
- package/tests/liq/extra/source.liq +476 -0
- package/tests/liq/extra/spinitron.liq +272 -0
- package/tests/liq/extra/telnet.liq +266 -0
- package/tests/liq/extra/video.liq +59 -0
- package/tests/liq/extra/visualization.liq +68 -0
- package/tests/liq/fades.liq +941 -0
- package/tests/liq/ffmpeg.liq +605 -0
- package/tests/liq/file.liq +387 -0
- package/tests/liq/getter.liq +74 -0
- package/tests/liq/hls.liq +329 -0
- package/tests/liq/http.liq +1048 -0
- package/tests/liq/http_codes.liq +447 -0
- package/tests/liq/icecast.liq +58 -0
- package/tests/liq/io.liq +106 -0
- package/tests/liq/liquidsoap.liq +31 -0
- package/tests/liq/list.liq +440 -0
- package/tests/liq/log.liq +47 -0
- package/tests/liq/lufs.liq +295 -0
- package/tests/liq/math.liq +23 -0
- package/tests/liq/medialib.liq +752 -0
- package/tests/liq/metadata.liq +253 -0
- package/tests/liq/nfo.liq +258 -0
- package/tests/liq/null.liq +71 -0
- package/tests/liq/playlist.liq +1347 -0
- package/tests/liq/predicate.liq +106 -0
- package/tests/liq/process.liq +93 -0
- package/tests/liq/profiler.liq +5 -0
- package/tests/liq/protocols.liq +1139 -0
- package/tests/liq/ref.liq +28 -0
- package/tests/liq/replaygain.liq +135 -0
- package/tests/liq/request.liq +467 -0
- package/tests/liq/resolvers.liq +33 -0
- package/tests/liq/runtime.liq +70 -0
- package/tests/liq/server.liq +99 -0
- package/tests/liq/settings.liq +41 -0
- package/tests/liq/socket.liq +33 -0
- package/tests/liq/source.liq +362 -0
- package/tests/liq/sqlite.liq +161 -0
- package/tests/liq/stdlib.liq +172 -0
- package/tests/liq/string.liq +476 -0
- package/tests/liq/switches.liq +197 -0
- package/tests/liq/testing.liq +37 -0
- package/tests/liq/thread.liq +161 -0
- package/tests/liq/tracks.liq +100 -0
- package/tests/liq/utils.liq +81 -0
- package/tests/liq/video.liq +918 -0
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
# Set of HTTP utils.
|
|
2
|
+
|
|
3
|
+
# Prepare a list of `(string, string)` arguments for
|
|
4
|
+
# sending as `"application/x-www-form-urlencoded"` content
|
|
5
|
+
# @category Internet
|
|
6
|
+
def http.www_form_urlencoded(params) =
|
|
7
|
+
params =
|
|
8
|
+
list.map(
|
|
9
|
+
fun (v) ->
|
|
10
|
+
begin
|
|
11
|
+
let (key, value) = v
|
|
12
|
+
"#{url.encode(key)}=#{url.encode(value)}"
|
|
13
|
+
end,
|
|
14
|
+
params
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
string.concat(separator="&", params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Prepare a list of data to be sent as multipart form data.
|
|
21
|
+
# @category Internet
|
|
22
|
+
# @param ~boundary Specify boundary to use for multipart/form-data.
|
|
23
|
+
# @param data data to insert
|
|
24
|
+
def http.multipart_form_data(~boundary=null, data) =
|
|
25
|
+
def default_boundary() =
|
|
26
|
+
range = [...string.char.ascii.alphabet, ...string.char.ascii.number]
|
|
27
|
+
l = list.init(12, fun (_) -> string.char.ascii.random(range))
|
|
28
|
+
string.concat(l)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
boundary = null.default(boundary, default_boundary)
|
|
32
|
+
|
|
33
|
+
def mk_content(contents, entry) =
|
|
34
|
+
data = entry.contents
|
|
35
|
+
attributes = [("name", entry.name), ...entry.attributes]
|
|
36
|
+
attributes =
|
|
37
|
+
list.map(
|
|
38
|
+
fun (v) -> "#{string(fst(v))}=#{string.quote(snd(v))}",
|
|
39
|
+
attributes
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
attributes =
|
|
43
|
+
string.concat(
|
|
44
|
+
separator="; ",
|
|
45
|
+
attributes
|
|
46
|
+
)
|
|
47
|
+
headers =
|
|
48
|
+
list.map(
|
|
49
|
+
fun (v) ->
|
|
50
|
+
"#{string(fst(v))}: #{string(snd(v))}",
|
|
51
|
+
entry.headers
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
headers = string.concat(separator="\r\n", headers)
|
|
55
|
+
headers = headers == "" ? "" : "#{headers}\r\n"
|
|
56
|
+
|
|
57
|
+
# This is for typing purposes
|
|
58
|
+
(entry : unit)
|
|
59
|
+
[
|
|
60
|
+
...contents,
|
|
61
|
+
getter("--#{boundary}\r\n"),
|
|
62
|
+
getter(
|
|
63
|
+
"Content-Disposition: form-data; #{attributes}\r\n"
|
|
64
|
+
),
|
|
65
|
+
getter(headers),
|
|
66
|
+
getter("\r\n"),
|
|
67
|
+
data,
|
|
68
|
+
getter("\r\n")
|
|
69
|
+
]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
contents = [...list.fold(mk_content, [], data), getter("--#{boundary}--\r\n")]
|
|
73
|
+
contents = string.getter.concat(contents)
|
|
74
|
+
contents =
|
|
75
|
+
if
|
|
76
|
+
list.for_all(fun (entry) -> getter.is_constant(entry.contents), data)
|
|
77
|
+
then
|
|
78
|
+
getter(string.getter.flush(contents))
|
|
79
|
+
else
|
|
80
|
+
contents
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
{contents = contents, boundary = boundary}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Initiate a response handler with pre-filled values.
|
|
87
|
+
# @category Internet
|
|
88
|
+
# @method content_type Set `"Content-Type"` header
|
|
89
|
+
# @method data Set response data.
|
|
90
|
+
# @method headers Replace response headers.
|
|
91
|
+
# @method header Set a single header on the response
|
|
92
|
+
# @method json Set content-type to json and data to `json.stringify` of the argument
|
|
93
|
+
# @method redirect Set `status_code` and `Location:` header for a HTTP redirect response
|
|
94
|
+
# @method html Set content-type to html and data to argument value
|
|
95
|
+
# @method http_version Set http protocol version
|
|
96
|
+
# @method status_code Set response status code
|
|
97
|
+
# @method status_message Set response status message
|
|
98
|
+
def http.response(
|
|
99
|
+
~http_version="1.1",
|
|
100
|
+
~status_code=null,
|
|
101
|
+
~status_message=null,
|
|
102
|
+
~headers=[],
|
|
103
|
+
~content_type=null,
|
|
104
|
+
~data=getter("")
|
|
105
|
+
) =
|
|
106
|
+
status_code =
|
|
107
|
+
status_code
|
|
108
|
+
?? if
|
|
109
|
+
http_version == "1.1"
|
|
110
|
+
and headers["expect"] == "100-continue"
|
|
111
|
+
and getter.get(data) == ""
|
|
112
|
+
then
|
|
113
|
+
100
|
|
114
|
+
else
|
|
115
|
+
200
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
http_version = ref(http_version)
|
|
119
|
+
status_code = ref(status_code)
|
|
120
|
+
status_message = ref(status_message)
|
|
121
|
+
headers = ref(headers)
|
|
122
|
+
content_type = ref(content_type)
|
|
123
|
+
data = ref(data)
|
|
124
|
+
status_sent = ref(false)
|
|
125
|
+
headers_sent = ref(false)
|
|
126
|
+
data_sent = ref(false)
|
|
127
|
+
response_ended = ref(false)
|
|
128
|
+
|
|
129
|
+
def mk_status() =
|
|
130
|
+
status_sent := true
|
|
131
|
+
http_version = http_version()
|
|
132
|
+
status_code = status_code()
|
|
133
|
+
status_code =
|
|
134
|
+
if
|
|
135
|
+
status_code == 100 and getter.get(data()) != ""
|
|
136
|
+
then
|
|
137
|
+
200
|
|
138
|
+
else
|
|
139
|
+
status_code
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
status_message = status_message() ?? http.codes[status_code]
|
|
143
|
+
"HTTP/#{http_version} #{status_code} #{status_message}\r\n"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def mk_headers() =
|
|
147
|
+
headers_sent := true
|
|
148
|
+
headers = headers()
|
|
149
|
+
content_type = content_type()
|
|
150
|
+
data = data()
|
|
151
|
+
headers =
|
|
152
|
+
if
|
|
153
|
+
getter.is_constant(data)
|
|
154
|
+
then
|
|
155
|
+
data = getter.get(data)
|
|
156
|
+
len = string.bytes.length(data)
|
|
157
|
+
if
|
|
158
|
+
data != ""
|
|
159
|
+
then
|
|
160
|
+
("Content-Length", "#{len}")::headers
|
|
161
|
+
else
|
|
162
|
+
headers
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
("Transfer-Encoding", "chunked")::headers
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
headers =
|
|
169
|
+
if
|
|
170
|
+
null.defined(content_type) and null.get(content_type) != ""
|
|
171
|
+
then
|
|
172
|
+
("Content-type", null.get(content_type))::headers
|
|
173
|
+
else
|
|
174
|
+
headers
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
headers =
|
|
178
|
+
list.map(
|
|
179
|
+
fun (v) ->
|
|
180
|
+
"#{fst(v)}: #{snd(v)}",
|
|
181
|
+
headers
|
|
182
|
+
)
|
|
183
|
+
headers = string.concat(separator="\r\n", headers)
|
|
184
|
+
headers = if headers != "" then "#{headers}\r\n" else "" end
|
|
185
|
+
"#{headers}\r\n"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def mk_data() =
|
|
189
|
+
data_sent := true
|
|
190
|
+
data = data()
|
|
191
|
+
if
|
|
192
|
+
getter.is_constant(data)
|
|
193
|
+
then
|
|
194
|
+
response_ended := true
|
|
195
|
+
getter.get(data)
|
|
196
|
+
else
|
|
197
|
+
data = getter.get(data)
|
|
198
|
+
len = string.bytes.length(data)
|
|
199
|
+
response_ended := data == ""
|
|
200
|
+
"#{string.hex_of_int(len)}\r\n#{data}\r\n"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def response() =
|
|
205
|
+
if
|
|
206
|
+
response_ended()
|
|
207
|
+
then
|
|
208
|
+
""
|
|
209
|
+
elsif
|
|
210
|
+
not status_sent()
|
|
211
|
+
then
|
|
212
|
+
mk_status()
|
|
213
|
+
elsif
|
|
214
|
+
not headers_sent()
|
|
215
|
+
then
|
|
216
|
+
mk_headers()
|
|
217
|
+
else
|
|
218
|
+
mk_data()
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def attr_method(sent, attr) =
|
|
223
|
+
def set(v) =
|
|
224
|
+
if
|
|
225
|
+
sent()
|
|
226
|
+
then
|
|
227
|
+
error.raise(
|
|
228
|
+
error.invalid,
|
|
229
|
+
"HTTP response has already been sent for this value!"
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
attr := v
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def get() =
|
|
237
|
+
attr()
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
set.{current = get}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def header(k, v) =
|
|
244
|
+
headers := (k, v)::headers()
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
code = status_code
|
|
248
|
+
|
|
249
|
+
def redirect(~status_code=301, location) =
|
|
250
|
+
if
|
|
251
|
+
status_sent()
|
|
252
|
+
then
|
|
253
|
+
error.raise(
|
|
254
|
+
error.invalid,
|
|
255
|
+
"HTTP response has already been sent for this value!"
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
code := status_code
|
|
260
|
+
header("Location", location)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def json(~compact=true, v) =
|
|
264
|
+
if
|
|
265
|
+
headers_sent()
|
|
266
|
+
then
|
|
267
|
+
error.raise(
|
|
268
|
+
error.invalid,
|
|
269
|
+
"HTTP response has already been sent for this value!"
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
content_type :=
|
|
274
|
+
"application/json; charset=utf-8"
|
|
275
|
+
data := json.stringify(v, compact=compact) ^ "\n"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def html(d) =
|
|
279
|
+
if
|
|
280
|
+
headers_sent()
|
|
281
|
+
then
|
|
282
|
+
error.raise(
|
|
283
|
+
error.invalid,
|
|
284
|
+
"HTTP response has already been sent for this value!"
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
content_type := "text/html"
|
|
289
|
+
data := d
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def send_status(socket) =
|
|
293
|
+
if not status_sent() then socket.write(mk_status()) end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def multipart_form(~boundary=null, contents) =
|
|
297
|
+
if
|
|
298
|
+
headers_sent()
|
|
299
|
+
then
|
|
300
|
+
error.raise(
|
|
301
|
+
error.invalid,
|
|
302
|
+
"HTTP response has already been sent for this value!"
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
form_data = http.multipart_form_data(boundary=boundary, contents)
|
|
307
|
+
content_type :=
|
|
308
|
+
"multipart/form-data; boundary=#{form_data.boundary}"
|
|
309
|
+
data := form_data.contents
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
response.{
|
|
313
|
+
http_version = attr_method(status_sent, http_version),
|
|
314
|
+
status_code = attr_method(status_sent, status_code),
|
|
315
|
+
status_message = attr_method(status_sent, status_message),
|
|
316
|
+
headers = attr_method(headers_sent, headers),
|
|
317
|
+
header = header,
|
|
318
|
+
redirect = redirect,
|
|
319
|
+
json = json,
|
|
320
|
+
html = html,
|
|
321
|
+
content_type = attr_method(headers_sent, content_type),
|
|
322
|
+
multipart_form = multipart_form,
|
|
323
|
+
data = attr_method(data_sent, data),
|
|
324
|
+
send_status = send_status,
|
|
325
|
+
status_sent = {status_sent()}
|
|
326
|
+
}
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @flag hidden
|
|
330
|
+
def harbor.http.regexp_of_path(path) =
|
|
331
|
+
def named_capture(s) =
|
|
332
|
+
name =
|
|
333
|
+
string.sub(
|
|
334
|
+
encoding="ascii",
|
|
335
|
+
s,
|
|
336
|
+
start=1,
|
|
337
|
+
length=string.bytes.length(s) - 1
|
|
338
|
+
)
|
|
339
|
+
"(?<#{name}>[^/]+)"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
rex = r/:[\w_]+/g.replace(named_capture, path)
|
|
343
|
+
regexp("^#{rex}$")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# @flag hidden
|
|
347
|
+
def harbor.http.mk_body(get_data) =
|
|
348
|
+
done = ref(false)
|
|
349
|
+
data = ref("")
|
|
350
|
+
|
|
351
|
+
def body(~timeout=10.) =
|
|
352
|
+
if
|
|
353
|
+
done()
|
|
354
|
+
then
|
|
355
|
+
data()
|
|
356
|
+
else
|
|
357
|
+
start_time = time()
|
|
358
|
+
|
|
359
|
+
def rec read() =
|
|
360
|
+
if
|
|
361
|
+
done()
|
|
362
|
+
then
|
|
363
|
+
data()
|
|
364
|
+
else
|
|
365
|
+
if
|
|
366
|
+
start_time + timeout < time()
|
|
367
|
+
then
|
|
368
|
+
error.raise(error.http, "Timeout!")
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
r = get_data(timeout=timeout)
|
|
372
|
+
if
|
|
373
|
+
r == ""
|
|
374
|
+
then
|
|
375
|
+
data()
|
|
376
|
+
else
|
|
377
|
+
data := "#{data()}#{r}"
|
|
378
|
+
read()
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
read()
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
body
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Register a HTTP handler on the harbor. This function offers a simple API,
|
|
391
|
+
# suitable for quick implementation of HTTP handlers. See `harbor.http.register`
|
|
392
|
+
# for a node/express like alternative API.
|
|
393
|
+
# @category Internet
|
|
394
|
+
# @argsof harbor.http.register
|
|
395
|
+
def harbor.http.register.simple(%argsof(harbor.http.register), path, handler) =
|
|
396
|
+
def handler(request) =
|
|
397
|
+
def data(~timeout=10.) =
|
|
398
|
+
request.data(timeout=timeout)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
handler(request.{data = data, body = harbor.http.mk_body(data)})
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
harbor.http.register(
|
|
405
|
+
%argsof(harbor.http.register),
|
|
406
|
+
harbor.http.regexp_of_path(path),
|
|
407
|
+
handler
|
|
408
|
+
)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Register a HTTP handler on the harbor with a generic regexp `path`. This function offers a simple API,
|
|
412
|
+
# suitable for quick implementation of HTTP handlers. See `harbor.http.register`
|
|
413
|
+
# for a node/express like alternative API.
|
|
414
|
+
# @category Internet
|
|
415
|
+
# @argsof harbor.http.register
|
|
416
|
+
def harbor.http.register.simple.regexp(
|
|
417
|
+
%argsof(harbor.http.register),
|
|
418
|
+
path,
|
|
419
|
+
handler
|
|
420
|
+
) =
|
|
421
|
+
harbor.http.register(%argsof(harbor.http.register), path, handler)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# @flag hidden
|
|
425
|
+
let harbor.http.middleware = ref(fun (req, res, next) -> next(req, res))
|
|
426
|
+
|
|
427
|
+
# Register a new harbor middleware
|
|
428
|
+
# @category Internet
|
|
429
|
+
def harbor.http.middleware.register(fn) =
|
|
430
|
+
middleware = harbor.http.middleware()
|
|
431
|
+
harbor.http.middleware :=
|
|
432
|
+
fun (req, res, next) ->
|
|
433
|
+
begin middleware(req, res, fun (res, res) -> fn(req, res, next)) end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# @flag hidden
|
|
437
|
+
def harbor.http.register.regexp(%argsof(harbor.http.register), path, handler) =
|
|
438
|
+
def handler(request) =
|
|
439
|
+
response = http.response(http_version=request.http_version)
|
|
440
|
+
is_response_done = ref(false)
|
|
441
|
+
|
|
442
|
+
def replaces response() =
|
|
443
|
+
ret = response()
|
|
444
|
+
is_response_done := ret == ""
|
|
445
|
+
ret
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def data(~timeout=10.) =
|
|
449
|
+
if
|
|
450
|
+
is_response_done()
|
|
451
|
+
then
|
|
452
|
+
error.raise(
|
|
453
|
+
error.http,
|
|
454
|
+
"Response ended!"
|
|
455
|
+
)
|
|
456
|
+
end
|
|
457
|
+
if
|
|
458
|
+
response.status_code.current() == 100 and not response.status_sent()
|
|
459
|
+
then
|
|
460
|
+
response.send_status(request.socket)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
request.data(timeout=timeout)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
request =
|
|
467
|
+
{
|
|
468
|
+
body = harbor.http.mk_body(data),
|
|
469
|
+
data = data,
|
|
470
|
+
headers = request.headers,
|
|
471
|
+
http_version = request.http_version,
|
|
472
|
+
method = request.method,
|
|
473
|
+
path = request.path,
|
|
474
|
+
query = request.query
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
handler =
|
|
478
|
+
fun (req, res) ->
|
|
479
|
+
begin
|
|
480
|
+
middleware = harbor.http.middleware()
|
|
481
|
+
middleware(req, res, fun (req, res) -> handler(req, res))
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
(handler(request, response) : unit)
|
|
485
|
+
response
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
harbor.http.register(%argsof(harbor.http.register), path, handler)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def replaces harbor.http.middleware =
|
|
492
|
+
()
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Register a HTTP handler on the harbor. The handler function
|
|
496
|
+
# receives as argument the full requested information and returns the
|
|
497
|
+
# answer sent to the client, including HTTP headers. This function
|
|
498
|
+
# registers exact path matches, i.e. `"/users"`, `"/index.hml"`
|
|
499
|
+
# as well as fragment matches, i.e. `"/user/:id"`, `"/users/:id/collabs/:cid"`,
|
|
500
|
+
# etc. If you need more advanced matching, use `harbor.http.register.regexp`
|
|
501
|
+
# to match regular expressions. Paths are resolved in the order they are declared
|
|
502
|
+
# and can override default harbor paths such as metadata handlers.
|
|
503
|
+
# The handler receives the request details as a record and a response
|
|
504
|
+
# handler. Matched fragments are reported as part of the response `query` parameter.
|
|
505
|
+
# The response handler can be used to fill up details about the http response,
|
|
506
|
+
# which will be converted into a plain HTTP response string after the handler returns.
|
|
507
|
+
# @category Internet
|
|
508
|
+
# @argsof harbor.http.register
|
|
509
|
+
def replaces harbor.http.register(
|
|
510
|
+
%argsof(harbor.http.register),
|
|
511
|
+
path,
|
|
512
|
+
handler
|
|
513
|
+
) =
|
|
514
|
+
harbor.http.register.regexp(
|
|
515
|
+
%argsof(harbor.http.register),
|
|
516
|
+
harbor.http.regexp_of_path(path),
|
|
517
|
+
handler
|
|
518
|
+
)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
let harbor.http.static = ()
|
|
522
|
+
|
|
523
|
+
# @flag hidden
|
|
524
|
+
def harbor.http.static.base(
|
|
525
|
+
serve,
|
|
526
|
+
~content_type,
|
|
527
|
+
~basepath,
|
|
528
|
+
~headers,
|
|
529
|
+
~browse,
|
|
530
|
+
directory
|
|
531
|
+
) =
|
|
532
|
+
directory = path.home.unrelate(directory)
|
|
533
|
+
basepath = if r/^\//.test(basepath) then basepath else "/#{basepath}" end
|
|
534
|
+
basepath = if r/\/$/.test(basepath) then basepath else "#{basepath}/" end
|
|
535
|
+
|
|
536
|
+
def handler(request, response) =
|
|
537
|
+
response.headers(headers)
|
|
538
|
+
rpath = string.residual(prefix=basepath, request.path)
|
|
539
|
+
if
|
|
540
|
+
not null.defined(rpath)
|
|
541
|
+
then
|
|
542
|
+
response.status_code(404)
|
|
543
|
+
else
|
|
544
|
+
rpath = url.decode(null.get(rpath))
|
|
545
|
+
fname = path.concat(directory, rpath)
|
|
546
|
+
log.debug(
|
|
547
|
+
"Serving static file: #{fname}"
|
|
548
|
+
)
|
|
549
|
+
if
|
|
550
|
+
not file.exists(fname)
|
|
551
|
+
then
|
|
552
|
+
response.status_code(404)
|
|
553
|
+
else
|
|
554
|
+
if
|
|
555
|
+
file.is_directory(fname)
|
|
556
|
+
then
|
|
557
|
+
if
|
|
558
|
+
not browse
|
|
559
|
+
then
|
|
560
|
+
response.status_code(403)
|
|
561
|
+
else
|
|
562
|
+
page = ref("")
|
|
563
|
+
base_url =
|
|
564
|
+
if
|
|
565
|
+
r/\/$/.test(request.path)
|
|
566
|
+
then
|
|
567
|
+
request.path
|
|
568
|
+
else
|
|
569
|
+
request.path ^ "/"
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def add(s) =
|
|
573
|
+
page := page() ^ s ^ "\n"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def add_file(f) =
|
|
577
|
+
add(
|
|
578
|
+
"<li><a href=\"#{base_url}#{url.encode(f)}\">#{f}</a></li>"
|
|
579
|
+
)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
add("<html><body><ul>")
|
|
583
|
+
list.iter(add_file, file.ls(sorted=true, fname))
|
|
584
|
+
add("</ul></body>")
|
|
585
|
+
response.content_type(
|
|
586
|
+
"text/html; charset=UTF-8"
|
|
587
|
+
)
|
|
588
|
+
response.data(string.getter.single(page()))
|
|
589
|
+
end
|
|
590
|
+
else
|
|
591
|
+
mime = content_type(fname)
|
|
592
|
+
if null.defined(mime) then response.content_type(null.get(mime)) end
|
|
593
|
+
if request.method == "GET" then response.data(file.read(fname)) end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
basepath = "#{basepath}.*"
|
|
600
|
+
|
|
601
|
+
def register(method) =
|
|
602
|
+
serve(method=method, basepath, handler)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
list.iter(register, ["OPTIONS", "HEAD", "GET"])
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# It seems that browsers want a trailing 0 for floats.
|
|
609
|
+
# @flag hidden
|
|
610
|
+
def http.string_of_float(x) =
|
|
611
|
+
s = string(x)
|
|
612
|
+
if r/\.$/.test(s) then "#{s}0" else s end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# @flag hidden
|
|
616
|
+
def get_mime_process(file) =
|
|
617
|
+
mime =
|
|
618
|
+
list.hd(
|
|
619
|
+
default="",
|
|
620
|
+
process.read.lines(
|
|
621
|
+
"file -b -I #{process.quote(file)}"
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if mime == "" then null else mime end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# @flag hidden
|
|
629
|
+
content_type = get_mime_process
|
|
630
|
+
%ifdef file.mime
|
|
631
|
+
# @flag hidden
|
|
632
|
+
content_type = file.mime
|
|
633
|
+
%endif
|
|
634
|
+
|
|
635
|
+
# Serve a static path.
|
|
636
|
+
# @category Internet
|
|
637
|
+
# @param ~port Port for incoming harbor (http) connections.
|
|
638
|
+
# @param ~transport Http transport. Use `http.transport.ssl` or http.transport.secure_transport`, when available, to enable HTTPS output
|
|
639
|
+
# @param ~path Base path.
|
|
640
|
+
# @param ~headers Default response headers.
|
|
641
|
+
# @param ~browse List files in directories.
|
|
642
|
+
# @param ~content_type Callback to specify Content-Type on a per file basis. Default: file.mime if compiled or file CLI if present.
|
|
643
|
+
# @param directory Local path to be served.
|
|
644
|
+
def replaces harbor.http.static(
|
|
645
|
+
~transport=http.transport.unix,
|
|
646
|
+
~port=8000,
|
|
647
|
+
~path="/",
|
|
648
|
+
~browse=false,
|
|
649
|
+
~content_type=(content_type : (string)->string?),
|
|
650
|
+
~headers=[("Access-Control-Allow-Origin", "*")],
|
|
651
|
+
directory
|
|
652
|
+
) =
|
|
653
|
+
# Make the method argument non-optional, see #1018
|
|
654
|
+
serve =
|
|
655
|
+
fun (~method, uri, handler) ->
|
|
656
|
+
harbor.http.register(
|
|
657
|
+
transport=transport,
|
|
658
|
+
port=port,
|
|
659
|
+
method=method,
|
|
660
|
+
uri,
|
|
661
|
+
handler
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
harbor.http.static.base(
|
|
665
|
+
serve,
|
|
666
|
+
content_type=content_type,
|
|
667
|
+
basepath=path,
|
|
668
|
+
browse=browse,
|
|
669
|
+
headers=headers,
|
|
670
|
+
directory
|
|
671
|
+
)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# @flag hidden
|
|
675
|
+
stdlib_file = file
|
|
676
|
+
|
|
677
|
+
# @flag hidden
|
|
678
|
+
upload_file_fn =
|
|
679
|
+
fun (
|
|
680
|
+
~name,
|
|
681
|
+
~content_type,
|
|
682
|
+
~headers,
|
|
683
|
+
~boundary,
|
|
684
|
+
~filename,
|
|
685
|
+
~file,
|
|
686
|
+
~contents,
|
|
687
|
+
~timeout,
|
|
688
|
+
~redirect,
|
|
689
|
+
url,
|
|
690
|
+
fn
|
|
691
|
+
) ->
|
|
692
|
+
begin
|
|
693
|
+
if
|
|
694
|
+
not null.defined(filename) and not null.defined(file)
|
|
695
|
+
then
|
|
696
|
+
error.raise(
|
|
697
|
+
error.http,
|
|
698
|
+
"At least one of: `file` or `filename` must be defined!"
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
if
|
|
703
|
+
null.defined(file) and null.defined(contents)
|
|
704
|
+
then
|
|
705
|
+
error.raise(
|
|
706
|
+
error.http,
|
|
707
|
+
"Only one of: `contents` or `file` must be defined!"
|
|
708
|
+
)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Massage parameters
|
|
712
|
+
filename =
|
|
713
|
+
null.defined(filename)
|
|
714
|
+
? null.get(filename)
|
|
715
|
+
: string(path.basename(null.get(file)))
|
|
716
|
+
|
|
717
|
+
contents =
|
|
718
|
+
null.defined(contents)
|
|
719
|
+
? null.get(contents)
|
|
720
|
+
: getter(stdlib_file.read(null.get(file)))
|
|
721
|
+
|
|
722
|
+
# Create query
|
|
723
|
+
content_type = content_type ?? "application/octet-stream"
|
|
724
|
+
data =
|
|
725
|
+
http.multipart_form_data(
|
|
726
|
+
boundary=boundary,
|
|
727
|
+
[
|
|
728
|
+
{
|
|
729
|
+
name = name,
|
|
730
|
+
attributes = [("filename", filename)],
|
|
731
|
+
headers = [("Content-Type", content_type)],
|
|
732
|
+
contents = contents
|
|
733
|
+
}
|
|
734
|
+
]
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
headers =
|
|
738
|
+
(
|
|
739
|
+
"Content-Type",
|
|
740
|
+
"multipart/form-data; boundary=#{data.boundary}"
|
|
741
|
+
)::headers
|
|
742
|
+
|
|
743
|
+
fn(
|
|
744
|
+
headers=headers,
|
|
745
|
+
timeout=timeout,
|
|
746
|
+
redirect=redirect,
|
|
747
|
+
data=data.contents,
|
|
748
|
+
url
|
|
749
|
+
)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
# Send a file via POST request encoded in multipart/form-data. The contents can
|
|
753
|
+
# either be directly specified (with the `contents` argument) or taken from a
|
|
754
|
+
# file (with the `file` argument).
|
|
755
|
+
# @category Internet
|
|
756
|
+
# @param ~name Name of the field field
|
|
757
|
+
# @param ~content_type Content-type (mime) for the file.
|
|
758
|
+
# @param ~headers Additional headers.
|
|
759
|
+
# @param ~boundary Specify boundary to use for multipart/form-data.
|
|
760
|
+
# @param ~filename File name sent in the request.
|
|
761
|
+
# @param ~file File whose contents is to be sent in the request.
|
|
762
|
+
# @param ~contents Contents of the file sent in the request.
|
|
763
|
+
# @param ~timeout Timeout in seconds.
|
|
764
|
+
# @param ~redirect Follow reidrections.
|
|
765
|
+
# @param url URL to post to.
|
|
766
|
+
def http.post.file(
|
|
767
|
+
~name="file",
|
|
768
|
+
~content_type=null,
|
|
769
|
+
~headers=[],
|
|
770
|
+
~boundary=null,
|
|
771
|
+
~filename=null,
|
|
772
|
+
~file=null,
|
|
773
|
+
~contents=null,
|
|
774
|
+
~timeout=null,
|
|
775
|
+
~redirect=true,
|
|
776
|
+
url
|
|
777
|
+
) =
|
|
778
|
+
upload_file_fn(
|
|
779
|
+
name=name,
|
|
780
|
+
content_type=content_type,
|
|
781
|
+
headers=headers,
|
|
782
|
+
boundary=boundary,
|
|
783
|
+
filename=filename,
|
|
784
|
+
file=file,
|
|
785
|
+
contents=contents,
|
|
786
|
+
timeout=timeout,
|
|
787
|
+
redirect=redirect,
|
|
788
|
+
url,
|
|
789
|
+
http.post
|
|
790
|
+
)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Send a file via PUT request encoded in multipart/form-data. The contents can
|
|
794
|
+
# either be directly specified (with the `contents` argument) or taken from a
|
|
795
|
+
# file (with the `file` argument).
|
|
796
|
+
# @category Internet
|
|
797
|
+
# @param ~name Name of the field field
|
|
798
|
+
# @param ~content_type Content-type (mime) for the file.
|
|
799
|
+
# @param ~headers Additional headers.
|
|
800
|
+
# @param ~boundary Specify boundary to use for multipart/form-data.
|
|
801
|
+
# @param ~filename File name sent in the request.
|
|
802
|
+
# @param ~file File whose contents is to be sent in the request.
|
|
803
|
+
# @param ~contents Contents of the file sent in the request.
|
|
804
|
+
# @param ~timeout Timeout in seconds.
|
|
805
|
+
# @param ~redirect Follow reidrections.
|
|
806
|
+
# @param url URL to put to.
|
|
807
|
+
def http.put.file(
|
|
808
|
+
~name="file",
|
|
809
|
+
~content_type=null,
|
|
810
|
+
~headers=[],
|
|
811
|
+
~boundary=null,
|
|
812
|
+
~filename=null,
|
|
813
|
+
~file=null,
|
|
814
|
+
~contents=null,
|
|
815
|
+
~timeout=null,
|
|
816
|
+
~redirect=true,
|
|
817
|
+
url
|
|
818
|
+
) =
|
|
819
|
+
upload_file_fn(
|
|
820
|
+
name=name,
|
|
821
|
+
content_type=content_type,
|
|
822
|
+
headers=headers,
|
|
823
|
+
boundary=boundary,
|
|
824
|
+
filename=filename,
|
|
825
|
+
file=file,
|
|
826
|
+
contents=contents,
|
|
827
|
+
timeout=timeout,
|
|
828
|
+
redirect=redirect,
|
|
829
|
+
url,
|
|
830
|
+
http.put
|
|
831
|
+
)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
let harbor.http.request = ()
|
|
835
|
+
let settings.http.mime =
|
|
836
|
+
settings.make.void(
|
|
837
|
+
"MIME-related settings for HTTP requests"
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
let settings.http.mime.extnames =
|
|
841
|
+
settings.make(
|
|
842
|
+
description="MIME to file extension mappings",
|
|
843
|
+
[
|
|
844
|
+
("application/mp4", ".mp4"),
|
|
845
|
+
("application/ogg", ".ogg"),
|
|
846
|
+
("application/pdf", ".pdf"),
|
|
847
|
+
("application/rss+xml", ".rss"),
|
|
848
|
+
("application/smil", ".smil"),
|
|
849
|
+
("application/smil+xml", ".smil"),
|
|
850
|
+
("application/x-cue", ".cue"),
|
|
851
|
+
("application/x-ogg", ".ogg"),
|
|
852
|
+
("application/xspf+xml", ".xspf"),
|
|
853
|
+
("audio/flac", ".flac"),
|
|
854
|
+
("audio/mp3", ".mp3"),
|
|
855
|
+
("audio/mp4", ".mp4"),
|
|
856
|
+
("audio/mpeg", ".mp3"),
|
|
857
|
+
("audio/mpegurl", ".m3u"),
|
|
858
|
+
("audio/ogg", ".ogg"),
|
|
859
|
+
("audio/vnd.wave", ".wav"),
|
|
860
|
+
("audio/wav", ".wav"),
|
|
861
|
+
("audio/wave", ".wav"),
|
|
862
|
+
("audio/x-flac", ".flac"),
|
|
863
|
+
("audio/x-mpegurl", ".m3u"),
|
|
864
|
+
("audio/x-ogg", ".ogg"),
|
|
865
|
+
("audio/x-scpls", ".pls"),
|
|
866
|
+
("audio/x-wav", ".wav"),
|
|
867
|
+
("image/bmp", ".bmp"),
|
|
868
|
+
("image/jpeg", ".jpg"),
|
|
869
|
+
("image/png", ".png"),
|
|
870
|
+
("text/plain", ".txt"),
|
|
871
|
+
("video/mp4", ".mp4"),
|
|
872
|
+
("video/ogg", ".ogg"),
|
|
873
|
+
("video/x-ms-asf", ".asf")
|
|
874
|
+
]
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
%ifndef file.mime
|
|
878
|
+
let file.mime = ()
|
|
879
|
+
%endif
|
|
880
|
+
|
|
881
|
+
# Return the file extension associated with the given
|
|
882
|
+
# content-type if it is known.
|
|
883
|
+
# @category File
|
|
884
|
+
def file.mime.extension(content_type) =
|
|
885
|
+
extnames = settings.http.mime.extnames()
|
|
886
|
+
extname = extnames[content_type]
|
|
887
|
+
extname == "" ? null : extname
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
let http.headers = ()
|
|
891
|
+
|
|
892
|
+
# Extract the content-type header
|
|
893
|
+
# @category Internet
|
|
894
|
+
def http.headers.content_type(headers) =
|
|
895
|
+
mime =
|
|
896
|
+
try
|
|
897
|
+
list.find(
|
|
898
|
+
fun (v) ->
|
|
899
|
+
begin
|
|
900
|
+
let (header_name, _) = v
|
|
901
|
+
string.case(lower=true, header_name) == "content-type"
|
|
902
|
+
end,
|
|
903
|
+
headers
|
|
904
|
+
)
|
|
905
|
+
catch _ : [error.not_found] do
|
|
906
|
+
null
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
mime = null.map(snd, mime)
|
|
910
|
+
null.map(
|
|
911
|
+
fun (mime) ->
|
|
912
|
+
begin
|
|
913
|
+
let [mime, ...args] =
|
|
914
|
+
list.map(string.trim, string.split(separator=";", mime))
|
|
915
|
+
|
|
916
|
+
def parse_arg(arg) =
|
|
917
|
+
let [name, ...value] = string.split(separator="=", arg)
|
|
918
|
+
(name, string.unquote(string.concat(separator="=", value)))
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
{mime = mime, args = list.map(parse_arg, args)}
|
|
922
|
+
end,
|
|
923
|
+
mime
|
|
924
|
+
)
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
# Extract the content-disposition header
|
|
928
|
+
# @category Internet
|
|
929
|
+
def http.headers.content_disposition(headers) =
|
|
930
|
+
content_disposition =
|
|
931
|
+
try
|
|
932
|
+
list.find(
|
|
933
|
+
fun (v) ->
|
|
934
|
+
begin
|
|
935
|
+
let (header_name, _) = v
|
|
936
|
+
string.case(lower=true, header_name) == "content-disposition"
|
|
937
|
+
end,
|
|
938
|
+
headers
|
|
939
|
+
)
|
|
940
|
+
catch _ : [error.not_found] do
|
|
941
|
+
null
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def parse_arg(arg) =
|
|
945
|
+
let [name, ...value] = string.split(separator="=", arg)
|
|
946
|
+
(name, string.unquote(string.concat(separator="=", value)))
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def parse_filename(args) =
|
|
950
|
+
plain_filename = args["filename"]
|
|
951
|
+
plain_filename = plain_filename == "" ? null : plain_filename
|
|
952
|
+
encoded_filename = args["filename*"]
|
|
953
|
+
encoded_filename = encoded_filename == "" ? null : encoded_filename
|
|
954
|
+
encoded_filename =
|
|
955
|
+
null.map(
|
|
956
|
+
fun (encoded_filename) ->
|
|
957
|
+
begin
|
|
958
|
+
let [encoding, _, filename] =
|
|
959
|
+
string.split(separator="'", encoded_filename)
|
|
960
|
+
|
|
961
|
+
string.recode(in_enc=encoding, filename)
|
|
962
|
+
end,
|
|
963
|
+
encoded_filename
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
filename =
|
|
967
|
+
null.defined(encoded_filename) ? encoded_filename : plain_filename
|
|
968
|
+
|
|
969
|
+
filename =
|
|
970
|
+
null.map(fun (filename) -> url.decode(string.unquote(filename)), filename)
|
|
971
|
+
|
|
972
|
+
(
|
|
973
|
+
filename,
|
|
974
|
+
list.filter(
|
|
975
|
+
fun (v) -> fst(v) != "filename" and fst(v) != "filename*",
|
|
976
|
+
args
|
|
977
|
+
)
|
|
978
|
+
)
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def parse_name(args) =
|
|
982
|
+
name = args["name"]
|
|
983
|
+
name = name == "" ? null : name
|
|
984
|
+
name = null.map(fun (name) -> url.decode(string.unquote(name)), name)
|
|
985
|
+
(name, list.filter(fun (v) -> fst(v) != "name", args))
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
null.map(
|
|
989
|
+
fun (v) ->
|
|
990
|
+
begin
|
|
991
|
+
let (_, header_value) = v
|
|
992
|
+
let [type, ...args] =
|
|
993
|
+
list.map(string.trim, string.split(separator=";", header_value))
|
|
994
|
+
|
|
995
|
+
args = list.map(parse_arg, args)
|
|
996
|
+
let (filename, args) = parse_filename(args)
|
|
997
|
+
let (name, args) = parse_name(args)
|
|
998
|
+
({type = type, filename = filename, name = name, args = args} :
|
|
999
|
+
{
|
|
1000
|
+
type: string,
|
|
1001
|
+
filename?: string,
|
|
1002
|
+
name?: string,
|
|
1003
|
+
args: [(string * string?)]
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
end,
|
|
1007
|
+
content_disposition
|
|
1008
|
+
)
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Try to get a filename from a request's headers.
|
|
1012
|
+
# @category Internet
|
|
1013
|
+
def http.headers.extname(headers) =
|
|
1014
|
+
content_disposition = http.headers.content_disposition(headers)
|
|
1015
|
+
content_type = http.headers.content_type(headers)
|
|
1016
|
+
extname =
|
|
1017
|
+
if
|
|
1018
|
+
null.defined(content_disposition?.filename)
|
|
1019
|
+
then
|
|
1020
|
+
extname = file.extension(null.get(content_disposition?.filename))
|
|
1021
|
+
extname == "" ? null : extname
|
|
1022
|
+
else
|
|
1023
|
+
null
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
if
|
|
1027
|
+
null.defined(extname)
|
|
1028
|
+
then
|
|
1029
|
+
extname
|
|
1030
|
+
elsif
|
|
1031
|
+
null.defined(content_type)
|
|
1032
|
+
then
|
|
1033
|
+
file.mime.extension(null.get(content_type).mime)
|
|
1034
|
+
else
|
|
1035
|
+
null
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
%ifdef input.harbor.dynamic.regexp
|
|
1040
|
+
# @docof input.harbor.dynamic.regexp
|
|
1041
|
+
# @argsof input.harbor.dynamic.regexp
|
|
1042
|
+
def replaces input.harbor.dynamic(%argsof(input.harbor.dynamic.regexp), path) =
|
|
1043
|
+
input.harbor.dynamic.regexp(
|
|
1044
|
+
%argsof(input.harbor.dynamic.regexp),
|
|
1045
|
+
harbor.http.regexp_of_path(path)
|
|
1046
|
+
)
|
|
1047
|
+
end
|
|
1048
|
+
%endif
|