storage.ml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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. let ( let* ) = Result.bind
  27. let ( >>= ) = Result.bind
  28. let pre = "app/var/db/"
  29. let fn = pre ^ "o/p.s"
  30. let fn_id_cdb = Mapcdb.Cdb (pre ^ "o/id.cdb")
  31. let fn_url_cdb = Mapcdb.Cdb (pre ^ "o/url.cdb")
  32. let fn_t_cdb = Mapcdb.Cdb (pre ^ "o/t.cdb")
  33. (** An id consists of a page name and number and an index within *)
  34. module Id = struct
  35. let uri_to_b id =
  36. id |> Uri.to_string |> Bytes.of_string
  37. (** defined by a name and a number. *)
  38. type page = string * int
  39. (** An ID consists of a page and an index within *)
  40. type t = page * int
  41. let to_page_i id : (t,string) result=
  42. if id |> Uri.scheme |> Option.is_none
  43. && id |> Uri.host |> Option.is_none
  44. && id |> Uri.path |> St.is_prefix ~affix:"o/"
  45. then
  46. let jig = "%-%/" |> Make.Jig.make in
  47. match id |> Uri.path |> Make.Jig.cut jig,
  48. id |> Uri.fragment with
  49. | Some [b;j] , Some i ->
  50. (try
  51. Ok ((b,j |> int_of_string)
  52. , i |> int_of_string)
  53. with Failure e -> Error e)
  54. | _ -> Error "no index given"
  55. else
  56. Error "must be like o/p-23/#42"
  57. end
  58. (* a tuple of two (file) positions *)
  59. module TwoPad10 = struct
  60. let length = 28
  61. type t = int * int
  62. let to_string (a,b : t) =
  63. (* write a canonical s-expression in one go *)
  64. let r = Printf.sprintf "(10:0x%08x10:0x%08x)" a b in
  65. assert (length == (r |> String.length));
  66. r
  67. let decode (sx : Csexp.t) : (t,'a) result =
  68. let h2i = int_of_string in
  69. match sx with
  70. | Csexp.(List [Atom p0; Atom p1]) -> Ok (h2i p0, h2i p1)
  71. | _ -> Error "couldn't decode"
  72. let decode_many l : t list =
  73. let h2i = int_of_string in
  74. l |> List.fold_left (fun init e ->
  75. match e with
  76. | Csexp.(List [Atom p0; Atom p1]) -> (h2i p0, h2i p1) :: init
  77. | _ -> init) []
  78. |> List.rev
  79. let fold_decode a (_ : (Csexp.t,'a) result) =
  80. a
  81. let from_channel ic =
  82. match Csexp.input_many ic with
  83. | Error _ -> []
  84. | Ok l -> decode_many l
  85. let from_file = File.in_channel from_channel
  86. let from_page_i ?(prefix = pre) (((fn,j),i) : Id.t) : (t,string) result =
  87. let jig = prefix ^ "%/%.s" |> Make.Jig.make in
  88. let l : t list = [fn;j |> string_of_int]
  89. |> Make.Jig.paste jig
  90. |> Option.get
  91. |> from_file in
  92. try Ok (i |> List.nth l)
  93. with _ -> Error "not found"
  94. let from_id ?(prefix = pre) id : (t,string) result =
  95. id
  96. |> Id.to_page_i
  97. >>= from_page_i ~prefix
  98. let strut (p0,p1 : t) =
  99. assert (p0 >= 0);
  100. assert (p1 - p0 - 6 >= 0);
  101. let l0,l1 = match p1 - p0 - 6 with
  102. | 0 as n -> 0,n - 0
  103. | 10 as n -> 1,n - 1
  104. | 101 as n -> 1,n - 2
  105. | 1_002 as n -> 1,n - 3
  106. | 10_003 as n -> 1,n - 4
  107. | 100_004 as n -> 1,n - 5
  108. | 1_000_005 as n -> 1,n - 6
  109. | 10_000_006 as n -> 1,n - 7
  110. | 100_000_007 as n -> 1,n - 8
  111. | 1_000_000_008 as n -> 1,n - 9
  112. | n ->
  113. let n' = n |> float_of_int in
  114. let dec' = n' |> log10 |> floor in
  115. let dec = n' -. dec' |> log10 |> int_of_float in
  116. 0,n - dec
  117. in
  118. let fil = 'x' in
  119. let r = Csexp.(List [Atom (String.make l0 fil); Atom (String.make l1 fil)]) in
  120. Logr.debug (fun m -> m "%s.%s %d" "Storage" "strut" (p1-p0));
  121. assert ((p1-p0) == (r |> Csexp.to_string |> String.length));
  122. r
  123. end
  124. (* hydrate entry (from main storage) *)
  125. let fold_of_twopad10 ?(fn = fn) a p =
  126. (* read entry from main storage *)
  127. let of_twopad10 (p0,p1 : TwoPad10.t) : (Csexp.t,'a) result =
  128. let ipt ic =
  129. seek_in ic p0;
  130. assert (pos_in ic = p0);
  131. let r = Csexp.input ic in
  132. assert (pos_in ic = p1);
  133. r
  134. in
  135. fn |> File.in_channel ipt
  136. in
  137. let ( >>= ) = Result.bind in
  138. (p
  139. |> TwoPad10.decode
  140. >>= of_twopad10
  141. >>= Rfc4287.Entry.decode)
  142. :: a
  143. module Page = struct
  144. type t = Id.page
  145. let jig = pre ^ "%/%.s" |> Make.Jig.make
  146. let of_fn fn : t option =
  147. match fn |> Make.Jig.cut jig with
  148. | Some [a;b] ->
  149. assert (a |> St.is_prefix ~affix:"o/");
  150. Some (a,b |> int_of_string)
  151. | _ -> None
  152. let to_fn (a,b : t) =
  153. assert (a |> St.is_prefix ~affix:"o/");
  154. [a;b |> string_of_int]
  155. |> Make.Jig.paste jig
  156. |> Option.get
  157. let to_posn (p : t) : TwoPad10.t list =
  158. p
  159. |> to_fn
  160. |> TwoPad10.from_file
  161. let find_max ?(prefix = pre) (dir,_ : t) : t option =
  162. assert (dir |> St.is_prefix ~affix:"o/");
  163. assert (not (dir |> St.is_suffix ~affix:"/"));
  164. let mx = File.fold_dir (fun c fn ->
  165. (try Scanf.sscanf fn "%d.s" (fun i -> i)
  166. with _ -> -1)
  167. |> max c,true)
  168. (-1) (prefix ^ dir) in
  169. if mx < 0
  170. then None
  171. else Some (dir,mx)
  172. let jig2 = "%-%/" |> Make.Jig.make
  173. let of_id = Id.to_page_i
  174. let modify_idx fu (a,x : t) : t =
  175. (a,x |> fu)
  176. let pred = modify_idx Int.pred
  177. let succ = modify_idx Int.succ
  178. let to_int = function
  179. | Some (_,x : t) -> x
  180. | _ -> -1
  181. (* the next id and page *)
  182. let next_id ~items_per_page (dir,_ as pa : t) : (Uri.t * t) =
  183. (* Logr.debug (fun m -> m "%s.%s %s" "Storage" "next_id" dir); *)
  184. assert (dir |> St.is_prefix ~affix:"o/");
  185. assert (not (dir |> St.is_suffix ~affix:"/"));
  186. let bytes_per_item = TwoPad10.length in
  187. (* get the previously highest index number and name *)
  188. let _ = pa |> to_fn |> Filename.dirname |> File.mkdir_p File.pDir in
  189. let pg,i =
  190. match pa |> find_max with
  191. | None ->
  192. (* Logr.debug (fun m -> m "%s.%s first %s" "Storage" "next_id" dir); *)
  193. 0,0
  194. | Some (di,pg) ->
  195. assert (di |> String.equal dir);
  196. let pa = (dir,pg) in
  197. let i = (try (pa |> to_fn |> Unix.stat).st_size
  198. with _ -> 0) / bytes_per_item in
  199. if i < items_per_page
  200. then pg,i
  201. else pg+1,0
  202. in
  203. assert (pg >= 0);
  204. assert (i >= 0);
  205. assert (i < items_per_page);
  206. let j = "%-%/#%" |> Make.Jig.make in
  207. let v = [dir;pg |> string_of_int;i |> string_of_int] in
  208. let id = v |> Make.Jig.paste j |> Option.get |> Uri.of_string in
  209. Logr.debug (fun m -> m "%s.%s %a" "Storage" "next_id" Uri.pp id);
  210. assert (id |> Uri.to_string |> St.is_prefix ~affix:"o/");
  211. id,(dir,pg)
  212. let apnd (_,b as pa) pos =
  213. assert (b >= 0);
  214. assert (TwoPad10.length == (pos |> Bytes.length));
  215. pa
  216. |> to_fn
  217. |> File.out_channel_append (fun oc -> output_bytes oc pos)
  218. let append (pa : t) (pos : TwoPad10.t) =
  219. let by = pos
  220. |> TwoPad10.to_string
  221. |> Bytes.of_string in
  222. by |> apnd pa;
  223. by
  224. let _remake fn ix =
  225. (* add csexp entry to .s and return (id,position) tuple *)
  226. let add_1_csx oc sx =
  227. let ol = pos_out oc in
  228. sx |> Csexp.to_channel oc;
  229. let ne = pos_out oc in
  230. let id = match sx |> Rfc4287.Entry.decode with
  231. | Error _ -> None
  232. | Ok r -> Some r.id in
  233. (id,(ol,ne)) in
  234. (* if Some id call fkt with id->(ol,ne) *)
  235. let add_1_p fkt = function
  236. | (None,_v) -> Logr.warn (fun m -> m "add a strut?")
  237. | (Some id,v) -> fkt (Id.uri_to_b id, v |> TwoPad10.to_string |> Bytes.of_string) in
  238. (* - read all csexps from the source *)
  239. let ic = open_in_gen [ Open_binary; Open_rdonly ] 0 fn in
  240. let* sxs = Csexp.input_many ic in
  241. close_in ic;
  242. (* copy fn content as csexps to tmp file fn' *)
  243. let fn' = fn ^ "~" in
  244. let oc = open_out_gen [ Open_binary; Open_wronly ] File.pFile fn' in
  245. let cp_csx oc sxs sx = (add_1_csx oc sx) :: sxs in
  246. let pos = List.fold_left (cp_csx oc) [] sxs in
  247. close_out oc;
  248. (* recreate cdb *)
  249. let none _ = false in
  250. let add_all fkt = List.iter (add_1_p fkt) pos in
  251. let _ = Mapcdb.add_many none add_all ix in
  252. (* swap tmp for real *)
  253. Unix.rename fn' fn;
  254. Ok fn
  255. open Rfc4287
  256. (* all but o/p/, unnumbered (dummy -3) *)
  257. let other_feeds (e : Entry.t) : t list =
  258. let day (Rfc3339.T iso) = ("o/d/" ^ String.sub iso 0 10,-3) in
  259. let open Category in
  260. let tag init (_,(Term (Single t)),_) = ("o/t/" ^ t,-3) :: init in
  261. day e.published
  262. :: (e.categories |> List.fold_left tag [])
  263. (* all but o/p/, numbered *)
  264. let next_other_pages ~items_per_page (e : Entry.t) : t list =
  265. let page init item =
  266. let _,pg = next_id ~items_per_page item in
  267. pg :: init
  268. in
  269. e
  270. |> other_feeds
  271. |> List.fold_left page []
  272. let find (pos : TwoPad10.t) (base : string) : t option =
  273. let compare (inner0,inner1) (outer0,outer1) =
  274. (* Logr.debug (fun m -> m "%s.%s (%i,%i) (%i,%i)" "Main.Note.Delete" "dirty.compare" in0 in1 out0 out1); *)
  275. assert (inner0 <= inner1);
  276. assert (outer0 <= outer1);
  277. if inner1 < outer0
  278. then (-1)
  279. else if inner0 > outer1
  280. then 1
  281. else 0
  282. in
  283. let union posn =
  284. match posn with
  285. | [] -> (0,0)
  286. | (a0,a1) :: _ ->
  287. let b0,b1 = posn |> St.last in
  288. (* Logr.debug (fun m -> m "%s.%s (%i,%i) (%i,%i)" "Main.Note.Delete" "dirty.range" p00 p01 p10 p11); *)
  289. assert (a0 <= a1);
  290. assert (b0 <= b1);
  291. assert (a0 <= b1);
  292. (a0,b1)
  293. in
  294. let includes (outer0,outer1) (inner0,inner1) =
  295. (* Logr.debug (fun m -> m "%s.%s (%i,%i) (%i,%i)" "Main.Note.Delete" "dirty.spans" in0 in1 out0 out1); *)
  296. (* assert (r = (0 == compare (in0,in1) (out0,out1))); *)
  297. inner0 >= outer0 && inner1 <= outer1
  298. in
  299. let rec bsearch (pos : TwoPad10.t) (p,i0 : t) (p1,i1 : t) =
  300. Logr.debug (fun m -> m "%s.%s (%s,%i) (%s,%i)" "Main.Note.Delete" "dirty.find.bsearch" p i0 p1 i1);
  301. assert (p |> String.equal p1);
  302. assert (i0 <= i1);
  303. let m = p , (i0 + i1) / 2 in
  304. match m
  305. |> to_posn
  306. |> union
  307. |> compare pos with
  308. | 0 -> Logr.debug (fun m -> m "%s.%s found: (%s,%i)" "Main.Note.Delete" "dirty.find.bsearch" p ((i0+i1)/2));
  309. Some m
  310. | -1 -> bsearch pos (p,i0) m
  311. | 1 -> bsearch pos m (p1,i1)
  312. | _ -> failwith __LOC__
  313. in
  314. Option.bind
  315. (find_max (base,-11))
  316. (fun mx ->
  317. let mx' = mx
  318. |> to_posn
  319. |> union in
  320. (* at first examine the most recent page *)
  321. if includes mx' pos
  322. then Some mx
  323. else let _,mx'1 = mx' in
  324. (* then binary search all *)
  325. let all = (0,mx'1) in
  326. if includes pos all
  327. then (let p,_ = mx in
  328. bsearch pos (p,0) mx)
  329. else None)
  330. end
  331. open Rfc4287
  332. (* all logical feed urls, xml+json, (including the main feed) outbox etc. *)
  333. let feed_urls (e : Entry.t) =
  334. let db = Uri.make ~path:"o/d/" () in
  335. let day (Rfc3339.T iso) =
  336. let p = String.sub iso 0 10 in
  337. Uri.make ~path:(p ^ "/") () |> Http.reso ~base:db in
  338. let tb = tagu in
  339. let open Category in
  340. let tag (_,(Term (Single p)),_) =
  341. Uri.make ~path:(p ^ "/") () |> Http.reso ~base:tb in
  342. let obox = Uri.make ~path:(Ap.apub ^ "outbox/") () in
  343. defa
  344. :: obox
  345. :: (e.published |> day)
  346. :: (e.categories |> List.map tag)
  347. let climb a : string =
  348. a
  349. |> String.split_on_char '/'
  350. |> List.map (fun _ -> "../")
  351. |> String.concat ""
  352. let make_feed_syml (unn,b : Page.t) fn' =
  353. Logr.debug (fun m -> m "%s.%s %s/%d %s" "Storage" "make_feed_syml" unn b fn');
  354. let ld = unn ^ "/" in
  355. let ln = ld ^ (Filename.basename fn') in
  356. let fn = (unn |> climb) ^ fn' in
  357. Logr.debug (fun m -> m "ln -s %s %s" fn ln);
  358. let open Unix in
  359. ((* should we take measures to only ever unlink symlinks? *)
  360. try unlink ln
  361. with Unix_error(ENOENT, "unlink", _) -> ());
  362. (try mkdir ld File.pDir
  363. with Unix_error(EEXIST, "mkdir", _) -> ());
  364. symlink ~to_dir:false fn ln;
  365. (fn, ln)
  366. (* return a list of Page.t the entry is part of *)
  367. let save
  368. ?(items_per_page = 50)
  369. ?(fn = fn)
  370. ?(fn_id_cdb = fn_id_cdb)
  371. ?(_fn_url_cdb = fn_url_cdb)
  372. ?(_fn_t_cdb = fn_t_cdb)
  373. (e : Rfc4287.Entry.t) =
  374. let rel_edit_for_id id : Rfc4287.Link.t =
  375. Logr.debug (fun m -> m "%s.%s id %a" "Storage" "save.rel_edit_for_id" Uri.pp id);
  376. let path = Cfg.seppo_cgi ^ "/edit" in
  377. let f = id |> Uri.fragment |> Option.value ~default:"" in
  378. assert (f != "");
  379. let query = [("id",[id |> Uri.to_string])] in
  380. {href = Uri.make ~path ~query ();
  381. rel = Some Link.edit;
  382. rfc7565 = None;
  383. title = None} in
  384. let id,(a,b as ix) = Page.next_id ~items_per_page ("o/p",-3) in
  385. Logr.debug (fun m -> m "%s.%s id: %a fn_x: %s%d" "Storage" "save" Uri.pp id a b);
  386. assert (Rfc4287.defa |> Uri.to_string |> String.equal (a ^"/"));
  387. assert (id |> Uri.to_string |> St.is_prefix ~affix:"o/p-");
  388. assert (a |> String.equal "o/p");
  389. assert (b >= 0);
  390. let e = {e with id;
  391. links = (id |> rel_edit_for_id) :: e.links} in
  392. (* append entry to global storage .s and record store position *)
  393. let p0 = try (Unix.stat fn).st_size with _ -> 0 in
  394. let mode = [ Open_append; Open_binary; Open_creat; Open_wronly ] in
  395. fn |> File.out_channel_append ~mode (fun oc ->
  396. e
  397. |> Rfc4287.Entry.encode
  398. |> Csexp.to_channel oc);
  399. let p1 = (Unix.stat fn).st_size in
  400. let pos = (p0,p1) |> Page.append ix in
  401. let _ = Mapcdb.add (Id.uri_to_b e.id) pos fn_id_cdb in
  402. Logr.warn (fun m -> m "@TODO append url->id to urls.cdb");
  403. e,ix,pos
  404. let from_channel (p0,_ : TwoPad10.t) sc =
  405. seek_in sc p0;
  406. sc |> Csexp.input >>= Entry.decode
  407. let overwrite fn (p0,p1 as pos : TwoPad10.t) =
  408. fn
  409. |> File.out_channel_patch
  410. (fun oc ->
  411. seek_out oc p0;
  412. assert (p0 == pos_out oc);
  413. pos |> TwoPad10.strut |> Csexp.to_channel oc;
  414. assert (p1 == pos_out oc) )
  415. (* overwrite in primary storage *)
  416. let delete
  417. ?(fn = fn)
  418. id : (Rfc4287.Entry.t, string) result =
  419. Logr.debug (fun m -> m "%s.%s %a" "Storage" "delete" Uri.pp_hum id);
  420. let* pos = id |> TwoPad10.from_id in
  421. let* r = fn |> File.in_channel (from_channel pos) in
  422. overwrite fn pos;
  423. Ok r
  424. let select ?(fn = fn) id : (Rfc4287.Entry.t, string) result =
  425. Logr.warn (fun m -> m "%s.%s %a" "Storage" "select" Uri.pp_hum id);
  426. let* pos = TwoPad10.from_id id in
  427. fn |> File.in_channel (from_channel pos)