diff --git a/src/Tiny_httpd_dir.ml b/src/Tiny_httpd_dir.ml index a86f0404..c250e9cb 100644 --- a/src/Tiny_httpd_dir.ml +++ b/src/Tiny_httpd_dir.ml @@ -59,15 +59,54 @@ 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 html_list_dir ~top ~parent d : string = - let entries = Sys.readdir @@ (top // d) in +module type VFS = sig + 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) ~parent d : string = + let entries = VFS.list_dir d in Array.sort compare entries; let body = Buffer.create 256 in (* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *) Printf.bprintf body {| http_of_dir %S

Index of %S

- |} top d; + |} VFS.descr d; begin match parent with | None -> () | Some p -> @@ -88,16 +127,17 @@ let html_list_dir ~top ~parent d : string = Printf.bprintf body "\n"; ); if not @@ contains_dot_dot (d // f) then ( - let fpath = top // d // f in - if not @@ Sys.file_exists fpath then ( + let fpath = d // f in + if not @@ VFS.contains fpath then ( Printf.bprintf body "
  • %s [invalid file]
  • \n" f ) else ( let size = - try Printf.sprintf " (%s)" @@ human_size (Unix.stat fpath).Unix.st_size - with _ -> "" + match VFS.file_size fpath with + | Some f -> Printf.sprintf " (%s)" @@ human_size f + | None -> "" in Printf.bprintf body "
  • %s %s%s
  • \n" - (encode_path (d // f)) f (if Sys.is_directory fpath then "[dir]" else "") size + (encode_path (d // f)) f (if VFS.is_directory fpath then "[dir]" else "") size ); ) ) @@ -114,7 +154,8 @@ let finally_ ~h x f = h x; 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= if config.delete then ( S.add_route_handler server ~meth:`DELETE @@ -125,7 +166,7 @@ let add_dir_path ~config ~dir ~prefix server = ) else ( S.Response.make_string (try - Sys.remove (dir // path); Ok "file deleted successfully" + VFS.delete path; Ok "file deleted successfully" with e -> Error (500, Printexc.to_string e)) ) ); @@ -147,17 +188,15 @@ let add_dir_path ~config ~dir ~prefix server = | _ -> Ok () ) (fun path req -> - let fpath = dir // path in - let oc = - try open_out fpath + let write, close = + try VFS.create path with e -> S.Response.fail_raise ~code:403 "cannot upload to %S: %s" path (Printexc.to_string e) 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; - flush oc; - close_out oc; + S.Byte_stream.iter write req.S.Request.body; + close (); S._debug (fun k->k "done uploading"); S.Response.make_raw ~code:201 "upload successful" ) @@ -171,32 +210,31 @@ let add_dir_path ~config ~dir ~prefix server = S.add_route_handler server ~meth:`GET S.Route.(exact_path prefix (rest_of_path_urlencoded)) (fun path req -> - let full_path = dir // path in let mtime = lazy ( - try Printf.sprintf "mtime: %f" (Unix.stat full_path).Unix.st_mtime - with _ -> S.Response.fail_raise ~code:403 "Cannot access file" + match VFS.file_mtime path with + | None -> S.Response.fail_raise ~code:403 "Cannot access file" + | Some t -> Printf.sprintf "mtime: %.4f" t ) in - if contains_dot_dot full_path then ( + if contains_dot_dot path then ( 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"; ) 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.Response.make_raw ~code:304 "" - ) else if Sys.is_directory full_path then ( - S._debug (fun k->k "list dir %S (topdir %S)" full_path dir); + ) else if VFS.is_directory path then ( + S._debug (fun k->k "list dir %S (topdir %S)" path VFS.descr); let parent = Filename.(dirname path) in let parent = if parent <> path then Some parent else None in match config.dir_behavior with - | Index | Index_or_lists when - Sys.file_exists (full_path // "index.html") -> + | Index | Index_or_lists when VFS.contains (path // "index.html") -> (* redirect using path, not full path *) let new_path = "/" // path // "index.html" in S._debug (fun k->k "redirect to `%s`" new_path); S.Response.make_raw ~code:301 "" ~headers:S.Headers.(empty |> set "location" new_path) | Lists | Index_or_lists -> - let body = html_list_dir ~top:dir path ~parent in + let body = html_list_dir vfs path ~parent in S.Response.make_string ~headers:[header_html; "ETag", Lazy.force mtime] (Ok body) @@ -204,23 +242,26 @@ let add_dir_path ~config ~dir ~prefix server = S.Response.make_raw ~code:405 "listing dir not allowed" ) else ( try - let ic = open_in full_path in let mime_type = - if Filename.extension full_path = ".css" then ( + if Filename.extension path = ".css" then ( ["Content-Type", "text/css"] - ) else if Filename.extension full_path = ".js" then ( + ) else if Filename.extension path = ".js" then ( ["Content-Type", "text/javascript"] - ) else try - let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" full_path) in + ) else if on_fs then ( + (* 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 (fun p -> try ["Content-Type", String.trim (input_line p)] with _ -> []) with _ -> [] + ) else [] in + let stream = VFS.read_file_content path in S.Response.make_raw_stream ~headers:(mime_type@["Etag", Lazy.force mtime]) - ~code:200 (S.Byte_stream.of_chan ic) + ~code:200 stream with e -> S.Response.fail ~code:500 "error while reading file: %s" (Printexc.to_string e)) ) @@ -229,3 +270,10 @@ let add_dir_path ~config ~dir ~prefix server = S.Route.(exact_path prefix (string @/ return)) (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 diff --git a/src/Tiny_httpd_dir.mli b/src/Tiny_httpd_dir.mli index 4e5cdf77..183d363c 100644 --- a/src/Tiny_httpd_dir.mli +++ b/src/Tiny_httpd_dir.mli @@ -78,3 +78,55 @@ val add_dir_path : dir:string -> prefix:string -> 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 +*) +