storage.ml 16 KB

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