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.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. 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