1 | type t = string
|
---|
2 | type item_t = string
|
---|
3 | type archive_t = {
|
---|
4 | name: string; archivists: Person.Set.t; id: Id.t;
|
---|
5 | kv: string Store.KV.t; store: t }
|
---|
6 | type record_t = Text.t * item_t
|
---|
7 |
|
---|
8 | let extension = ".txt"
|
---|
9 |
|
---|
10 | let to_string f =
|
---|
11 | let ic = open_in f in
|
---|
12 | let n = in_channel_length ic in
|
---|
13 | let s = Bytes.create n in
|
---|
14 | really_input ic s 0 n;
|
---|
15 | close_in ic;
|
---|
16 | Bytes.to_string s
|
---|
17 |
|
---|
18 | let file path content = let out = open_out path in
|
---|
19 | output_string out content; close_out out
|
---|
20 |
|
---|
21 | let (//) a b = a ^ "/" ^ b
|
---|
22 |
|
---|
23 | let to_text path =
|
---|
24 | if Filename.extension path = extension then
|
---|
25 | (to_string path |> Text.of_string |> Result.map_error (fun m -> path ^": "^ m))
|
---|
26 | else Error "Not txt"
|
---|
27 |
|
---|
28 | let newest (a,_pa) (b,_pb) = Text.newest a b
|
---|
29 | let oldest (a,_pa) (b,_pb) = Text.oldest a b
|
---|
30 |
|
---|
31 | let list_iter fn {store;_} paths =
|
---|
32 | let link f = match to_text (Filename.concat store f)
|
---|
33 | with Ok t -> fn store t f | Error s -> prerr_endline s in
|
---|
34 | List.iter link paths
|
---|
35 |
|
---|
36 | let iter_valid_text pred fn p =
|
---|
37 | match to_text p with Error _ -> () | Ok t -> if pred t then fn (t, p)
|
---|
38 |
|
---|
39 | let fold_valid_text pred fn acc p =
|
---|
40 | match to_text p with Error _ -> acc | Ok t -> if pred t then fn acc (t, p) else acc
|
---|
41 |
|
---|
42 | let list_fs dir =
|
---|
43 | let rec loop result = function
|
---|
44 | | [] -> result
|
---|
45 | | f::fs when Sys.is_directory f ->
|
---|
46 | Array.map (Filename.concat f) (Sys.readdir f)
|
---|
47 | |> Array.to_list |> List.append fs |> loop result
|
---|
48 | | f::fs -> loop (f::result) fs
|
---|
49 | in loop [] [dir]
|
---|
50 |
|
---|
51 | let list_take n =
|
---|
52 | let rec take acc n = function [] -> []
|
---|
53 | | x::_ when n = 1 -> x::acc
|
---|
54 | | x::xs -> take (x::acc) (n-1) xs
|
---|
55 | in take [] n
|
---|
56 |
|
---|
57 | let iter ?(predicate=fun _ -> true) ?order ?number fn {store;_} =
|
---|
58 | match order with
|
---|
59 | | None -> List.iter (iter_valid_text predicate fn) @@ list_fs store
|
---|
60 | | Some comp ->
|
---|
61 | List.iter fn
|
---|
62 | @@ (match number with None -> (fun x -> x) | Some n -> list_take n)
|
---|
63 | @@ List.fast_sort comp
|
---|
64 | @@ List.fold_left (fold_valid_text predicate (fun a e -> List.cons e a)) []
|
---|
65 | @@ list_fs store
|
---|
66 |
|
---|
67 | let fold ?(predicate=fun _ -> true) ?order ?number fn acc {store;_} =
|
---|
68 | match order with
|
---|
69 | | None -> List.fold_left (fold_valid_text predicate fn) acc @@ list_fs store
|
---|
70 | | Some comp ->
|
---|
71 | List.fold_left fn acc
|
---|
72 | @@ (match number with None -> (fun x -> x) | Some n -> list_take n)
|
---|
73 | @@ List.fast_sort comp
|
---|
74 | @@ List.fold_left (fold_valid_text predicate (fun a e -> List.cons e a)) []
|
---|
75 | @@ list_fs store
|
---|
76 |
|
---|
77 | let with_id { store; _ } id =
|
---|
78 | let matched acc path =
|
---|
79 | match to_text path with
|
---|
80 | | Error x -> prerr_endline x; acc
|
---|
81 | | Ok text when text.Text.uuid <> id -> acc
|
---|
82 | | Ok text ->
|
---|
83 | match acc with
|
---|
84 | | Ok None -> Ok (Some text)
|
---|
85 | | Ok (Some prev) -> if prev = text then acc else Error [text; prev]
|
---|
86 | | Error x -> Error (text :: x)
|
---|
87 | in List.fold_left matched (Ok None) (list_fs store)
|
---|
88 |
|
---|
89 | module Directory = struct
|
---|
90 | let print ?(descr="") dir result =
|
---|
91 | let () = match result with
|
---|
92 | | Ok true -> print_endline ("Created " ^ descr ^ " directory " ^ dir)
|
---|
93 | | Ok false -> print_endline ("Using existing " ^ descr ^ " directory " ^ dir)
|
---|
94 | | Error (`Msg msg) -> prerr_endline @@ "Failed making " ^ descr ^ ". " ^ msg
|
---|
95 | in
|
---|
96 | result
|
---|
97 |
|
---|
98 | let directory dir = Result.bind (Fpath.of_string dir) Bos.OS.Dir.create
|
---|
99 |
|
---|
100 | let rec directories = function
|
---|
101 | | [] -> Ok ()
|
---|
102 | | (d, descr)::tl ->
|
---|
103 | match directory d |> print ~descr d with
|
---|
104 | | Ok _ -> directories tl
|
---|
105 | | Error _ -> Error (d, descr)
|
---|
106 | end
|
---|
107 |
|
---|
108 | let copy ?(recursive = false) src dst =
|
---|
109 | Bos.OS.Cmd.run (Bos.Cmd.(v "cp" %% (on recursive @@ v "-r") % src % dst))
|
---|
110 |
|
---|
111 | let versioned_basename_of_title ?(version=0) repo extension (title : string) =
|
---|
112 | let basename = Text.string_alias title in
|
---|
113 | let rec next version =
|
---|
114 | let candidate = repo // basename ^ "." ^ string_of_int version ^ extension in
|
---|
115 | if Sys.file_exists candidate then next (succ version) else candidate
|
---|
116 | in
|
---|
117 | next version
|
---|
118 |
|
---|
119 | let uuid_filename repo extension text =
|
---|
120 | let basename = Text.alias text in
|
---|
121 | let candidate = repo // String.sub (Id.to_string text.uuid) 0 6 ^ "." ^ basename ^ extension in
|
---|
122 | if Sys.file_exists candidate then Error "Name clash, try again" else Ok candidate
|
---|
123 |
|
---|
124 | let with_text {store;_} new_text =
|
---|
125 | Result.bind (uuid_filename store extension new_text) @@
|
---|
126 | fun path ->
|
---|
127 | try file path (Text.to_string new_text); Ok (path, new_text) with Sys_error s -> Error s
|
---|
128 |
|
---|
129 | let basic_config () =
|
---|
130 | "Archive-Name: "
|
---|
131 | ^ "\nArchive-ID: " ^ Id.(generate () |> to_string)
|
---|
132 | ^ "\nArchivists: " ^ Bos.OS.Env.opt_var "USER" ~absent:""
|
---|
133 | |> Bytes.of_string
|
---|
134 |
|
---|
135 | let init ?(dotdir=".logarion/") () =
|
---|
136 | match Directory.directories [dotdir, "dotdir"] with
|
---|
137 | | Error (_dir, _desc) -> ()
|
---|
138 | | Ok () ->
|
---|
139 | let config_file =
|
---|
140 | open_out_gen [Open_creat; Open_excl; Open_wronly]
|
---|
141 | 0o700 (dotdir // "config") in
|
---|
142 | output_bytes config_file (basic_config ());
|
---|
143 | close_out config_file
|
---|
144 |
|
---|
145 | module Config = struct
|
---|
146 | type t = archive_t
|
---|
147 | let key_value k v a = match k with
|
---|
148 | | "Archive-Name" -> { a with name = String.trim v }
|
---|
149 | | "Archive-ID" -> { a with id = Option.get (Id.of_string (String.trim v)) }
|
---|
150 | | "Archivists" -> { a with archivists = Person.Set.of_string v }
|
---|
151 | | _ -> { a with kv = Store.KV.add k (String.trim v) a.kv }
|
---|
152 | end
|
---|
153 |
|
---|
154 | let of_path store =
|
---|
155 | let open Text_parse in
|
---|
156 | let subsyntaxes = [| (module Parsers.Key_value.Make (Config) : Parser.S with type t = Config.t); (module Parsers.Key_value.Make (Config)); |] in
|
---|
157 | let of_string text acc = Parser.parse subsyntaxes { text; pos = 0; right_boundary = String.length text - 1 } acc in
|
---|
158 | Ok (
|
---|
159 | of_string (to_string @@ store ^ "/.logarion/config") {
|
---|
160 | name = "";
|
---|
161 | archivists = Person.Set.empty;
|
---|
162 | id = Id.nil;
|
---|
163 | kv = Store.KV.empty;
|
---|
164 | store = try Sys.getenv "LOGARION_DIR" with Not_found -> "."
|
---|
165 | }
|
---|
166 | )
|
---|