feat: support multipart ranges (#535)

pull/556/head
45gfg9 2025-02-01 08:28:34 +08:00 committed by GitHub
parent d255f1376a
commit eda9769b2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 163 additions and 52 deletions

View File

@ -843,7 +843,7 @@ impl Server {
}
}
let range = if use_range {
let ranges = if use_range {
headers.get(RANGE).map(|range| {
range
.to_str()
@ -864,27 +864,59 @@ impl Server {
res.headers_mut().typed_insert(AcceptRanges::bytes());
if let Some(range) = range {
if let Some((start, end)) = range {
file.seek(SeekFrom::Start(start)).await?;
let range_size = end - start + 1;
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let content_range = format!("bytes {}-{}/{}", start, end, size);
res.headers_mut()
.insert(CONTENT_RANGE, content_range.parse()?);
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
if head_only {
return Ok(());
}
if let Some(ranges) = ranges {
if let Some(ranges) = ranges {
if ranges.len() == 1 {
let (start, end) = ranges[0];
file.seek(SeekFrom::Start(start)).await?;
let range_size = end - start + 1;
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let content_range = format!("bytes {}-{}/{}", start, end, size);
res.headers_mut()
.insert(CONTENT_RANGE, content_range.parse()?);
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
if head_only {
return Ok(());
}
let stream_body = StreamBody::new(
LengthLimitedStream::new(file, range_size as usize)
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
let boxed_body = stream_body.boxed();
*res.body_mut() = boxed_body;
let stream_body = StreamBody::new(
LengthLimitedStream::new(file, range_size as usize)
.map_ok(Frame::data)
.map_err(|err| anyhow!("{err}")),
);
let boxed_body = stream_body.boxed();
*res.body_mut() = boxed_body;
} else {
*res.status_mut() = StatusCode::PARTIAL_CONTENT;
let boundary = Uuid::new_v4();
let mut body = Vec::new();
let content_type = get_content_type(path).await?;
for (start, end) in ranges {
file.seek(SeekFrom::Start(start)).await?;
let range_size = end - start + 1;
let content_range = format!("bytes {}-{}/{}", start, end, size);
let part_header = format!(
"--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n",
);
body.extend_from_slice(part_header.as_bytes());
let mut buffer = vec![0; range_size as usize];
file.read_exact(&mut buffer).await?;
body.extend_from_slice(&buffer);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
res.headers_mut().insert(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}").parse()?,
);
res.headers_mut()
.insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?);
if head_only {
return Ok(());
}
*res.body_mut() = body_full(body);
}
} else {
*res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
res.headers_mut()
@ -1771,8 +1803,10 @@ fn parse_upload_offset(headers: &HeaderMap<HeaderValue>, size: u64) -> Result<Op
if value == "append" {
return Ok(Some(size));
}
let (start, _) = parse_range(value, size).ok_or_else(err)?;
Ok(Some(start))
// use the first range
let ranges = parse_range(value, size).ok_or_else(err)?;
let (start, _) = ranges.first().ok_or_else(err)?;
Ok(Some(*start))
}
async fn sha256_file(path: &Path) -> Result<String> {

View File

@ -100,36 +100,42 @@ pub fn load_private_key<T: AsRef<Path>>(filename: T) -> Result<PrivateKeyDer<'st
anyhow::bail!("No supported private key in file");
}
pub fn parse_range(range: &str, size: u64) -> Option<(u64, u64)> {
let (unit, range) = range.split_once('=')?;
if unit != "bytes" || range.contains(',') {
pub fn parse_range(range: &str, size: u64) -> Option<Vec<(u64, u64)>> {
let (unit, ranges) = range.split_once('=')?;
if unit != "bytes" {
return None;
}
let (start, end) = range.split_once('-')?;
if start.is_empty() {
let offset = end.parse::<u64>().ok()?;
if offset <= size {
Some((size - offset, size - 1))
} else {
None
}
} else {
let start = start.parse::<u64>().ok()?;
if start < size {
if end.is_empty() {
Some((start, size - 1))
let mut result = Vec::new();
for range in ranges.split(',') {
let (start, end) = range.trim().split_once('-')?;
if start.is_empty() {
let offset = end.parse::<u64>().ok()?;
if offset <= size {
result.push((size - offset, size - 1));
} else {
let end = end.parse::<u64>().ok()?;
if end < size {
Some((start, end))
} else {
None
}
return None;
}
} else {
None
let start = start.parse::<u64>().ok()?;
if start < size {
if end.is_empty() {
result.push((start, size - 1));
} else {
let end = end.parse::<u64>().ok()?;
if end < size {
result.push((start, end));
} else {
return None;
}
}
} else {
return None;
}
}
}
Some(result)
}
#[cfg(test)]
@ -162,13 +168,19 @@ mod tests {
#[test]
fn test_parse_range() {
assert_eq!(parse_range("bytes=0-499", 500), Some((0, 499)));
assert_eq!(parse_range("bytes=0-", 500), Some((0, 499)));
assert_eq!(parse_range("bytes=299-", 500), Some((299, 499)));
assert_eq!(parse_range("bytes=-500", 500), Some((0, 499)));
assert_eq!(parse_range("bytes=-300", 500), Some((200, 499)));
assert_eq!(parse_range("bytes=0-499", 500), Some(vec![(0, 499)]));
assert_eq!(parse_range("bytes=0-", 500), Some(vec![(0, 499)]));
assert_eq!(parse_range("bytes=299-", 500), Some(vec![(299, 499)]));
assert_eq!(parse_range("bytes=-500", 500), Some(vec![(0, 499)]));
assert_eq!(parse_range("bytes=-300", 500), Some(vec![(200, 499)]));
assert_eq!(
parse_range("bytes=0-199, 100-399, 400-, -200", 500),
Some(vec![(0, 199), (100, 399), (400, 499), (300, 499)])
);
assert_eq!(parse_range("bytes=500-", 500), None);
assert_eq!(parse_range("bytes=-501", 500), None);
assert_eq!(parse_range("bytes=0-500", 500), None);
assert_eq!(parse_range("bytes=0-199,", 500), None);
assert_eq!(parse_range("bytes=0-199, 500-", 500), None);
}
}

View File

@ -2,7 +2,7 @@ mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use reqwest::header::HeaderValue;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use rstest::rstest;
#[rstest]
@ -39,3 +39,68 @@ fn get_file_range_invalid(server: TestServer) -> Result<(), Error> {
assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18");
Ok(())
}
fn parse_multipart_body<'a>(body: &'a str, boundary: &str) -> Vec<(HeaderMap, &'a str)> {
body.split(&format!("--{}", boundary))
.filter(|part| !part.is_empty() && *part != "--\r\n")
.map(|part| {
let (head, body) = part.trim_ascii().split_once("\r\n\r\n").unwrap();
let headers = head
.split("\r\n")
.fold(HeaderMap::new(), |mut headers, header| {
let (key, value) = header.split_once(":").unwrap();
let key = HeaderName::from_bytes(key.as_bytes()).unwrap();
let value = HeaderValue::from_str(value.trim_ascii_start()).unwrap();
headers.insert(key, value);
headers
});
(headers, body)
})
.collect()
}
#[rstest]
fn get_file_multipart_range(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
.header("range", HeaderValue::from_static("bytes=0-11, 6-17"))
.send()?;
assert_eq!(resp.status(), 206);
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
let content_type = resp
.headers()
.get("content-type")
.unwrap()
.to_str()?
.to_string();
assert!(content_type.starts_with("multipart/byteranges; boundary="));
let boundary = content_type.split_once('=').unwrap().1.trim_ascii_start();
assert!(!boundary.is_empty());
let body = resp.text()?;
let parts = parse_multipart_body(&body, boundary);
assert_eq!(parts.len(), 2);
let (headers, body) = &parts[0];
assert_eq!(headers.get("content-range").unwrap(), "bytes 0-11/18");
assert_eq!(*body, "This is inde");
let (headers, body) = &parts[1];
assert_eq!(headers.get("content-range").unwrap(), "bytes 6-17/18");
assert_eq!(*body, "s index.html");
Ok(())
}
#[rstest]
fn get_file_multipart_range_invalid(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
.header("range", HeaderValue::from_static("bytes=0-6, 20-30"))
.send()?;
assert_eq!(resp.status(), 416);
assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18");
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
assert_eq!(resp.headers().get("content-length").unwrap(), "0");
Ok(())
}