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