My agentic slop goes here. Not intended for anyone else!
at main 5.5 kB view raw
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