mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2025-12-07 03:35:34 -05:00
Merge branch 'wip-virtual-dir'
This commit is contained in:
commit
c68ec5c2f6
17 changed files with 599 additions and 57 deletions
8
.github/workflows/main.yml
vendored
8
.github/workflows/main.yml
vendored
|
|
@ -10,7 +10,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- macos-latest
|
- macos-latest
|
||||||
|
|
@ -25,6 +25,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
||||||
uses: ocaml/setup-ocaml@v2
|
uses: ocaml/setup-ocaml@v2
|
||||||
|
|
@ -34,7 +36,7 @@ jobs:
|
||||||
|
|
||||||
- run: opam install . --deps-only --with-test
|
- run: opam install . --deps-only --with-test
|
||||||
|
|
||||||
- run: opam exec -- dune build @install
|
- run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip,curly
|
||||||
|
|
||||||
- run: opam exec -- dune runtest
|
- run: opam exec -- dune build @src/runtest @examples/runtest
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "vendor/curly"]
|
||||||
|
path = vendor/curly
|
||||||
|
url = https://github.com/rgrinberg/curly.git
|
||||||
40
README.md
40
README.md
|
|
@ -1,7 +1,8 @@
|
||||||
# Tiny_httpd [](https://github.com/c-cube/tiny_httpd/actions)
|
# Tiny_httpd [](https://github.com/c-cube/tiny_httpd/actions)
|
||||||
|
|
||||||
Minimal HTTP server using good old threads, with stream abstractions,
|
Minimal HTTP server using good old threads, with stream abstractions,
|
||||||
simple routing, URL encoding/decoding, and optional compression with camlzip.
|
simple routing, URL encoding/decoding, static asset serving,
|
||||||
|
and optional compression with camlzip.
|
||||||
It also supports [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
It also supports [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||||
([w3c](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation))
|
([w3c](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation))
|
||||||
|
|
||||||
|
|
@ -67,6 +68,43 @@ $ curl -X GET http://localhost:8080
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Static assets and files
|
||||||
|
|
||||||
|
The program `http_of_dir` relies on the module `Tiny_httpd_dir`, which
|
||||||
|
can serve directories, as well as _virtual file systems_.
|
||||||
|
|
||||||
|
In 'examples/dune', we produce an OCaml module `vfs.ml` using
|
||||||
|
the program `tiny-httpd-vfs-pack`. This module contains a VFS (virtual file
|
||||||
|
system) which can be served as if it were an actual directory.
|
||||||
|
|
||||||
|
The dune rule:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(rule
|
||||||
|
(targets vfs.ml)
|
||||||
|
(deps (source_tree files) (:out test_output.txt.expected))
|
||||||
|
(enabled_if (= %{system} "linux"))
|
||||||
|
(action (run ../src/bin/vfs_pack.exe -o %{targets}
|
||||||
|
--mirror=files/
|
||||||
|
--file=test_out.txt,%{out}
|
||||||
|
--url=example_dot_com,http://example.com)))
|
||||||
|
```
|
||||||
|
|
||||||
|
The code to serve the VFS from `vfs.ml` is as follows:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
…
|
||||||
|
Tiny_httpd_dir.add_vfs server
|
||||||
|
~config:(Tiny_httpd_dir.config ~download:true
|
||||||
|
~dir_behavior:Tiny_httpd_dir.Index_or_lists ())
|
||||||
|
~vfs:Vfs.vfs ~prefix:"vfs";
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
it allows downloading the files, and listing directories.
|
||||||
|
If a directory contains `index.html` then this will be served
|
||||||
|
instead of listing the content.
|
||||||
|
|
||||||
## Socket activation
|
## Socket activation
|
||||||
|
|
||||||
Since version 0.10, socket activation is supported indirectly, by allowing a
|
Since version 0.10, socket activation is supported indirectly, by allowing a
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
(executable
|
(executable
|
||||||
(name echo)
|
(name echo)
|
||||||
(flags :standard -warn-error -a+8)
|
(flags :standard -warn-error -a+8)
|
||||||
(modules echo)
|
(modules echo vfs)
|
||||||
(libraries tiny_httpd tiny_httpd_camlzip))
|
(libraries tiny_httpd tiny_httpd_camlzip))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
|
|
@ -30,3 +30,23 @@
|
||||||
(deps test_output.txt)
|
(deps test_output.txt)
|
||||||
(action
|
(action
|
||||||
(diff test_output.txt.expected test_output.txt)))
|
(diff test_output.txt.expected test_output.txt)))
|
||||||
|
|
||||||
|
; produce an embedded FS
|
||||||
|
(rule
|
||||||
|
(targets vfs.ml)
|
||||||
|
(deps (source_tree files) (:out test_output.txt.expected))
|
||||||
|
(enabled_if (= %{system} "linux"))
|
||||||
|
(action (run ../src/bin/vfs_pack.exe -o %{targets}
|
||||||
|
--mirror=files/
|
||||||
|
--file=test_out.txt,%{out}
|
||||||
|
--url=example_dot_com,http://example.com)))
|
||||||
|
|
||||||
|
(rule
|
||||||
|
(targets vfs.ml)
|
||||||
|
(enabled_if (<> %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(progn
|
||||||
|
(echo "let embedded_fs = Tiny_httpd_dir.Embedded_fs.create ~mtime:0. ()")
|
||||||
|
(echo "let vfs = Tiny_httpd_dir.Embedded_fs.to_vfs embedded_fs")))))
|
||||||
|
|
|
||||||
|
|
@ -107,16 +107,23 @@ let () =
|
||||||
S.Response.make_string @@ Ok stats
|
S.Response.make_string @@ Ok stats
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(* VFS *)
|
||||||
|
Tiny_httpd_dir.add_vfs server
|
||||||
|
~config:(Tiny_httpd_dir.config ~download:true
|
||||||
|
~dir_behavior:Tiny_httpd_dir.Index_or_lists ())
|
||||||
|
~vfs:Vfs.vfs ~prefix:"vfs";
|
||||||
|
|
||||||
(* main page *)
|
(* main page *)
|
||||||
S.add_route_handler server S.Route.(return)
|
S.add_route_handler server S.Route.(return)
|
||||||
(fun _req ->
|
(fun _req ->
|
||||||
let s = "<head></head><body>\n\
|
let s = "<head></head><body>\n\
|
||||||
<p><b>welcome!</b>\n<p>endpoints are:\n<ul>\
|
<p><b>welcome!</b>\n<p>endpoints are:\n<ul>\
|
||||||
<li><pre>/hello/'name' (GET)</pre></li>\n\
|
<li><pre>/hello/'name' (GET)</pre></li>\n\
|
||||||
<li><pre>/echo/ (GET) echoes back query</pre></li>\n\
|
<li><pre><a href=\"/echo/\">/echo/</a> (GET) echoes back query</pre></li>\n\
|
||||||
<li><pre>/upload/'path' (PUT) to upload a file</pre></li>\n\
|
<li><pre>/upload/'path' (PUT) to upload a file</pre></li>\n\
|
||||||
<li><pre>/zcat/'path' (GET) to download a file (compressed)</pre></li>\n\
|
<li><pre>/zcat/'path' (GET) to download a file (compressed)</pre></li>\n\
|
||||||
<li><pre>/stats/ (GET) to access statistics</pre></li>\n\
|
<li><pre>/stats/ (GET) to access statistics</pre></li>\n\
|
||||||
|
<li><pre><a href=\"/vfs/\">/vfs/</a> (GET) to access statistics</pre></li>\n\
|
||||||
</ul></body>"
|
</ul></body>"
|
||||||
in
|
in
|
||||||
S.Response.make_string ~headers:["content-type", "text/html"] @@ Ok s);
|
S.Response.make_string ~headers:["content-type", "text/html"] @@ Ok s);
|
||||||
|
|
|
||||||
2
examples/files/a.txt
Normal file
2
examples/files/a.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
hello
|
||||||
|
world
|
||||||
18
examples/files/foo.html
Normal file
18
examples/files/foo.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1> hello! </h1>
|
||||||
|
<ul>
|
||||||
|
<li> item 1 </li>
|
||||||
|
<li> item 2 </li>
|
||||||
|
<li> item 3 </li>
|
||||||
|
<li> item 4 </li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="."> escape from this file </a>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
examples/files/sub/b.txt
Normal file
1
examples/files/sub/b.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
lorem ipsum etc.
|
||||||
18
examples/files/sub/yolo.html
Normal file
18
examples/files/sub/yolo.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2> funky: </h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="../"> go up!! </a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../foo.html"> up/foo </a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -5,22 +5,36 @@ module Pf = Printf
|
||||||
type dir_behavior =
|
type dir_behavior =
|
||||||
| Index | Lists | Index_or_lists | Forbidden
|
| Index | Lists | Index_or_lists | Forbidden
|
||||||
|
|
||||||
|
type hidden = unit
|
||||||
type config = {
|
type config = {
|
||||||
mutable download: bool;
|
mutable download: bool;
|
||||||
mutable dir_behavior: dir_behavior;
|
mutable dir_behavior: dir_behavior;
|
||||||
mutable delete: bool;
|
mutable delete: bool;
|
||||||
mutable upload: bool;
|
mutable upload: bool;
|
||||||
mutable max_upload_size: int;
|
mutable max_upload_size: int;
|
||||||
|
_rest: hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_config () : config =
|
let default_config_ : config =
|
||||||
{ download=true;
|
{ download=true;
|
||||||
dir_behavior=Forbidden;
|
dir_behavior=Forbidden;
|
||||||
delete=false;
|
delete=false;
|
||||||
upload=false;
|
upload=false;
|
||||||
max_upload_size = 10 * 1024 * 1024;
|
max_upload_size = 10 * 1024 * 1024;
|
||||||
|
_rest=();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let default_config () = default_config_
|
||||||
|
let config
|
||||||
|
?(download=default_config_.download)
|
||||||
|
?(dir_behavior=default_config_.dir_behavior)
|
||||||
|
?(delete=default_config_.delete)
|
||||||
|
?(upload=default_config_.upload)
|
||||||
|
?(max_upload_size=default_config_.max_upload_size)
|
||||||
|
() : config =
|
||||||
|
{ download; dir_behavior; delete; upload; max_upload_size;
|
||||||
|
_rest=()}
|
||||||
|
|
||||||
let contains_dot_dot s =
|
let contains_dot_dot s =
|
||||||
try
|
try
|
||||||
String.iteri
|
String.iteri
|
||||||
|
|
@ -45,20 +59,59 @@ let _decode_path s = match U.percent_decode s with Some s->s | None -> s
|
||||||
|
|
||||||
let is_hidden s = String.length s>0 && s.[0] = '.'
|
let is_hidden s = String.length s>0 && s.[0] = '.'
|
||||||
|
|
||||||
let html_list_dir ~top ~parent d : string =
|
module type VFS = sig
|
||||||
let entries = Sys.readdir @@ (top // d) in
|
val descr : string
|
||||||
|
val is_directory : string -> bool
|
||||||
|
val contains : string -> bool
|
||||||
|
val list_dir : string -> string array
|
||||||
|
val delete : string -> unit
|
||||||
|
val create : string -> (bytes -> int -> int -> unit) * (unit -> unit)
|
||||||
|
val read_file_content : string -> Tiny_httpd.Byte_stream.t
|
||||||
|
val file_size : string -> int option
|
||||||
|
val file_mtime : string -> float option
|
||||||
|
end
|
||||||
|
|
||||||
|
type vfs = (module VFS)
|
||||||
|
|
||||||
|
let vfs_of_dir (top:string) : vfs =
|
||||||
|
let module M = struct
|
||||||
|
let descr = top
|
||||||
|
let (//) = Filename.concat
|
||||||
|
let is_directory f = Sys.is_directory (top // f)
|
||||||
|
let contains f = Sys.file_exists (top // f)
|
||||||
|
let list_dir f = Sys.readdir (top // f)
|
||||||
|
let read_file_content f =
|
||||||
|
let ic = open_in_bin (top // f) in
|
||||||
|
S.Byte_stream.of_chan ic
|
||||||
|
let create f =
|
||||||
|
let oc = open_out_bin (top // f) in
|
||||||
|
let write = output oc in
|
||||||
|
let close() = close_out oc in
|
||||||
|
write, close
|
||||||
|
let delete f = Sys.remove (top // f)
|
||||||
|
let file_size f =
|
||||||
|
try Some (Unix.stat (top // f)).Unix.st_size
|
||||||
|
with _ -> None
|
||||||
|
let file_mtime f =
|
||||||
|
try Some (Unix.stat (top // f)).Unix.st_mtime
|
||||||
|
with _ -> None
|
||||||
|
end in
|
||||||
|
(module M)
|
||||||
|
|
||||||
|
let html_list_dir (module VFS:VFS) ~prefix ~parent d : string =
|
||||||
|
let entries = VFS.list_dir d in
|
||||||
Array.sort compare entries;
|
Array.sort compare entries;
|
||||||
let body = Buffer.create 256 in
|
let body = Buffer.create 256 in
|
||||||
(* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *)
|
(* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *)
|
||||||
Printf.bprintf body {|<head><title> http_of_dir %S</title><meta charset="utf-8">
|
Printf.bprintf body {|<head><title> list directory %S</title><meta charset="utf-8">
|
||||||
</head><body>
|
</head><body>
|
||||||
<h2> Index of %S</h2>
|
<h2> Index of %S</h2>
|
||||||
|} top d;
|
|} VFS.descr d;
|
||||||
begin match parent with
|
begin match parent with
|
||||||
| None -> ()
|
| None -> ()
|
||||||
| Some p ->
|
| Some p ->
|
||||||
Printf.bprintf body "<a href=\"/%s\"> (parent directory) </a>\n"
|
Printf.bprintf body "<a href=\"/%s\"> (parent directory) </a>\n"
|
||||||
(encode_path p);
|
(encode_path (prefix // p));
|
||||||
end;
|
end;
|
||||||
Printf.bprintf body "<ul>\n";
|
Printf.bprintf body "<ul>\n";
|
||||||
let hidden_stop = ref 0 in
|
let hidden_stop = ref 0 in
|
||||||
|
|
@ -74,16 +127,18 @@ let html_list_dir ~top ~parent d : string =
|
||||||
Printf.bprintf body "</details/>\n";
|
Printf.bprintf body "</details/>\n";
|
||||||
);
|
);
|
||||||
if not @@ contains_dot_dot (d // f) then (
|
if not @@ contains_dot_dot (d // f) then (
|
||||||
let fpath = top // d // f in
|
let fpath = d // f in
|
||||||
if not @@ Sys.file_exists fpath then (
|
if not @@ VFS.contains fpath then (
|
||||||
Printf.bprintf body " <li> %s [invalid file]</li>\n" f
|
Printf.bprintf body " <li> %s [invalid file]</li>\n" f
|
||||||
) else (
|
) else (
|
||||||
let size =
|
let size =
|
||||||
try Printf.sprintf " (%s)" @@ human_size (Unix.stat fpath).Unix.st_size
|
match VFS.file_size fpath with
|
||||||
with _ -> ""
|
| Some f -> Printf.sprintf " (%s)" @@ human_size f
|
||||||
|
| None -> ""
|
||||||
in
|
in
|
||||||
Printf.bprintf body " <li> <a href=\"/%s\"> %s </a> %s%s </li>\n"
|
Printf.bprintf body " <li> <a href=\"/%s\"> %s </a> %s%s </li>\n"
|
||||||
(encode_path (d // f)) f (if Sys.is_directory fpath then "[dir]" else "") size
|
(encode_path (prefix // fpath)) f
|
||||||
|
(if VFS.is_directory fpath then "[dir]" else "") size
|
||||||
);
|
);
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -100,30 +155,32 @@ let finally_ ~h x f =
|
||||||
h x;
|
h x;
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
let add_dir_path ~config ~dir ~prefix server =
|
(* @param on_fs: if true, we assume the file exists on the FS *)
|
||||||
|
let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS:VFS) as vfs) ~prefix server : unit=
|
||||||
|
|
||||||
|
let route () =
|
||||||
|
if prefix="" then S.Route.rest_of_path_urlencoded
|
||||||
|
else S.Route.exact_path prefix S.Route.rest_of_path_urlencoded
|
||||||
|
in
|
||||||
if config.delete then (
|
if config.delete then (
|
||||||
S.add_route_handler server ~meth:`DELETE
|
S.add_route_handler server ~meth:`DELETE (route())
|
||||||
S.Route.(exact_path prefix (rest_of_path_urlencoded))
|
|
||||||
(fun path _req ->
|
(fun path _req ->
|
||||||
if contains_dot_dot path then (
|
if contains_dot_dot path then (
|
||||||
S.Response.fail_raise ~code:403 "invalid path in delete"
|
S.Response.fail_raise ~code:403 "invalid path in delete"
|
||||||
) else (
|
) else (
|
||||||
S.Response.make_string
|
S.Response.make_string
|
||||||
(try
|
(try
|
||||||
Sys.remove (dir // path); Ok "file deleted successfully"
|
VFS.delete path; Ok "file deleted successfully"
|
||||||
with e -> Error (500, Printexc.to_string e))
|
with e -> Error (500, Printexc.to_string e))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
) else (
|
) else (
|
||||||
S.add_route_handler server ~meth:`DELETE
|
S.add_route_handler server ~meth:`DELETE (route())
|
||||||
S.Route.(exact_path prefix (S.Route.(string @/ return)))
|
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "delete not allowed");
|
(fun _ _ -> S.Response.make_raw ~code:405 "delete not allowed");
|
||||||
);
|
);
|
||||||
|
|
||||||
if config.upload then (
|
if config.upload then (
|
||||||
S.add_route_handler_stream server ~meth:`PUT
|
S.add_route_handler_stream server ~meth:`PUT (route())
|
||||||
S.Route.(exact_path prefix (rest_of_path_urlencoded))
|
|
||||||
~accept:(fun req ->
|
~accept:(fun req ->
|
||||||
match S.Request.get_header_int req "Content-Length" with
|
match S.Request.get_header_int req "Content-Length" with
|
||||||
| Some n when n > config.max_upload_size ->
|
| Some n when n > config.max_upload_size ->
|
||||||
|
|
@ -133,56 +190,52 @@ let add_dir_path ~config ~dir ~prefix server =
|
||||||
| _ -> Ok ()
|
| _ -> Ok ()
|
||||||
)
|
)
|
||||||
(fun path req ->
|
(fun path req ->
|
||||||
let fpath = dir // path in
|
let write, close =
|
||||||
let oc =
|
try VFS.create path
|
||||||
try open_out fpath
|
|
||||||
with e ->
|
with e ->
|
||||||
S.Response.fail_raise ~code:403 "cannot upload to %S: %s"
|
S.Response.fail_raise ~code:403 "cannot upload to %S: %s"
|
||||||
path (Printexc.to_string e)
|
path (Printexc.to_string e)
|
||||||
in
|
in
|
||||||
let req = S.Request.limit_body_size ~max_size:config.max_upload_size req in
|
let req = S.Request.limit_body_size ~max_size:config.max_upload_size req in
|
||||||
S.Byte_stream.to_chan oc req.S.Request.body;
|
S.Byte_stream.iter write req.S.Request.body;
|
||||||
flush oc;
|
close ();
|
||||||
close_out oc;
|
|
||||||
S._debug (fun k->k "done uploading");
|
S._debug (fun k->k "done uploading");
|
||||||
S.Response.make_raw ~code:201 "upload successful"
|
S.Response.make_raw ~code:201 "upload successful"
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
S.add_route_handler server ~meth:`PUT
|
S.add_route_handler server ~meth:`PUT (route())
|
||||||
S.Route.(exact_path prefix (string @/ return))
|
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "upload not allowed");
|
(fun _ _ -> S.Response.make_raw ~code:405 "upload not allowed");
|
||||||
);
|
);
|
||||||
|
|
||||||
if config.download then (
|
if config.download then (
|
||||||
S.add_route_handler server ~meth:`GET
|
S.add_route_handler server ~meth:`GET (route())
|
||||||
S.Route.(exact_path prefix (rest_of_path_urlencoded))
|
|
||||||
(fun path req ->
|
(fun path req ->
|
||||||
let full_path = dir // path in
|
S._debug (fun k->k "path=%S" path);
|
||||||
let mtime = lazy (
|
let mtime = lazy (
|
||||||
try Printf.sprintf "mtime: %f" (Unix.stat full_path).Unix.st_mtime
|
match VFS.file_mtime path with
|
||||||
with _ -> S.Response.fail_raise ~code:403 "Cannot access file"
|
| None -> S.Response.fail_raise ~code:403 "Cannot access file"
|
||||||
|
| Some t -> Printf.sprintf "mtime: %.4f" t
|
||||||
) in
|
) in
|
||||||
if contains_dot_dot full_path then (
|
if contains_dot_dot path then (
|
||||||
S.Response.fail ~code:403 "Path is forbidden";
|
S.Response.fail ~code:403 "Path is forbidden";
|
||||||
) else if not (Sys.file_exists full_path) then (
|
) else if not (VFS.contains path) then (
|
||||||
S.Response.fail ~code:404 "File not found";
|
S.Response.fail ~code:404 "File not found";
|
||||||
) else if S.Request.get_header req "If-None-Match" = Some (Lazy.force mtime) then (
|
) else if S.Request.get_header req "If-None-Match" = Some (Lazy.force mtime) then (
|
||||||
S._debug (fun k->k "cached object %S (etag: %S)" path (Lazy.force mtime));
|
S._debug (fun k->k "cached object %S (etag: %S)" path (Lazy.force mtime));
|
||||||
S.Response.make_raw ~code:304 ""
|
S.Response.make_raw ~code:304 ""
|
||||||
) else if Sys.is_directory full_path then (
|
) else if VFS.is_directory path then (
|
||||||
S._debug (fun k->k "list dir %S (topdir %S)" full_path dir);
|
S._debug (fun k->k "list dir %S (topdir %S)" path VFS.descr);
|
||||||
let parent = Filename.(dirname path) in
|
let parent = Filename.(dirname path) in
|
||||||
let parent = if parent <> path then Some parent else None in
|
let parent = if parent <> "." && parent <> path then Some parent else None in
|
||||||
match config.dir_behavior with
|
match config.dir_behavior with
|
||||||
| Index | Index_or_lists when
|
| Index | Index_or_lists when VFS.contains (path // "index.html") ->
|
||||||
Sys.file_exists (full_path // "index.html") ->
|
|
||||||
(* redirect using path, not full path *)
|
(* redirect using path, not full path *)
|
||||||
let new_path = "/" // path // "index.html" in
|
let new_path = "/" // path // "index.html" in
|
||||||
S._debug (fun k->k "redirect to `%s`" new_path);
|
S._debug (fun k->k "redirect to `%s`" new_path);
|
||||||
S.Response.make_raw ~code:301 ""
|
S.Response.make_raw ~code:301 ""
|
||||||
~headers:S.Headers.(empty |> set "location" new_path)
|
~headers:S.Headers.(empty |> set "location" new_path)
|
||||||
| Lists | Index_or_lists ->
|
| Lists | Index_or_lists ->
|
||||||
let body = html_list_dir ~top:dir path ~parent in
|
let body = html_list_dir ~prefix vfs path ~parent in
|
||||||
S.Response.make_string
|
S.Response.make_string
|
||||||
~headers:[header_html; "ETag", Lazy.force mtime]
|
~headers:[header_html; "ETag", Lazy.force mtime]
|
||||||
(Ok body)
|
(Ok body)
|
||||||
|
|
@ -190,28 +243,142 @@ let add_dir_path ~config ~dir ~prefix server =
|
||||||
S.Response.make_raw ~code:405 "listing dir not allowed"
|
S.Response.make_raw ~code:405 "listing dir not allowed"
|
||||||
) else (
|
) else (
|
||||||
try
|
try
|
||||||
let ic = open_in full_path in
|
|
||||||
let mime_type =
|
let mime_type =
|
||||||
if Filename.extension full_path = ".css" then (
|
if Filename.extension path = ".css" then (
|
||||||
["Content-Type", "text/css"]
|
["Content-Type", "text/css"]
|
||||||
) else if Filename.extension full_path = ".js" then (
|
) else if Filename.extension path = ".js" then (
|
||||||
["Content-Type", "text/javascript"]
|
["Content-Type", "text/javascript"]
|
||||||
) else try
|
) else if on_fs then (
|
||||||
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" full_path) in
|
(* call "file" util *)
|
||||||
|
try
|
||||||
|
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" (top // path)) in
|
||||||
finally_ ~h:(fun p->ignore @@ Unix.close_process_in p) p
|
finally_ ~h:(fun p->ignore @@ Unix.close_process_in p) p
|
||||||
(fun p ->
|
(fun p ->
|
||||||
try ["Content-Type", String.trim (input_line p)]
|
try ["Content-Type", String.trim (input_line p)]
|
||||||
with _ -> [])
|
with _ -> [])
|
||||||
with _ -> []
|
with _ -> []
|
||||||
|
) else []
|
||||||
in
|
in
|
||||||
|
let stream = VFS.read_file_content path in
|
||||||
S.Response.make_raw_stream
|
S.Response.make_raw_stream
|
||||||
~headers:(mime_type@["Etag", Lazy.force mtime])
|
~headers:(mime_type@["Etag", Lazy.force mtime])
|
||||||
~code:200 (S.Byte_stream.of_chan ic)
|
~code:200 stream
|
||||||
with e ->
|
with e ->
|
||||||
S.Response.fail ~code:500 "error while reading file: %s" (Printexc.to_string e))
|
S.Response.fail ~code:500 "error while reading file: %s" (Printexc.to_string e))
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
S.add_route_handler server ~meth:`GET
|
S.add_route_handler server ~meth:`GET (route())
|
||||||
S.Route.(exact_path prefix (string @/ return))
|
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "download not allowed");
|
(fun _ _ -> S.Response.make_raw ~code:405 "download not allowed");
|
||||||
);
|
);
|
||||||
|
()
|
||||||
|
|
||||||
|
let add_vfs ~config ~vfs ~prefix server : unit =
|
||||||
|
add_vfs_ ~on_fs:false ~top:"." ~config ~prefix ~vfs server
|
||||||
|
|
||||||
|
let add_dir_path ~config ~dir ~prefix server : unit =
|
||||||
|
add_vfs_ ~on_fs:true ~top:dir ~config ~prefix ~vfs:(vfs_of_dir dir) server
|
||||||
|
|
||||||
|
module Embedded_fs = struct
|
||||||
|
module Str_map = Map.Make(String)
|
||||||
|
|
||||||
|
type t = {
|
||||||
|
mtime: float;
|
||||||
|
mutable entries: entry Str_map.t
|
||||||
|
}
|
||||||
|
|
||||||
|
and entry =
|
||||||
|
| File of {
|
||||||
|
content: string;
|
||||||
|
mtime: float;
|
||||||
|
}
|
||||||
|
| Dir of t
|
||||||
|
|
||||||
|
let create ?(mtime=Unix.gettimeofday()) () : t = {
|
||||||
|
mtime;
|
||||||
|
entries=Str_map.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_path_ (path:string) : string list * string =
|
||||||
|
let basename = Filename.basename path in
|
||||||
|
let dirname =
|
||||||
|
Filename.dirname path
|
||||||
|
|> String.split_on_char '/'
|
||||||
|
|> List.filter (function "" | "." -> false | _ -> true) in
|
||||||
|
dirname, basename
|
||||||
|
|
||||||
|
let add_file ?mtime (self:t) ~path content : unit =
|
||||||
|
let mtime = match mtime with Some t -> t | None -> self.mtime in
|
||||||
|
let dir_path, basename = split_path_ path in
|
||||||
|
if List.mem ".." dir_path then (
|
||||||
|
invalid_arg "add_file: '..' is not allowed";
|
||||||
|
);
|
||||||
|
|
||||||
|
let rec loop self dir = match dir with
|
||||||
|
| [] ->
|
||||||
|
self.entries <- Str_map.add basename (File {mtime; content}) self.entries
|
||||||
|
| d :: ds ->
|
||||||
|
let sub =
|
||||||
|
match Str_map.find d self.entries with
|
||||||
|
| Dir sub -> sub
|
||||||
|
| File _ ->
|
||||||
|
invalid_arg
|
||||||
|
(Printf.sprintf "in path %S, %S is a file, not a directory" path d)
|
||||||
|
| exception Not_found ->
|
||||||
|
let sub = create ~mtime:self.mtime () in
|
||||||
|
self.entries <- Str_map.add d (Dir sub) self.entries;
|
||||||
|
sub
|
||||||
|
in
|
||||||
|
loop sub ds
|
||||||
|
in
|
||||||
|
loop self dir_path
|
||||||
|
|
||||||
|
(* find entry *)
|
||||||
|
let find_ self path : entry option =
|
||||||
|
let dir_path, basename = split_path_ path in
|
||||||
|
let rec loop self dir_name = match dir_name with
|
||||||
|
| [] -> (try Some (Str_map.find basename self.entries) with _ -> None)
|
||||||
|
| d :: ds ->
|
||||||
|
match Str_map.find d self.entries with
|
||||||
|
| exception Not_found -> None
|
||||||
|
| File _ -> None
|
||||||
|
| Dir sub -> loop sub ds
|
||||||
|
in
|
||||||
|
if path="" then Some (Dir self)
|
||||||
|
else loop self dir_path
|
||||||
|
|
||||||
|
let to_vfs self : vfs =
|
||||||
|
let module M = struct
|
||||||
|
let descr = "Embedded_fs"
|
||||||
|
let file_mtime p = match find_ self p with
|
||||||
|
| Some (File {mtime;_}) -> Some mtime
|
||||||
|
| Some (Dir _) -> Some self.mtime
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
let file_size p = match find_ self p with
|
||||||
|
| Some (File {content;_}) -> Some (String.length content)
|
||||||
|
| _ -> None
|
||||||
|
|
||||||
|
let contains p = S._debug (fun k->k "contains %S" p); match find_ self p with
|
||||||
|
| Some _ -> true
|
||||||
|
| None -> false
|
||||||
|
|
||||||
|
let is_directory p = match find_ self p with
|
||||||
|
| Some (Dir _) -> true
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
let read_file_content p = match find_ self p with
|
||||||
|
| Some (File {content;_}) -> Tiny_httpd.Byte_stream.of_string content
|
||||||
|
| _ -> failwith (Printf.sprintf "no such file: %S" p)
|
||||||
|
|
||||||
|
let list_dir p = S._debug (fun k->k "list dir %S" p); match find_ self p with
|
||||||
|
| Some (Dir sub) ->
|
||||||
|
Str_map.fold (fun sub _ acc -> sub::acc) sub.entries [] |> Array.of_list
|
||||||
|
| _ -> failwith (Printf.sprintf "no such directory: %S" p)
|
||||||
|
|
||||||
|
let create _ = failwith "Embedded_fs is read-only"
|
||||||
|
let delete _ = failwith "Embedded_fs is read-only"
|
||||||
|
|
||||||
|
end in (module M)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ type dir_behavior =
|
||||||
| Forbidden
|
| Forbidden
|
||||||
(** Forbid access to directory. This is suited for serving assets, for example. *)
|
(** Forbid access to directory. This is suited for serving assets, for example. *)
|
||||||
|
|
||||||
(** configuration for static file handlers *)
|
type hidden
|
||||||
|
(** Type used to prevent users from building a config directly.
|
||||||
|
Use {!default_config} or {!config} instead. *)
|
||||||
|
|
||||||
|
(** configuration for static file handlers. This might get
|
||||||
|
more fields over time. *)
|
||||||
type config = {
|
type config = {
|
||||||
mutable download: bool;
|
mutable download: bool;
|
||||||
(** Is downloading files allowed? *)
|
(** Is downloading files allowed? *)
|
||||||
|
|
@ -40,6 +45,9 @@ type config = {
|
||||||
mutable max_upload_size: int;
|
mutable max_upload_size: int;
|
||||||
(** If {!upload} is true, this is the maximum size in bytes for
|
(** If {!upload} is true, this is the maximum size in bytes for
|
||||||
uploaded files. *)
|
uploaded files. *)
|
||||||
|
|
||||||
|
_rest: hidden;
|
||||||
|
(** Just ignore this field. *)
|
||||||
}
|
}
|
||||||
|
|
||||||
(** default configuration: [
|
(** default configuration: [
|
||||||
|
|
@ -51,6 +59,17 @@ type config = {
|
||||||
}] *)
|
}] *)
|
||||||
val default_config : unit -> config
|
val default_config : unit -> config
|
||||||
|
|
||||||
|
val config :
|
||||||
|
?download:bool ->
|
||||||
|
?dir_behavior:dir_behavior ->
|
||||||
|
?delete:bool ->
|
||||||
|
?upload:bool ->
|
||||||
|
?max_upload_size:int ->
|
||||||
|
unit ->
|
||||||
|
config
|
||||||
|
(** Build a config from {!default_config}.
|
||||||
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
(** [add_dirpath ~config ~dir ~prefix server] adds route handle to the
|
(** [add_dirpath ~config ~dir ~prefix server] adds route handle to the
|
||||||
[server] to serve static files in [dir] when url starts with [prefix],
|
[server] to serve static files in [dir] when url starts with [prefix],
|
||||||
using the given configuration [config]. *)
|
using the given configuration [config]. *)
|
||||||
|
|
@ -59,3 +78,74 @@ val add_dir_path :
|
||||||
dir:string ->
|
dir:string ->
|
||||||
prefix:string ->
|
prefix:string ->
|
||||||
Tiny_httpd.t -> unit
|
Tiny_httpd.t -> unit
|
||||||
|
|
||||||
|
(** Virtual file system.
|
||||||
|
|
||||||
|
This is used to emulate a file system from pure OCaml functions and data,
|
||||||
|
e.g. for resources bundled inside the web server.
|
||||||
|
@since NEXT_RELEASE
|
||||||
|
*)
|
||||||
|
module type VFS = sig
|
||||||
|
val descr : string
|
||||||
|
(** Description of the VFS *)
|
||||||
|
|
||||||
|
val is_directory : string -> bool
|
||||||
|
|
||||||
|
val contains : string -> bool
|
||||||
|
(** [file_exists vfs path] returns [true] if [path] points to a file
|
||||||
|
or directory inside [vfs]. *)
|
||||||
|
|
||||||
|
val list_dir : string -> string array
|
||||||
|
(** List directory. This only returns basenames, the files need
|
||||||
|
to be put in the directory path using {!Filename.concat}. *)
|
||||||
|
|
||||||
|
val delete : string -> unit
|
||||||
|
(** Delete path *)
|
||||||
|
|
||||||
|
val create : string -> (bytes -> int -> int -> unit) * (unit -> unit)
|
||||||
|
(** Create a file and obtain a pair [write, close] *)
|
||||||
|
|
||||||
|
val read_file_content : string -> Tiny_httpd.Byte_stream.t
|
||||||
|
(** Read content of a file *)
|
||||||
|
|
||||||
|
val file_size : string -> int option
|
||||||
|
(** File size, e.g. using "stat" *)
|
||||||
|
|
||||||
|
val file_mtime : string -> float option
|
||||||
|
(** File modification time, e.g. using "stat" *)
|
||||||
|
end
|
||||||
|
|
||||||
|
val vfs_of_dir : string -> (module VFS)
|
||||||
|
(** [vfs_of_dir dir] makes a virtual file system that reads from the
|
||||||
|
disk.
|
||||||
|
@since NEXT_RELEASE
|
||||||
|
*)
|
||||||
|
|
||||||
|
val add_vfs :
|
||||||
|
config:config ->
|
||||||
|
vfs:(module VFS) ->
|
||||||
|
prefix:string ->
|
||||||
|
Tiny_httpd.t -> unit
|
||||||
|
(** Similar to {!add_dir_path} but using a virtual file system instead.
|
||||||
|
@since NEXT_RELEASE
|
||||||
|
*)
|
||||||
|
|
||||||
|
(** An embedded file system, as a list of files with (relative) paths.
|
||||||
|
This is useful in combination with the "tiny-httpd-mkfs" tool,
|
||||||
|
which embeds the files it's given into a OCaml module.
|
||||||
|
|
||||||
|
@since NEXT_RELEASE
|
||||||
|
*)
|
||||||
|
module Embedded_fs : sig
|
||||||
|
type t
|
||||||
|
(** The pseudo-filesystem *)
|
||||||
|
|
||||||
|
val create : ?mtime:float -> unit -> t
|
||||||
|
|
||||||
|
val add_file : ?mtime:float -> t -> path:string -> string -> unit
|
||||||
|
(** Add file to the virtual file system.
|
||||||
|
@raise Invalid_argument if the path contains '..' or if it tries to
|
||||||
|
make a directory out of an existing path that is a file. *)
|
||||||
|
|
||||||
|
val to_vfs : t -> (module VFS)
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,14 @@
|
||||||
(name http_of_dir)
|
(name http_of_dir)
|
||||||
(public_name http_of_dir)
|
(public_name http_of_dir)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
|
(modules http_of_dir)
|
||||||
(flags :standard -warn-error -3)
|
(flags :standard -warn-error -3)
|
||||||
(libraries tiny_httpd))
|
(libraries tiny_httpd))
|
||||||
|
|
||||||
|
(executable
|
||||||
|
(name vfs_pack)
|
||||||
|
(public_name tiny-httpd-vfs-pack)
|
||||||
|
(package tiny_httpd)
|
||||||
|
(modules vfs_pack)
|
||||||
|
(libraries curly unix)
|
||||||
|
(flags :standard -warn-error -3))
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ let parse_size s : int =
|
||||||
with _ -> raise (Arg.Bad "invalid size (expected <int>[kM]?)")
|
with _ -> raise (Arg.Bad "invalid size (expected <int>[kM]?)")
|
||||||
|
|
||||||
let main () =
|
let main () =
|
||||||
let config = D.default_config() in
|
let config =
|
||||||
config.dir_behavior <- Index_or_lists; (* keep old behavior *)
|
D.config ~dir_behavior:Index_or_lists ()
|
||||||
|
in
|
||||||
let dir_ = ref "." in
|
let dir_ = ref "." in
|
||||||
let addr = ref "127.0.0.1" in
|
let addr = ref "127.0.0.1" in
|
||||||
let port = ref 8080 in
|
let port = ref 8080 in
|
||||||
|
|
|
||||||
162
src/bin/vfs_pack.ml
Normal file
162
src/bin/vfs_pack.ml
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
|
||||||
|
let spf = Printf.sprintf
|
||||||
|
let fpf = Printf.fprintf
|
||||||
|
let now_ = Unix.gettimeofday()
|
||||||
|
let verbose = ref false
|
||||||
|
|
||||||
|
type entry =
|
||||||
|
| File of string * string
|
||||||
|
| Url of string * string
|
||||||
|
| Mirror of string * string
|
||||||
|
| Source_file of string
|
||||||
|
|
||||||
|
let read_file filename =
|
||||||
|
let ic = open_in_bin filename in
|
||||||
|
let buf = Buffer.create 32 in
|
||||||
|
let b = Bytes.create 1024 in
|
||||||
|
while
|
||||||
|
let n=input ic b 0 (Bytes.length b) in
|
||||||
|
Buffer.add_subbytes buf b 0 n;
|
||||||
|
n > 0
|
||||||
|
do () done;
|
||||||
|
close_in ic;
|
||||||
|
Buffer.contents buf
|
||||||
|
|
||||||
|
let split_comma s = Scanf.sscanf s "%s@,%s" (fun x y -> x,y)
|
||||||
|
|
||||||
|
let is_url s =
|
||||||
|
let is_prefix pre s =
|
||||||
|
String.length s > String.length pre &&
|
||||||
|
String.sub s 0 (String.length pre) = pre
|
||||||
|
in
|
||||||
|
is_prefix "http://" s || is_prefix "https://" s
|
||||||
|
|
||||||
|
let emit oc (l:entry list) : unit =
|
||||||
|
fpf oc "let embedded_fs = Tiny_httpd_dir.Embedded_fs.create ~mtime:%f ()\n" now_;
|
||||||
|
|
||||||
|
let add_vfs ~mtime vfs_path content =
|
||||||
|
fpf oc
|
||||||
|
"let () = Tiny_httpd_dir.Embedded_fs.add_file embedded_fs \n \
|
||||||
|
~mtime:%h ~path:%S\n \
|
||||||
|
%S\n"
|
||||||
|
mtime vfs_path content
|
||||||
|
in
|
||||||
|
|
||||||
|
let rec add_entry = function
|
||||||
|
| File (vfs_path, actual_path) ->
|
||||||
|
if !verbose then Printf.eprintf "add file %S = %S\n%!" vfs_path actual_path;
|
||||||
|
|
||||||
|
let content = read_file actual_path in
|
||||||
|
let mtime = (Unix.stat actual_path).Unix.st_mtime in
|
||||||
|
add_vfs ~mtime vfs_path content
|
||||||
|
|
||||||
|
| Url (vfs_path, url) ->
|
||||||
|
if !verbose then Printf.eprintf "add url %S = %S\n%!" vfs_path url;
|
||||||
|
|
||||||
|
begin match Curly.get url with
|
||||||
|
| Ok b ->
|
||||||
|
let code = b.Curly.Response.code in
|
||||||
|
if code >= 200 && code < 300 then (
|
||||||
|
add_vfs ~mtime:now_ vfs_path b.Curly.Response.body
|
||||||
|
) else (
|
||||||
|
failwith (Printf.sprintf "download of %S failed with code: %d" url code)
|
||||||
|
)
|
||||||
|
| Error err ->
|
||||||
|
failwith (Format.asprintf "download of %S failed: %a" url Curly.Error.pp err)
|
||||||
|
end
|
||||||
|
|
||||||
|
| Mirror (vfs_path, dir) ->
|
||||||
|
if !verbose then Printf.eprintf "mirror directory %S as %S\n%!" dir vfs_path;
|
||||||
|
|
||||||
|
let rec traverse rpath =
|
||||||
|
let real_path = Filename.concat dir rpath in
|
||||||
|
if Sys.is_directory real_path then (
|
||||||
|
let arr = Sys.readdir real_path in
|
||||||
|
Array.iter (fun e -> traverse (Filename.concat rpath e)) arr
|
||||||
|
) else (
|
||||||
|
add_entry (File (Filename.concat vfs_path rpath, real_path))
|
||||||
|
)
|
||||||
|
in
|
||||||
|
traverse "."
|
||||||
|
|
||||||
|
| Source_file f ->
|
||||||
|
if !verbose then Printf.eprintf "read source file %S\n%!" f;
|
||||||
|
|
||||||
|
let lines =
|
||||||
|
read_file f |> String.split_on_char '\n'
|
||||||
|
|> List.map String.trim
|
||||||
|
|> List.filter ((<>) "")
|
||||||
|
in
|
||||||
|
|
||||||
|
let process_line line =
|
||||||
|
let vfs_path, path = split_comma line in
|
||||||
|
if is_url path then add_entry (Url(vfs_path, path))
|
||||||
|
else add_entry (File (vfs_path, path))
|
||||||
|
in
|
||||||
|
|
||||||
|
List.iter process_line lines
|
||||||
|
in
|
||||||
|
List.iter add_entry l;
|
||||||
|
|
||||||
|
fpf oc "let vfs = Tiny_httpd_dir.Embedded_fs.to_vfs embedded_fs\n";
|
||||||
|
()
|
||||||
|
|
||||||
|
|
||||||
|
let help = {|vfs-pack [opt]+
|
||||||
|
|
||||||
|
Builds an OCaml module containing a `Tiny_httpd_dir.Embedded_fs.t`
|
||||||
|
virtual file system. This is useful to pack assets into an OCaml binary,
|
||||||
|
for example.
|
||||||
|
|
||||||
|
Each entry in the VFS can be added from the command line using:
|
||||||
|
|
||||||
|
--file=foo/bar,actual/path/to/file to add an entry foo/bar in the VFS
|
||||||
|
with the content of actual/path/to/file. The mtime of the file is preserved.
|
||||||
|
|
||||||
|
--url=foo/bar,https://something.com/ to add an entry foo/bar in the VFS
|
||||||
|
with the content of the URL (downloaded using curl).
|
||||||
|
|
||||||
|
-F=file reads lines from file. Each line is a pair vfs_path,actual_path
|
||||||
|
and is processed as previously. If actual_path looks like an http(s) URL
|
||||||
|
it is treated as such.
|
||||||
|
|}
|
||||||
|
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let entries = ref [] in
|
||||||
|
let out = ref "" in
|
||||||
|
|
||||||
|
let add_entry e = entries := e :: !entries in
|
||||||
|
|
||||||
|
let add_file s =
|
||||||
|
let vfs_path, path = split_comma s in
|
||||||
|
add_entry (File (vfs_path, path))
|
||||||
|
and add_mirror s =
|
||||||
|
let vfs_path, path = split_comma s in
|
||||||
|
let vfs_path, path = if path="" then "", vfs_path else vfs_path, path in
|
||||||
|
add_entry (Mirror (vfs_path, path))
|
||||||
|
and add_source f = add_entry (Source_file f)
|
||||||
|
and add_url s =
|
||||||
|
let vfs_path, path = split_comma s in
|
||||||
|
if is_url path then add_entry (Url(vfs_path, path))
|
||||||
|
else invalid_arg (spf "--url: invalid URL %S" path)
|
||||||
|
in
|
||||||
|
|
||||||
|
let opts = [
|
||||||
|
"-v", Arg.Set verbose, " verbose mode";
|
||||||
|
"-o", Arg.Set_string out, " set output file";
|
||||||
|
"--file", Arg.String add_file, " <name,file> adds name=file to the VFS";
|
||||||
|
"--url", Arg.String add_url, " <name,url> adds name=url to the VFS";
|
||||||
|
"--mirror", Arg.String add_mirror, " <prefix,dir> copies directory dir into the VFS under prefix";
|
||||||
|
"-F", Arg.String add_source, " <file> reads entries from the file, on per line";
|
||||||
|
] |> Arg.align in
|
||||||
|
Arg.parse opts (fun _ -> raise (Arg.Help "no positional arg")) help;
|
||||||
|
|
||||||
|
let out, close =
|
||||||
|
if !out="" then stdout,ignore
|
||||||
|
else open_out !out, close_out
|
||||||
|
in
|
||||||
|
emit out !entries;
|
||||||
|
close out;
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ build: [
|
||||||
depends: [
|
depends: [
|
||||||
"dune" { >= "2.0" }
|
"dune" { >= "2.0" }
|
||||||
"base-threads"
|
"base-threads"
|
||||||
|
"result"
|
||||||
"ocaml" { >= "4.04.0" }
|
"ocaml" { >= "4.04.0" }
|
||||||
"odoc" {with-doc}
|
"odoc" {with-doc}
|
||||||
"qtest" { >= "2.9" & with-test}
|
"qtest" { >= "2.9" & with-test}
|
||||||
|
|
|
||||||
1
vendor/curly
vendored
Submodule
1
vendor/curly
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 9417bd97fdf293f469c38e726c169583638d5aa1
|
||||||
2
vfs_path.sh
Executable file
2
vfs_path.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
exec dune exec --display=quiet src/bin/vfs_pack.exe --profile=release -- $@
|
||||||
Loading…
Add table
Reference in a new issue