use std::io::Read;
use axum::body::{Body, Bytes};
use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde_json::Value;
use super::PlatformAppState;
const CHUNK: usize = 64 * 1024;
#[derive(Clone, Copy)]
pub(super) struct Asked<'a> {
pub method: &'a Method,
pub headers: &'a HeaderMap,
}
struct Stored {
store: String,
key: String,
size: u64,
modified: Option<i64>,
etag: String,
}
impl Stored {
fn read(value: &Value) -> Option<Self> {
Some(Self {
store: value.get("store")?.as_str()?.to_string(),
key: value.get("key")?.as_str()?.to_string(),
size: value.get("size")?.as_u64()?,
modified: value.get("modified").and_then(Value::as_i64),
etag: value.get("etag")?.as_str()?.to_string(),
})
}
fn last_modified(&self) -> Option<String> {
let secs = self.modified?;
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0).map(|t| t.format("%a, %d %b %Y %H:%M:%S GMT").to_string())
}
}
pub(super) async fn stored_response(
state: &PlatformAppState,
owner: &str,
project: &str,
stored: &Value,
status: Option<StatusCode>,
declared: &[(String, String)],
asked: Option<Asked<'_>>,
) -> Response {
let Some(stored) = Stored::read(stored) else {
return (StatusCode::INTERNAL_SERVER_ERROR, "a stored answer without its store, key, size and validator").into_response();
};
let get = Method::GET;
let method = asked.map(|a| a.method).unwrap_or(&get);
let empty = HeaderMap::new();
let request = asked.map(|a| a.headers).unwrap_or(&empty);
let plain = status.is_none_or(|s| s == StatusCode::OK) && (method == Method::GET || method == Method::HEAD);
let last_modified = stored.last_modified();
let mut answer_status = status.unwrap_or(StatusCode::OK);
let (mut offset, mut len) = (0u64, stored.size);
if plain {
if not_modified(request, &stored) {
return validators_only(StatusCode::NOT_MODIFIED, &stored, last_modified.as_deref(), declared);
}
if let Some(range) = text(request, header::RANGE)
&& if_range_holds(request, &stored, last_modified.as_deref())
{
match parse_range(range, stored.size) {
RangeAsk::Ignore => {}
RangeAsk::Bytes(start, end) => {
answer_status = StatusCode::PARTIAL_CONTENT;
(offset, len) = (start, end - start + 1);
}
RangeAsk::Unsatisfiable => {
let mut resp = validators_only(StatusCode::RANGE_NOT_SATISFIABLE, &stored, last_modified.as_deref(), declared);
if let Ok(value) = HeaderValue::from_str(&format!("bytes */{}", stored.size)) {
resp.headers_mut().insert(header::CONTENT_RANGE, value);
}
return resp;
}
}
}
}
let body = if method == Method::HEAD {
Body::empty()
} else {
match open(state, owner, project, &stored, offset, len).await {
Ok(reader) => stream(reader),
Err(code) if code == "ZEBFS_NOT_FOUND" => return StatusCode::NOT_FOUND.into_response(),
Err(code) => {
eprintln!("⚠ stored answer {owner}/{project}: '{}' in store '{}' could not be opened ({code})", stored.key, stored.store);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
}
};
let mut resp = Response::new(body);
*resp.status_mut() = answer_status;
super::apply_zf_extras(&mut resp, declared);
let headers = resp.headers_mut();
put_validators(headers, &stored, last_modified.as_deref());
headers.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
headers.insert(header::CONTENT_LENGTH, HeaderValue::from(len));
if answer_status == StatusCode::PARTIAL_CONTENT
&& let Ok(value) = HeaderValue::from_str(&format!("bytes {offset}-{}/{}", offset + len - 1, stored.size))
{
headers.insert(header::CONTENT_RANGE, value);
}
super::harden_byte_headers(headers);
resp
}
fn validators_only(status: StatusCode, stored: &Stored, last_modified: Option<&str>, declared: &[(String, String)]) -> Response {
let mut resp = Response::new(Body::empty());
*resp.status_mut() = status;
let keep: Vec<(String, String)> = declared
.iter()
.filter(|(name, _)| name.eq_ignore_ascii_case("cache-control") || name.eq_ignore_ascii_case("vary"))
.cloned()
.collect();
super::apply_zf_extras(&mut resp, &keep);
put_validators(resp.headers_mut(), stored, last_modified);
resp
}
fn put_validators(headers: &mut HeaderMap, stored: &Stored, last_modified: Option<&str>) {
if let Ok(value) = HeaderValue::from_str(&stored.etag) {
headers.insert(header::ETAG, value);
}
if let Some(value) = last_modified.and_then(|v| HeaderValue::from_str(v).ok()) {
headers.insert(header::LAST_MODIFIED, value);
}
}
fn text(headers: &HeaderMap, name: header::HeaderName) -> Option<&str> {
headers.get(name).and_then(|v| v.to_str().ok()).map(str::trim).filter(|v| !v.is_empty())
}
fn not_modified(request: &HeaderMap, stored: &Stored) -> bool {
if let Some(tags) = text(request, header::IF_NONE_MATCH) {
let ours = opaque(&stored.etag);
return tags.split(',').map(str::trim).any(|tag| tag == "*" || opaque(tag) == ours);
}
match (text(request, header::IF_MODIFIED_SINCE).and_then(parse_http_date), stored.modified) {
(Some(since), Some(modified)) => modified <= since,
_ => false,
}
}
fn if_range_holds(request: &HeaderMap, stored: &Stored, last_modified: Option<&str>) -> bool {
match text(request, header::IF_RANGE) {
None => true,
Some(tag) if tag.starts_with('"') => !stored.etag.starts_with("W/") && tag == stored.etag,
Some(tag) if tag.starts_with("W/") => false,
Some(date) => last_modified == Some(date),
}
}
fn opaque(tag: &str) -> &str {
tag.trim().trim_start_matches("W/")
}
fn parse_http_date(value: &str) -> Option<i64> {
chrono::DateTime::parse_from_rfc2822(value).ok().map(|t| t.timestamp())
}
#[derive(Debug, PartialEq)]
enum RangeAsk {
Ignore,
Bytes(u64, u64),
Unsatisfiable,
}
fn parse_range(value: &str, size: u64) -> RangeAsk {
let Some(spec) = value.trim().strip_prefix("bytes=") else { return RangeAsk::Ignore };
if spec.contains(',') {
return RangeAsk::Ignore;
}
let Some((first, last)) = spec.trim().split_once('-') else { return RangeAsk::Ignore };
let (first, last) = (first.trim(), last.trim());
let number = |s: &str| if s.bytes().all(|b| b.is_ascii_digit()) { s.parse::<u64>().ok() } else { None };
match (first.is_empty(), last.is_empty()) {
(true, true) => RangeAsk::Ignore,
(true, false) => match number(last) {
None => RangeAsk::Ignore,
Some(0) => RangeAsk::Unsatisfiable,
Some(_) if size == 0 => RangeAsk::Unsatisfiable,
Some(n) => RangeAsk::Bytes(size.saturating_sub(n), size - 1),
},
(false, _) => {
let Some(start) = number(first) else { return RangeAsk::Ignore };
let end = if last.is_empty() { Some(u64::MAX) } else { number(last) };
match end {
None => RangeAsk::Ignore,
Some(end) if end < start => RangeAsk::Ignore,
Some(_) if start >= size => RangeAsk::Unsatisfiable,
Some(end) => RangeAsk::Bytes(start, end.min(size - 1)),
}
}
}
}
async fn open(
state: &PlatformAppState,
owner: &str,
project: &str,
stored: &Stored,
offset: u64,
len: u64,
) -> Result<Box<dyn Read + Send>, &'static str> {
let platform = state.platform.clone();
let (owner, project, store, key) = (owner.to_string(), project.to_string(), stored.store.clone(), stored.key.clone());
tokio::task::spawn_blocking(move || {
let opened = platform.file.open_store(&owner, &project, Some(&store)).map_err(|err| err.code)?;
opened.fs.open_range(&key, offset, len).map_err(|err| err.code)
})
.await
.unwrap_or(Err("ZEBFS_IO"))
}
fn stream(mut reader: Box<dyn Read + Send>) -> Body {
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, std::io::Error>>(4);
tokio::task::spawn_blocking(move || {
let mut buf = vec![0u8; CHUNK];
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if tx.blocking_send(Ok(Bytes::copy_from_slice(&buf[..n]))).is_err() {
break;
}
}
Err(err) => {
let _ = tx.blocking_send(Err(err));
break;
}
}
}
});
Body::from_stream(futures::stream::unfold(rx, |mut rx| async move { rx.recv().await.map(|item| (item, rx)) }))
}
#[cfg(test)]
mod tests {
use super::{RangeAsk, parse_range};
#[test]
fn one_byte_range_is_served_and_anything_else_is_the_whole_file_or_416() {
assert_eq!(parse_range("bytes=0-99", 1000), RangeAsk::Bytes(0, 99));
assert_eq!(parse_range("bytes=900-", 1000), RangeAsk::Bytes(900, 999));
assert_eq!(parse_range("bytes=900-5000", 1000), RangeAsk::Bytes(900, 999));
assert_eq!(parse_range("bytes=-100", 1000), RangeAsk::Bytes(900, 999));
assert_eq!(parse_range("bytes=-5000", 1000), RangeAsk::Bytes(0, 999));
assert_eq!(parse_range("bytes=1000-", 1000), RangeAsk::Unsatisfiable);
assert_eq!(parse_range("bytes=-0", 1000), RangeAsk::Unsatisfiable);
assert_eq!(parse_range("bytes=0-", 0), RangeAsk::Unsatisfiable);
for ignored in ["bytes=0-1,5-6", "items=0-1", "bytes=5-1", "bytes=a-b", "bytes=-", "bytes=+1-2"] {
assert_eq!(parse_range(ignored, 1000), RangeAsk::Ignore, "{ignored}");
}
}
}