|
|
|
@ -121,13 +121,27 @@ async fn serve_dir_json (
|
|
|
|
|
#[instrument (level = "debug", skip (f))]
|
|
|
|
|
async fn serve_file (
|
|
|
|
|
mut f: File,
|
|
|
|
|
should_send_body: bool,
|
|
|
|
|
range: range::ValidParsed
|
|
|
|
|
client_wants_body: bool,
|
|
|
|
|
range: range::ValidParsed,
|
|
|
|
|
if_none_match: Option <&Vec <u8>>,
|
|
|
|
|
)
|
|
|
|
|
-> Result <Response, FileServerError>
|
|
|
|
|
{
|
|
|
|
|
// Tripping the etag through UTF-8 isn't the best way to encourage it to
|
|
|
|
|
// be valid ASCII, but if I make it binary I might accidentally pass the
|
|
|
|
|
// hash binary as a header, which is not valid.
|
|
|
|
|
|
|
|
|
|
let etag = get_file_etag (&f).await.map (String::into_bytes);
|
|
|
|
|
let client_cache_hit = match &etag {
|
|
|
|
|
None => false,
|
|
|
|
|
Some (actual) => match &if_none_match {
|
|
|
|
|
None => false,
|
|
|
|
|
Some (if_none_match) => &actual == if_none_match,
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let (tx, rx) = channel (1);
|
|
|
|
|
let body = if should_send_body {
|
|
|
|
|
let body = if client_wants_body && ! client_cache_hit {
|
|
|
|
|
Some (rx)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
@ -143,7 +157,7 @@ async fn serve_file (
|
|
|
|
|
let seek = SeekFrom::Start (range.start);
|
|
|
|
|
f.seek (seek).await?;
|
|
|
|
|
|
|
|
|
|
if should_send_body {
|
|
|
|
|
if body.is_some () {
|
|
|
|
|
tokio::spawn (async move {
|
|
|
|
|
let mut bytes_sent = 0;
|
|
|
|
|
let mut bytes_left = content_length;
|
|
|
|
@ -194,7 +208,7 @@ async fn serve_file (
|
|
|
|
|
// The intended semantics I'm using are:
|
|
|
|
|
// - etag - Some random hashed value that changes whenever the metadata
|
|
|
|
|
// (name, inode number, length, mtime) of a file changes. Also changes
|
|
|
|
|
// on new server instance.
|
|
|
|
|
// on new server instance. Maybe.
|
|
|
|
|
// - no-cache - Clients and the relay can store this, but should revalidate
|
|
|
|
|
// with the origin server (us) because only we can check if the file
|
|
|
|
|
// changed on disk.
|
|
|
|
@ -203,7 +217,9 @@ async fn serve_file (
|
|
|
|
|
// consider it stale.
|
|
|
|
|
|
|
|
|
|
response.header ("cache-control".to_string (), b"no-cache,max-age=0".to_vec ());
|
|
|
|
|
response.header ("etag".to_string (), rusty_ulid::generate_ulid_string ().into_bytes ());
|
|
|
|
|
etag.map (|etag| {
|
|
|
|
|
response.header ("etag".to_string (), etag);
|
|
|
|
|
});
|
|
|
|
|
response.header (String::from ("accept-ranges"), b"bytes".to_vec ());
|
|
|
|
|
|
|
|
|
|
if range_requested {
|
|
|
|
@ -215,12 +231,15 @@ async fn serve_file (
|
|
|
|
|
response.header (String::from ("content-length"), range.end.to_string ().into_bytes ());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if should_send_body {
|
|
|
|
|
response.content_length = Some (content_length);
|
|
|
|
|
if client_cache_hit {
|
|
|
|
|
response.status_code (StatusCode::NotModified);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
else if ! client_wants_body {
|
|
|
|
|
response.status_code (StatusCode::NoContent);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
response.content_length = Some (content_length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some (body) = body {
|
|
|
|
|
response.body (body);
|
|
|
|
@ -229,6 +248,12 @@ async fn serve_file (
|
|
|
|
|
Ok (response)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_file_etag (f: &File) -> Option <String>
|
|
|
|
|
{
|
|
|
|
|
let md = f.metadata ().await;
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass a request to the internal decision-making logic.
|
|
|
|
|
// When it returns, prettify it as HTML or JSON based on what the client
|
|
|
|
|
// asked for.
|
|
|
|
@ -293,7 +318,7 @@ pub async fn serve_all (
|
|
|
|
|
file,
|
|
|
|
|
send_body,
|
|
|
|
|
range,
|
|
|
|
|
}) => serve_file (file.into_inner (), send_body, range).await?,
|
|
|
|
|
}) => serve_file (file.into_inner (), send_body, range, headers.get ("if-none-match")).await?,
|
|
|
|
|
MarkdownErr (e) => {
|
|
|
|
|
#[cfg (feature = "markdown")]
|
|
|
|
|
{
|
|
|
|
|