My agentic slop goes here. Not intended for anyone else!
1(** Chunk file management for partial downloads *)
2
3open Eio
4
5module Range = Range
6module Flags = Flags
7
8type t = {
9 key : string;
10 hash : string;
11 range : Range.t;
12 path : Fs.dir_ty Path.t;
13 flags : Flags.t;
14 ttl : float option;
15}
16
17let key c = c.key
18let hash c = c.hash
19let range c = c.range
20let path c = c.path
21let flags c = c.flags
22let ttl c = c.ttl
23
24
25(** Parse a chunk from a filename *)
26let parse ~path ~filename =
27 try
28 (* Expected format: {hash_rest}:2:{ttl},{flags}:{start}-{end} *)
29 match String.split_on_char ':' filename with
30 | hash_rest :: "2" :: rest when List.length rest >= 1 ->
31 (* Extract components *)
32 let ttl_flags_range = String.concat ":" rest in
33
34 (* Find the last colon that separates range *)
35 (match String.rindex_opt ttl_flags_range ':' with
36 | None -> None (* No range part *)
37 | Some idx ->
38 let ttl_flags = String.sub ttl_flags_range 0 idx in
39 let range_str = String.sub ttl_flags_range (idx + 1)
40 (String.length ttl_flags_range - idx - 1) in
41
42 (* Parse range *)
43 match Range.of_string range_str with
44 | None -> None
45 | Some range ->
46 (* Parse TTL and flags *)
47 let ttl, flags_str =
48 match String.split_on_char ',' ttl_flags with
49 | [] -> None, ""
50 | [ttl_or_flags] ->
51 if String.contains ttl_or_flags ',' then
52 None, ttl_or_flags
53 else
54 (try Some (float_of_string ttl_or_flags) with _ -> None), ""
55 | ttl :: rest ->
56 (try Some (float_of_string ttl) with _ -> None),
57 String.concat "," rest
58 in
59
60 let flags = Flags.of_string flags_str in
61
62 (* Only consider it a chunk if it has the Chunk flag *)
63 if Flags.is_chunk flags then
64 (* We don't have the full hash or key from just the filename *)
65 (* The caller needs to provide these through context *)
66 Some {
67 key = ""; (* Will be filled by caller *)
68 hash = hash_rest; (* Partial hash *)
69 range;
70 path;
71 flags;
72 ttl;
73 }
74 else
75 None)
76 | _ -> None
77 with _ -> None
78
79(** Generate a chunk filename *)
80let make_filename ~hash ~range ?ttl ?(flags=Flags.empty) () =
81 (* Ensure Chunk flag is set *)
82 let flags = Flags.add `Chunk flags in
83
84 let hash_rest = String.sub hash 4 (String.length hash - 4) in
85 let ttl_part = match ttl with
86 | None -> ""
87 | Some t -> Printf.sprintf "%d" (int_of_float t)
88 in
89 let flags_str = Flags.to_string flags in
90 let range_str = Range.to_string range in
91
92 Printf.sprintf "%s:2:%s,%s:%s" hash_rest ttl_part flags_str range_str
93
94(** Hash a key using SHA256 *)
95let hash_key key =
96 Digestif.SHA256.(to_hex (digest_string key))
97
98(** Get directory components from hash *)
99let key_to_dirs key =
100 let hash = hash_key key in
101 if String.length hash < 4 then
102 invalid_arg "hash too short"
103 else
104 [ String.sub hash 0 2;
105 String.sub hash 2 2 ]
106
107(** Find all chunks for a key *)
108let find_chunks ~base_dir ~key =
109 let dirs = key_to_dirs key in
110 let dir_path = List.fold_left (fun acc d -> Path.(acc / d)) base_dir dirs in
111
112 try
113 let entries = Path.read_dir dir_path in
114 let hash = hash_key key in
115 let hash_rest = String.sub hash 4 (String.length hash - 4) in
116
117 List.filter_map (fun entry ->
118 if String.starts_with ~prefix:(hash_rest ^ ":2") entry then
119 match parse ~path:Path.(dir_path / entry) ~filename:entry with
120 | Some chunk ->
121 (* Fill in the complete key and hash *)
122 Some { chunk with key; hash }
123 | None -> None
124 else None
125 ) entries
126 with _ -> []
127
128(** Sort chunks by range start position *)
129let sort_by_range chunks =
130 List.sort (fun c1 c2 -> Range.compare c1.range c2.range) chunks
131
132(** Check if chunks form a complete sequence *)
133let is_complete_sequence chunks ~total_size =
134 let sorted = sort_by_range chunks in
135
136 let rec check_continuous pos = function
137 | [] -> pos >= total_size
138 | chunk :: rest ->
139 if Range.start chunk.range > pos then
140 false (* Gap found *)
141 else
142 let next_pos = Int64.succ (Range.end_ chunk.range) in
143 check_continuous (max pos next_pos) rest
144 in
145
146 check_continuous 0L sorted
147
148(** Get missing ranges from a list of chunks *)
149let missing_ranges chunks ~total_size =
150 let sorted = sort_by_range chunks in
151
152 let rec find_gaps acc pos = function
153 | [] ->
154 if pos < total_size then
155 (* Gap at the end *)
156 List.rev (Range.create ~start:pos ~end_:(Int64.pred total_size) :: acc)
157 else
158 List.rev acc
159 | chunk :: rest ->
160 let chunk_start = Range.start chunk.range in
161 if chunk_start > pos then
162 (* Found a gap *)
163 let gap = Range.create ~start:pos ~end_:(Int64.pred chunk_start) in
164 find_gaps (gap :: acc) (Int64.succ (Range.end_ chunk.range)) rest
165 else
166 (* No gap, move to next position *)
167 find_gaps acc (max pos (Int64.succ (Range.end_ chunk.range))) rest
168 in
169
170 find_gaps [] 0L sorted
171
172let pp fmt c =
173 Format.fprintf fmt "Chunk{key=%s, range=%a, flags=%a}"
174 c.key Range.pp c.range Flags.pp c.flags