use std::io::{Error as IoError, ErrorKind, Read, Result as IoResult, Seek, SeekFrom};
use std::time::Duration;
const CHUNK: usize = 512 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
pub struct RangeStreamSource {
url: String,
client: reqwest::blocking::Client,
total_size: u64,
pos: u64,
chunk: Vec<u8>,
chunk_start: u64,
}
impl RangeStreamSource {
pub fn new(url: String, user_agent: Option<String>) -> IoResult<Self> {
let ua =
user_agent.unwrap_or_else(|| concat!("Kopuz/", env!("CARGO_PKG_VERSION")).to_string());
let client = reqwest::blocking::Client::builder()
.tcp_nodelay(true)
.user_agent(ua)
.timeout(REQUEST_TIMEOUT)
.build()
.map_err(IoError::other)?;
let resp = client
.get(&url)
.header("Range", "bytes=0-0")
.send()
.map_err(IoError::other)?;
let status = resp.status();
if !status.is_success() {
return Err(IoError::other(format!("range probe HTTP {status}")));
}
let total_size = parse_total_size(&resp)
.ok_or_else(|| IoError::other("server didn't expose total size on range probe"))?;
Ok(Self {
url,
client,
total_size,
pos: 0,
chunk: Vec::with_capacity(CHUNK),
chunk_start: 0,
})
}
pub fn total_size(&self) -> u64 {
self.total_size
}
fn fetch_chunk(&mut self, start: u64) -> IoResult<()> {
let end = (start + CHUNK as u64 - 1).min(self.total_size - 1);
let resp = self
.client
.get(&self.url)
.header("Range", format!("bytes={start}-{end}"))
.send()
.map_err(IoError::other)?;
if !resp.status().is_success() {
return Err(IoError::other(format!(
"range fetch {start}-{end} HTTP {}",
resp.status()
)));
}
let bytes = resp.bytes().map_err(IoError::other)?;
self.chunk.clear();
self.chunk.extend_from_slice(&bytes);
self.chunk_start = start;
Ok(())
}
fn pos_in_cache(&self, pos: u64) -> bool {
!self.chunk.is_empty()
&& pos >= self.chunk_start
&& pos < self.chunk_start + self.chunk.len() as u64
}
}
impl Read for RangeStreamSource {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
if self.pos >= self.total_size {
return Ok(0);
}
if !self.pos_in_cache(self.pos) {
self.fetch_chunk(self.pos)?;
if self.chunk.is_empty() {
return Ok(0);
}
}
let offset = (self.pos - self.chunk_start) as usize;
let available = self.chunk.len() - offset;
let to_copy = available.min(buf.len());
buf[..to_copy].copy_from_slice(&self.chunk[offset..offset + to_copy]);
self.pos += to_copy as u64;
Ok(to_copy)
}
}
impl Seek for RangeStreamSource {
fn seek(&mut self, p: SeekFrom) -> IoResult<u64> {
let new_pos: i64 = match p {
SeekFrom::Start(n) => n as i64,
SeekFrom::Current(n) => self.pos as i64 + n,
SeekFrom::End(n) => self.total_size as i64 + n,
};
if new_pos < 0 {
return Err(IoError::new(
ErrorKind::InvalidInput,
"seek to negative position",
));
}
self.pos = new_pos as u64;
Ok(self.pos)
}
}
fn parse_total_size(resp: &reqwest::blocking::Response) -> Option<u64> {
if let Some(v) = resp.headers().get("content-range")
&& let Ok(s) = v.to_str()
&& let Some(slash) = s.rfind('/')
{
let tail = &s[slash + 1..];
if tail != "*"
&& let Ok(n) = tail.parse()
{
return Some(n);
}
}
resp.content_length()
}