1use std::io::Read;
2
3use crate::control::ControlFile;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HttpError {
7 #[error("HTTP error: {0}")]
8 Http(String),
9 #[error("IO error: {0}")]
10 Io(#[from] std::io::Error),
11 #[error("Invalid URL: {0}")]
12 InvalidUrl(String),
13 #[error("No URLs available")]
14 NoUrls,
15}
16
17pub struct HttpClient {
18 agent: ureq::Agent,
19}
20
21impl Default for HttpClient {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl HttpClient {
28 pub fn new() -> Self {
29 Self {
30 agent: ureq::Agent::config_builder()
31 .https_only(false)
32 .build()
33 .new_agent(),
34 }
35 }
36
37 pub fn fetch_control_file(&self, url: &str) -> Result<ControlFile, HttpError> {
38 let response = self
39 .agent
40 .get(url)
41 .call()
42 .map_err(|e| HttpError::Http(e.to_string()))?;
43
44 let mut reader = response.into_body().into_reader();
45 ControlFile::parse(&mut reader).map_err(|e| HttpError::Http(e.to_string()))
46 }
47
48 pub fn fetch_range(&self, url: &str, start: u64, end: u64) -> Result<Vec<u8>, HttpError> {
49 let range_header = format!("bytes={}-{}", start, end);
50
51 let response = self
52 .agent
53 .get(url)
54 .header("Range", &range_header)
55 .call()
56 .map_err(|e| HttpError::Http(e.to_string()))?;
57
58 let status = response.status();
59 if status != 206 && status != 200 {
60 return Err(HttpError::Http(format!(
61 "Expected 206 Partial Content, got {}",
62 status
63 )));
64 }
65
66 let mut buf = Vec::new();
67 response.into_body().into_reader().read_to_end(&mut buf)?;
68
69 Ok(buf)
70 }
71
72 pub fn fetch_ranges(
73 &self,
74 url: &str,
75 ranges: &[(u64, u64)],
76 blocksize: usize,
77 ) -> Result<Vec<(u64, Vec<u8>)>, HttpError> {
78 let mut results = Vec::new();
79
80 for &(start, end) in ranges {
81 let data = self.fetch_range(url, start, end)?;
82 let aligned_start = (start / blocksize as u64) * blocksize as u64;
83 results.push((aligned_start, data));
84 }
85
86 Ok(results)
87 }
88}
89
90pub const DEFAULT_RANGE_GAP_THRESHOLD: u64 = 256 * 1024;
92
93pub fn merge_byte_ranges(ranges: &[(u64, u64)], gap_threshold: u64) -> Vec<(u64, u64)> {
96 if ranges.len() <= 1 {
97 return ranges.to_vec();
98 }
99
100 let mut merged = vec![ranges[0]];
101 for &(start, end) in &ranges[1..] {
102 let last = merged.last_mut().unwrap();
103 let gap = start.saturating_sub(last.1 + 1);
104 if gap <= gap_threshold {
105 last.1 = end;
106 } else {
107 merged.push((start, end));
108 }
109 }
110 merged
111}
112
113pub fn byte_ranges_from_block_ranges(
114 block_ranges: &[(usize, usize)],
115 blocksize: usize,
116 file_length: u64,
117) -> Vec<(u64, u64)> {
118 block_ranges
119 .iter()
120 .map(|&(start_block, end_block)| {
121 let start = start_block as u64 * blocksize as u64;
122 let end =
123 ((end_block as u64 * blocksize as u64).saturating_sub(1)).min(file_length - 1);
124 (start, end)
125 })
126 .collect()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_byte_ranges_from_block_ranges() {
135 let block_ranges = vec![(0, 2), (4, 6)];
136 let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 10000);
137 assert_eq!(byte_ranges, vec![(0, 2047), (4096, 6143)]);
138 }
139
140 #[test]
141 fn test_byte_ranges_clamped_to_file_length() {
142 let block_ranges = vec![(9, 10)];
143 let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 9500);
144 assert_eq!(byte_ranges, vec![(9216, 9499)]);
145 }
146}