file.ml 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. (*
  2. * _ _ ____ _
  3. * _| || |_/ ___| ___ _ __ _ __ ___ | |
  4. * |_ .. _\___ \ / _ \ '_ \| '_ \ / _ \| |
  5. * |_ _|___) | __/ |_) | |_) | (_) |_|
  6. * |_||_| |____/ \___| .__/| .__/ \___/(_)
  7. * |_| |_|
  8. *
  9. * Personal Social Web.
  10. *
  11. * Copyright (C) The #Seppo contributors. All rights reserved.
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *)
  26. open Astring
  27. (* rather leverage fileutils? *)
  28. let rec find_path_tail predicate ?(prfx = "") ?(sep = "/") lst =
  29. match lst with
  30. | [] -> Error "not found"
  31. | hd :: tl ->
  32. let prfx = sep ^ hd ^ prfx in
  33. match predicate prfx with
  34. | Error _ as e -> e
  35. | Ok true -> Ok prfx
  36. | Ok false -> find_path_tail predicate ~prfx ~sep tl
  37. let mtime_0 ?(default = 0.) fn =
  38. (* Logr.debug (fun m -> m "mtime_0 %s" fn); *)
  39. try (Unix.stat fn).st_mtime
  40. with
  41. | _ -> default
  42. let pDir = 0o755
  43. (** typical permissions (directories) *)
  44. let pFile = 0o644
  45. (** typical permissions (files) *)
  46. let pFileRO = 0o444
  47. let rec mkdir_p perm n =
  48. (* TODO should we block anything starting with / or . ? *)
  49. match Sys.file_exists n with
  50. | true -> Ok n
  51. | false -> (
  52. match n |> Filename.dirname |> mkdir_p perm with
  53. | Ok _ -> (
  54. Unix.(try
  55. mkdir n perm;
  56. Ok n
  57. with Unix_error (n, a, b) ->
  58. Error ((n |> error_message) ^ ": " ^ a ^ " " ^ b)))
  59. | e -> e)
  60. let _chdir f d =
  61. Logr.debug (fun m -> m "%s.%s %s" "File" "chdir" d);
  62. let cwd = Unix.getcwd () in
  63. let _ = mkdir_p pDir d in
  64. Unix.chdir d;
  65. let r = f () in
  66. Unix.chdir cwd;
  67. r
  68. (** similar to List.fold_left but for dirctory contents. Low level.
  69. * f init fn: fn is the filename local to the directory
  70. * init: aggregate
  71. * dn: directory name
  72. *)
  73. let fold_dir f init dn =
  74. try let dh = dn |> Unix.opendir in
  75. let rec next init =
  76. try
  77. match dh
  78. |> Unix.readdir
  79. |> f init with
  80. | init,false -> init
  81. | init,true -> init |> next
  82. with End_of_file -> init
  83. in
  84. let ret = next init in
  85. dh |> Unix.closedir;
  86. ret
  87. with Unix.(Unix_error(ENOENT, "opendir", _)) -> init
  88. let count_dir ?(max = Int.max_int) ?(pred = (fun f -> not (f = "." || f = ".."))) dn =
  89. fold_dir (fun count fn ->
  90. let count = count + if pred fn
  91. then 1
  92. else 0 in
  93. (count,count < max))
  94. 0 dn
  95. let any pred d : string option =
  96. (* use File.fold_dir? *)
  97. let wa = Unix.opendir d in
  98. let rec loop () =
  99. try
  100. let fn = wa |> Unix.readdir in
  101. if pred fn
  102. then Some fn
  103. else loop ()
  104. with End_of_file -> None
  105. in
  106. let r = loop () in
  107. Unix.closedir wa;
  108. r
  109. let exists = Sys.file_exists
  110. (* evtl. https://rosettacode.org/wiki/Read_entire_file#OCaml *)
  111. let to_bytes (fn : string) : bytes =
  112. try
  113. let len = (Unix.stat fn).st_size in
  114. let ic = open_in_gen [ Open_binary; Open_rdonly ] 0 fn in
  115. let buf = Bytes.create len in
  116. really_input ic buf 0 len;
  117. close_in ic;
  118. buf
  119. with _ -> Bytes.empty
  120. let to_string fn = fn
  121. |> to_bytes
  122. |> Bytes.to_string
  123. let cat fn = try
  124. fn |> to_string |> Result.ok
  125. with
  126. | Sys_error e -> Error e
  127. | Invalid_argument e -> Error e
  128. (* | End_of_file -> Error ("error reading file " ^ fn) *)
  129. (** open, read, close a file.
  130. *
  131. * rdr the receiving function
  132. * fn filename
  133. *)
  134. let in_channel rdr fn =
  135. let ic = open_in_gen [ Open_rdonly; Open_binary ] 0 fn in
  136. let ret = rdr ic in
  137. close_in ic;
  138. ret
  139. (** generic write - don't use directly.*)
  140. let out_channel' ~tmp ~mode ~perm wrtr fn =
  141. Logr.debug (fun m -> m "%s.%s %s cwd: %s" "File" "out_channel" fn (Unix.getcwd ()));
  142. let fn' = match tmp with
  143. | None -> fn
  144. | Some "~" -> fn ^ "~"
  145. | Some s -> s in
  146. let oc = open_out_gen mode perm fn' in
  147. let ret = wrtr oc in
  148. oc |> close_out;
  149. if tmp |> Option.is_some
  150. then Unix.rename fn' fn;
  151. ret
  152. (** atomic write.
  153. @TODO aquire on exclusive lock? *)
  154. let out_channel_append ?(mode = [ Open_append; Open_binary; Open_creat; Open_wronly; ]) ?(perm = pFile) wrtr fn =
  155. assert (mode |> List.exists (function Open_append -> true | _ -> false));
  156. assert (mode |> List.exists (function Open_trunc -> true | _ -> false) |> not);
  157. assert (mode |> List.exists (function Open_wronly -> true | _ -> false));
  158. out_channel' ~tmp:None ~mode ~perm wrtr fn
  159. (** atomic write.
  160. I'm not convinced that https://notes.eatonphil.com/2024-09-29-build-a-serverless-acid-database-with-this-one-neat-trick.html#a-filesystem-blob-store
  161. would be enough: O_CREAT | O_EXCL. *)
  162. let out_channel_replace ?(tmp = "~") ?(mode = [ Open_binary; Open_creat; Open_trunc; Open_wronly; ]) ?(perm = pFile) wrtr fn =
  163. assert (mode |> List.exists (function Open_append -> true | _ -> false) |> not);
  164. assert (mode |> List.exists (function Open_trunc -> true | _ -> false));
  165. assert (mode |> List.exists (function Open_wronly -> true | _ -> false));
  166. out_channel' ~tmp:(Some tmp) ~mode ~perm wrtr fn
  167. let out_channel_create ?(tmp = "~") ?(mode = [ Open_binary; Open_creat; Open_excl; Open_wronly; ]) ?(perm = pFile) wrtr fn =
  168. assert (mode |> List.exists (function Open_append -> true | _ -> false) |> not);
  169. assert (mode |> List.exists (function Open_excl -> true | _ -> false));
  170. assert (mode |> List.exists (function Open_trunc -> true | _ -> false) |> not);
  171. assert (mode |> List.exists (function Open_wronly -> true | _ -> false));
  172. out_channel' ~tmp:(Some tmp) ~mode ~perm wrtr fn
  173. (** non-atomic write inside a file *)
  174. let out_channel_patch ?(mode = [ Open_binary; Open_wronly; ]) ?(perm = pFile) wrtr fn =
  175. assert (mode |> List.exists (function Open_append -> true | _ -> false) |> not);
  176. assert (mode |> List.exists (function Open_trunc -> true | _ -> false) |> not);
  177. assert (mode |> List.exists (function Open_wronly -> true | _ -> false));
  178. out_channel' ~tmp:None ~mode ~perm wrtr fn
  179. let touch fn =
  180. fn
  181. |> open_out_gen [ Open_append; Open_binary; Open_creat; Open_wronly; ] pFileRO
  182. |> close_out
  183. let copy_channel ?(buf = 16 * 0x400 |> Bytes.create) oc ic =
  184. (* primitive take copy inspired by
  185. https://sylvain.le-gall.net/ocaml-fileutils.html *)
  186. let len = buf |> Bytes.length in
  187. let r = ref 0 in
  188. while r := input ic buf 0 len;
  189. !r <> 0
  190. do
  191. output oc buf 0 !r
  192. done
  193. let restore_static ?(perm = pFile) fn =
  194. if fn |> exists
  195. then None
  196. else
  197. let _ = fn |> Filename.dirname |> mkdir_p pDir in
  198. fn |> out_channel_replace ~perm (fun oc ->
  199. match Res.read ("static/" ^ fn) with
  200. | None ->
  201. Logr.err (fun m -> m "%s missing %s" E.e1028 fn);
  202. None
  203. | Some str as r ->
  204. str |> output_string oc;
  205. Logr.info (fun m -> m "unpacked %s" fn);
  206. r )
  207. let fold_lines f init ic =
  208. let rec next_line init' =
  209. try
  210. ic
  211. |> input_line
  212. |> f init'
  213. |> next_line
  214. with
  215. | End_of_file -> init'
  216. in
  217. next_line init
  218. let fold_bind_lines f init ic =
  219. let ( let* ) = Result.bind in
  220. let rec next_line init' =
  221. try
  222. let* init' = ic |> input_line |> f init' in
  223. next_line init'
  224. with
  225. | End_of_file -> Ok init'
  226. in
  227. next_line init
  228. module Path = struct
  229. let sep = String.of_char '/'
  230. let hd (ch : char) (str : string) : string option =
  231. assert (ch = '/');
  232. Option.bind
  233. (String.cut ~sep str)
  234. (fun (s,_) -> Some s)
  235. let tl (ch : char) (str : string) : string option =
  236. assert (ch = '/');
  237. Option.bind
  238. (String.cut ~sep str)
  239. (fun (_,s) -> Some s)
  240. end