|
| 1 | +(* This module implements a Zip file zarr store that uses the Eio library for |
| 2 | + non-blocking I/O operations. The main requirement is to implement the signature |
| 3 | + of Zarr.Types.IO. Below we show how to implement this custom Zarr Store. |
| 4 | +
|
| 5 | + To compile & run this example execute the command |
| 6 | + dune exec -- examples/zipstore.exe |
| 7 | + in your shell at the root of this project. *) |
| 8 | + |
| 9 | +module ZipStore : sig |
| 10 | + include Zarr.Storage.STORE with module Deferred = Zarr_eio.Deferred |
| 11 | + val with_open : ?clevel:int -> string -> (t -> 'a) -> 'a |
| 12 | +end = struct |
| 13 | + |
| 14 | + module Z = struct |
| 15 | + module Deferred = Zarr_eio.Deferred |
| 16 | + |
| 17 | + type t = {path : string; level : int option} |
| 18 | + |
| 19 | + let with_open_in path f = |
| 20 | + let ic = Zip.open_in path in |
| 21 | + Fun.protect ~finally:(fun () -> Zip.close_in ic) (fun () -> f ic) |
| 22 | + |
| 23 | + let with_open_out path f = |
| 24 | + let oc = Zip.open_update path in |
| 25 | + Fun.protect ~finally:(fun () -> Zip.close_out oc) (fun () -> f oc) |
| 26 | + |
| 27 | + let is_member t key = |
| 28 | + let entry_exists ~key ic = match Zip.find_entry ic key with |
| 29 | + | exception Not_found -> false |
| 30 | + | _ -> true |
| 31 | + in |
| 32 | + with_open_in t.path (entry_exists ~key) |
| 33 | + |
| 34 | + let size t key = |
| 35 | + let entry_size ~key ic = match Zip.find_entry ic key with |
| 36 | + | exception Not_found -> 0 |
| 37 | + | e -> e.uncompressed_size |
| 38 | + in |
| 39 | + with_open_in t.path (entry_size ~key) |
| 40 | + |
| 41 | + let get t key = |
| 42 | + let read_entry ~key ic = match Zip.find_entry ic key with |
| 43 | + | exception Not_found -> raise (Zarr.Storage.Key_not_found key) |
| 44 | + | e -> Zip.read_entry ic e |
| 45 | + in |
| 46 | + with_open_in t.path (read_entry ~key) |
| 47 | + |
| 48 | + let get_partial_values t key ranges = |
| 49 | + let read_range ~data ~size (ofs, len) = match len with |
| 50 | + | Some l -> String.sub data ofs l |
| 51 | + | None -> String.sub data ofs (size - ofs) |
| 52 | + in |
| 53 | + let data = get t key in |
| 54 | + let size = String.length data in |
| 55 | + List.map (read_range ~data ~size) ranges |
| 56 | + |
| 57 | + let list t = |
| 58 | + let entry_filename = function |
| 59 | + | (e : Zip.entry) when not e.is_directory -> Some e.filename |
| 60 | + | _ -> None |
| 61 | + in |
| 62 | + let entries = with_open_in t.path (fun ic -> Zip.entries ic) in |
| 63 | + List.filter_map entry_filename entries |
| 64 | + |
| 65 | + let list_dir t prefix = |
| 66 | + let module S = Set.Make(String) in |
| 67 | + let n = String.length prefix in |
| 68 | + let add_entry_with_prefix ((l, r) as acc) = function |
| 69 | + | (e : Zip.entry) when e.is_directory -> acc |
| 70 | + | e when not (String.starts_with ~prefix e.filename) -> acc |
| 71 | + | e when String.contains_from e.filename n '/' -> |
| 72 | + let key = e.filename in |
| 73 | + let pre = String.sub key 0 (1 + String.index_from key n '/') in |
| 74 | + S.add pre l, r |
| 75 | + | e -> l, e.filename :: r |
| 76 | + in |
| 77 | + let entries = with_open_in t.path (fun ic -> Zip.entries ic) in |
| 78 | + let prefs, keys = List.fold_left add_entry_with_prefix (S.empty, []) entries in |
| 79 | + keys, S.elements prefs |
| 80 | + |
| 81 | + let set t key value = |
| 82 | + with_open_out t.path (fun oc -> Zip.add_entry ?level:t.level value oc key) |
| 83 | + |
| 84 | + let set_partial_values t key ?(append=false) rvs = |
| 85 | + let ov = try get t key with |
| 86 | + | Zarr.Storage.Key_not_found _ -> String.empty |
| 87 | + in |
| 88 | + let f = if append || ov = String.empty then |
| 89 | + fun acc (_, v) -> acc ^ v else |
| 90 | + fun acc (rs, v) -> |
| 91 | + let s = Bytes.unsafe_of_string acc in |
| 92 | + Bytes.blit_string v 0 s rs String.(length v); |
| 93 | + Bytes.unsafe_to_string s |
| 94 | + in |
| 95 | + set t key (List.fold_left f ov rvs) |
| 96 | + |
| 97 | + let add_to_zip ~oc ~level (path, v) = Zip.add_entry ?level v oc path |
| 98 | + |
| 99 | + let rename t prefix new_prefix = |
| 100 | + let add_pair ~ic ~prefix ~new_prefix acc = function |
| 101 | + | (e : Zip.entry) when not (String.starts_with ~prefix e.filename) -> |
| 102 | + (e.filename, Zip.read_entry ic e) :: acc |
| 103 | + | e -> |
| 104 | + let l = String.length prefix in |
| 105 | + let path = new_prefix ^ String.sub e.filename l (String.length e.filename - l) in |
| 106 | + (path, Zip.read_entry ic e) :: acc |
| 107 | + in |
| 108 | + let rename_entries ic = |
| 109 | + List.fold_left (add_pair ~ic ~prefix ~new_prefix) [] (Zip.entries ic) |
| 110 | + in |
| 111 | + let pairs = with_open_in t.path rename_entries in |
| 112 | + let oc = Zip.open_out t.path in Zip.close_out oc; (* truncate the old zip file *) |
| 113 | + with_open_out t.path @@ fun oc -> List.iter (add_to_zip ~oc ~level:t.level) pairs |
| 114 | + |
| 115 | + let erase t key = |
| 116 | + let filter ~ic acc = function |
| 117 | + | (e : Zip.entry) when e.filename = key -> acc |
| 118 | + | e -> (e.filename, Zip.read_entry ic e) :: acc |
| 119 | + in |
| 120 | + let filter_entries ic = List.fold_left (filter ~ic) [] (Zip.entries ic) in |
| 121 | + let pairs = with_open_in t.path filter_entries in |
| 122 | + let oc = Zip.open_out t.path in Zip.close_out oc; (* truncate the old zip file *) |
| 123 | + with_open_out t.path @@ fun oc -> List.iter (add_to_zip ~oc ~level:t.level) pairs |
| 124 | + |
| 125 | + let erase_prefix t prefix = |
| 126 | + let filter ~ic ~prefix acc = function |
| 127 | + | (e : Zip.entry) when String.starts_with ~prefix e.filename -> acc |
| 128 | + | e -> (e.filename, Zip.read_entry ic e) :: acc |
| 129 | + in |
| 130 | + let filter_entries ic = List.fold_left (filter ~ic ~prefix) [] (Zip.entries ic) in |
| 131 | + let pairs = with_open_in t.path filter_entries in |
| 132 | + let oc = Zip.open_out t.path in Zip.close_out oc; (* truncate the old zip file *) |
| 133 | + with_open_out t.path @@ fun oc -> List.iter (add_to_zip ~oc ~level:t.level) pairs |
| 134 | + end |
| 135 | + |
| 136 | + include Zarr.Storage.Make(Z) |
| 137 | + |
| 138 | + let with_open ?clevel path f = |
| 139 | + if not @@ Sys.file_exists path then begin |
| 140 | + Zip.(close_out @@ open_out path) |
| 141 | + end; |
| 142 | + let level = match clevel with |
| 143 | + | Some l when l < 0 || l > 9 -> |
| 144 | + raise @@ invalid_arg (Printf.sprintf "wrong compression level: %d" l) |
| 145 | + | l -> l |
| 146 | + in |
| 147 | + f Z.{path; level} |
| 148 | +end |
| 149 | + |
| 150 | +let _ = |
| 151 | + Eio_main.run @@ fun _ -> |
| 152 | + let open Zarr in |
| 153 | + let open Zarr.Ndarray in |
| 154 | + let open Zarr.Indexing in |
| 155 | + |
| 156 | + let test_functionality store = |
| 157 | + let xs, _ = ZipStore.hierarchy store in |
| 158 | + let anode = List.hd @@ List.filter |
| 159 | + (fun node -> Node.Array.to_path node = "/some/group/name") xs in |
| 160 | + let slice = [|R [|0; 20|]; I 10; R [||]|] in |
| 161 | + let x = ZipStore.Array.read store anode slice Char in |
| 162 | + let x' = Zarr.Ndarray.map (fun _ -> Random.int 256 |> Char.chr) x in |
| 163 | + ZipStore.Array.write store anode slice x'; |
| 164 | + let y = ZipStore.Array.read store anode slice Char in |
| 165 | + assert (Zarr.Ndarray.equal x' y); |
| 166 | + ZipStore.Array.rename store anode "name2"; |
| 167 | + let exists = ZipStore.Array.exists store @@ Node.Array.of_path "/some/group/name2" in |
| 168 | + assert exists; |
| 169 | + ZipStore.clear store (* deletes all zip entries *) |
| 170 | + in |
| 171 | + ZipStore.with_open "examples/data/testdata.zip" test_functionality; |
| 172 | + print_endline "Zip store has been updated." |
0 commit comments