From 53cd2a8bd69ba53eea7bbe60412bd52c6f19c2bc Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 16:53:00 -0800 Subject: [PATCH 01/23] utils url paths feel more sturdy --- response/src/range_response.rs | 9 ++++----- response/src/serialize_details.rs | 31 ++++++++++++++++++++----------- response/src/utils_flyweight.rs | 24 ++++++++---------------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 4968737..f82e28a 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -28,16 +28,15 @@ pub async fn build_response( }; // flatten this - if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { - if let Some(ranges) = get_ranges(&range_header) { + if let Some(ranges) = get_ranges(&range_header) { + if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { let encodings = get_encodings(req, &res_params.available_encodings); - if let Some(response) = build_single_range_response(&filepath, &encodings, ranges).await { return Some(response); } - }; - } + } + }; Some(last_resort_response::build_response( StatusCode::NOT_FOUND, diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 3354344..1b06be1 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -41,30 +41,39 @@ pub async fn build_response( req: &Request, res_params: &ResponseParams, ) -> Option> { - // Di this at start of request send down function chain - let params = match req.uri().query() { - Some(prms) => prms, + let params_map = match get_uri_query_map(req) { + Some(pm) => pm, _ => return None, }; - if 0 == params.len() { + let details_as = match params_map.get("details_as") { + Some(srlz) => srlz, + _ => return None, + }; + + // details is only available in json for now + if "json" != details_as { return None; } - let params_map = form_urlencoded::parse(params.as_bytes()) - .into_owned() - .collect::>(); + compose_entry_details_response(req, res_params).await +} - let serialize_as = match params_map.get("details_as") { - Some(srlz) => srlz, +fn get_uri_query_map(req: &Request) -> Option> { + let params = match req.uri().query() { + Some(prms) => prms, _ => return None, }; - if "json" != serialize_as { + if 0 == params.len() { return None; } - compose_entry_details_response(req, res_params).await + Some( + form_urlencoded::parse(params.as_bytes()) + .into_owned() + .collect::>(), + ) } async fn compose_entry_details_response( diff --git a/response/src/utils_flyweight.rs b/response/src/utils_flyweight.rs index f9f9f01..d791458 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/utils_flyweight.rs @@ -37,13 +37,9 @@ pub fn get_url_path_from_request( add_index_on_directory: bool, ) -> Option { let uri_path = req.uri().path(); - let url_path_as_os_path = convert_to_os_path(&uri_path, add_index_on_directory); - + let url_path_as_os_path = convert_url_path_to_os_path(&uri_path, add_index_on_directory); let joined_path = directory.join(url_path_as_os_path); - let normalized = match normalize_path_lexicographically(&joined_path) { - Some(pth) => pth, - _ => return None, - }; + let normalized = normalize_path_lexicographically(&joined_path); match normalized.starts_with(directory) { true => Some(normalized), @@ -51,11 +47,9 @@ pub fn get_url_path_from_request( } } -fn convert_to_os_path(path_buf_str: &str, add_index_on_directory: bool) -> PathBuf { +fn convert_url_path_to_os_path(path_buf_str: &str, add_index_on_directory: bool) -> PathBuf { let mut normalized_uri_path = PathBuf::new(); - - let splitted = path_buf_str.split("/"); - for split in splitted { + for split in path_buf_str.split("/") { match split { "" => {} "." => normalized_uri_path.push(Component::CurDir), @@ -72,16 +66,14 @@ fn convert_to_os_path(path_buf_str: &str, add_index_on_directory: bool) -> PathB } // return with a path or an error -fn normalize_path_lexicographically(path_buf: &Path) -> Option { +fn normalize_path_lexicographically(path_buf: &Path) -> PathBuf { let mut normalized_stack: Vec = Vec::new(); for part in path_buf.components() { match part { + Component::CurDir => {} Component::ParentDir => { - if let None = normalized_stack.pop() { - return None; - } + normalized_stack.pop(); } - Component::CurDir => {} _ => normalized_stack.push(part), } } @@ -91,5 +83,5 @@ fn normalize_path_lexicographically(path_buf: &Path) -> Option { normalized.push(part); } - Some(normalized) + normalized } From 35a6027dacb96716821bb63620e0bb49e3be0c31 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 18:41:37 -0800 Subject: [PATCH 02/23] railway logic for serialize_details --- response/src/serialize_details.rs | 56 ++++++++++++++++++------------- response/src/utils_flyweight.rs | 1 - 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 1b06be1..326681f 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -51,12 +51,17 @@ pub async fn build_response( _ => return None, }; - // details is only available in json for now - if "json" != details_as { - return None; + // After this point, this is a serialize details as + if "json" == details_as { + if let Some(res) = compose_entry_details_response(req, res_params).await { + return Some(res); + } } - compose_entry_details_response(req, res_params).await + Some(last_resort_response::build_response( + StatusCode::NOT_FOUND, + NOT_FOUND_404, + )) } fn get_uri_query_map(req: &Request) -> Option> { @@ -90,7 +95,7 @@ async fn compose_entry_details_response( _ => return None, }; - // immediately drop symlink, feels dangerous + // Drop symlink, feels dangerous if metadata.is_symlink() { return None; } @@ -109,17 +114,19 @@ async fn compose_entry_details_response( } } +// This should be optional >>>???? fn build_file_entry( metadata: &Metadata, req_path: &Path, base_path: &Path, ) -> Option { - let details = create_entry_details(metadata, req_path, base_path); - - Some(EntryDetails { - details, - entries: Vec::new(), - }) + match create_entry_details(metadata, req_path, base_path) { + Some(details) => Some(EntryDetails { + details, + entries: Vec::new(), + }), + _ => None, + } } async fn build_directory_entry( @@ -127,7 +134,10 @@ async fn build_directory_entry( req_path: &Path, base_path: &Path, ) -> Option { - let details = create_entry_details(metadata, req_path, base_path); + let details = match create_entry_details(metadata, req_path, base_path) { + Some(deets) => deets, + _ => return None, + }; let mut entry_details = EntryDetails { details, @@ -146,9 +156,9 @@ async fn build_directory_entry( _ => continue, }; - entry_details - .entries - .push(create_entry_details(&metadata, &entry.path(), base_path)); + if let Some(details) = create_entry_details(&metadata, &entry.path(), base_path) { + entry_details.entries.push(details); + } continue; } @@ -159,25 +169,26 @@ async fn build_directory_entry( Some(entry_details) } -fn create_entry_details(metadata: &Metadata, req_path: &Path, base_path: &Path) -> FileDetails { +fn create_entry_details( + metadata: &Metadata, + req_path: &Path, + base_path: &Path, +) -> Option { let file_name = match req_path.file_name() { Some(flnm) => flnm.to_string_lossy().to_string(), None => "[unable to parse filename]".to_string(), }; - // strip path from base path - // make sure url_path starts with / and ends with / if a directory - let url_path = match get_url_path_from_filepath(metadata, base_path, req_path) { Some(p) => p, - _ => "".to_string(), + _ => return None, }; let created_at = get_duration_since_as_ms(metadata.created()); let accessed_at = get_duration_since_as_ms(metadata.accessed()); let modified_at = get_duration_since_as_ms(metadata.modified()); - FileDetails { + Some(FileDetails { is_dir: metadata.is_dir(), is_file: metadata.is_file(), is_symlink: metadata.is_symlink(), @@ -187,7 +198,7 @@ fn create_entry_details(metadata: &Metadata, req_path: &Path, base_path: &Path) accessed_at, modified_at, size: metadata.len(), - } + }) } async fn compose_response( @@ -225,7 +236,6 @@ fn get_duration_since_as_ms(timestamp_result: Result) -> Op None } -// get url path from path(directory, pathbuf) pub fn get_url_path_from_filepath( metadata: &Metadata, directory: &Path, diff --git a/response/src/utils_flyweight.rs b/response/src/utils_flyweight.rs index d791458..b08c902 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/utils_flyweight.rs @@ -65,7 +65,6 @@ fn convert_url_path_to_os_path(path_buf_str: &str, add_index_on_directory: bool) normalized_uri_path } -// return with a path or an error fn normalize_path_lexicographically(path_buf: &Path) -> PathBuf { let mut normalized_stack: Vec = Vec::new(); for part in path_buf.components() { From d59d9eb1a423656655681c152e0dbcc6ad28d9d5 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 18:57:54 -0800 Subject: [PATCH 03/23] remove async from compose response --- response/src/serialize_details.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 326681f..777d2fd 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -105,13 +105,7 @@ async fn compose_entry_details_response( _ => build_file_entry(&metadata, &uri_path, &res_params.directory), }; - match compose_response(details).await { - Some(res) => Some(res), - _ => Some(last_resort_response::build_response( - StatusCode::NOT_FOUND, - NOT_FOUND_404, - )), - } + compose_response(details) } // This should be optional >>>???? @@ -201,7 +195,7 @@ fn create_entry_details( }) } -async fn compose_response( +fn compose_response( details_opt: Option, ) -> Option> { let details = match details_opt { From e4a30d11f2f82caecbae8cc1c6fd20a5038bb23e Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 19:10:31 -0800 Subject: [PATCH 04/23] u128 int as string --- response/src/serialize_details.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 777d2fd..7256013 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -26,9 +26,9 @@ pub struct FileDetails { file_name: String, url_path: String, size: u64, - created_at: Option, - accessed_at: Option, - modified_at: Option, + created_at: Option, + accessed_at: Option, + modified_at: Option, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -220,10 +220,10 @@ fn compose_response( ) } -fn get_duration_since_as_ms(timestamp_result: Result) -> Option { +fn get_duration_since_as_ms(timestamp_result: Result) -> Option { if let Ok(timestamp) = timestamp_result { if let Ok(duration) = timestamp.duration_since(SystemTime::UNIX_EPOCH) { - return Some(duration.as_millis()); + return Some(duration.as_millis().to_string()); } } From 3aecc36bffbeda99f8f2b8bcca700ebc09663ee0 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 19:26:53 -0800 Subject: [PATCH 05/23] size as string --- response/src/serialize_details.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 7256013..c481545 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -25,7 +25,7 @@ pub struct FileDetails { is_symlink: bool, file_name: String, url_path: String, - size: u64, + size: String, created_at: Option, accessed_at: Option, modified_at: Option, @@ -191,7 +191,7 @@ fn create_entry_details( created_at, accessed_at, modified_at, - size: metadata.len(), + size: metadata.len().to_string(), }) } From e98364910a362f5771f9b5736447f6aecc477c29 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 1 Nov 2026 19:35:27 -0800 Subject: [PATCH 06/23] Json is different structure --- response/Cargo.toml | 2 +- response/src/lib.rs | 2 +- response/src/serialize_details.rs | 23 +++++++++++------------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/response/Cargo.toml b/response/Cargo.toml index 9652f1a..fdc72d6 100644 --- a/response/Cargo.toml +++ b/response/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "response" -version = "0.2.3" +version = "0.2.4" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/response/src/lib.rs b/response/src/lib.rs index 8027b82..5e6b4ea 100644 --- a/response/src/lib.rs +++ b/response/src/lib.rs @@ -9,5 +9,5 @@ mod serialize_details; mod utils_flyweight; pub use crate::responses::compose_response; -pub use crate::serialize_details::{EntryDetails, FileDetails}; +pub use crate::serialize_details::{EntryDetailsJson, FileDetailsJson}; pub use crate::utils_flyweight::{BoxedResponse, ResponseParams, get_url_path_from_request}; diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index c481545..179607d 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -17,9 +17,8 @@ use std::path::Path; use std::time::SystemTime; use tokio::fs; -// Need to jouge up for some clean json stuff, option should be property exists or no not NULL #[derive(Clone, Serialize, Deserialize, Debug)] -pub struct FileDetails { +pub struct FileDetailsJson { is_file: bool, is_dir: bool, is_symlink: bool, @@ -32,9 +31,9 @@ pub struct FileDetails { } #[derive(Clone, Serialize, Deserialize, Debug)] -pub struct EntryDetails { - details: FileDetails, - entries: Vec, +pub struct EntryDetailsJson { + details: FileDetailsJson, + entries: Vec, } pub async fn build_response( @@ -113,9 +112,9 @@ fn build_file_entry( metadata: &Metadata, req_path: &Path, base_path: &Path, -) -> Option { +) -> Option { match create_entry_details(metadata, req_path, base_path) { - Some(details) => Some(EntryDetails { + Some(details) => Some(EntryDetailsJson { details, entries: Vec::new(), }), @@ -127,13 +126,13 @@ async fn build_directory_entry( metadata: &Metadata, req_path: &Path, base_path: &Path, -) -> Option { +) -> Option { let details = match create_entry_details(metadata, req_path, base_path) { Some(deets) => deets, _ => return None, }; - let mut entry_details = EntryDetails { + let mut entry_details = EntryDetailsJson { details, entries: Vec::new(), }; @@ -167,7 +166,7 @@ fn create_entry_details( metadata: &Metadata, req_path: &Path, base_path: &Path, -) -> Option { +) -> Option { let file_name = match req_path.file_name() { Some(flnm) => flnm.to_string_lossy().to_string(), None => "[unable to parse filename]".to_string(), @@ -182,7 +181,7 @@ fn create_entry_details( let accessed_at = get_duration_since_as_ms(metadata.accessed()); let modified_at = get_duration_since_as_ms(metadata.modified()); - Some(FileDetails { + Some(FileDetailsJson { is_dir: metadata.is_dir(), is_file: metadata.is_file(), is_symlink: metadata.is_symlink(), @@ -196,7 +195,7 @@ fn create_entry_details( } fn compose_response( - details_opt: Option, + details_opt: Option, ) -> Option> { let details = match details_opt { Some(deets) => deets, From 6391128710a65aa650e2c894e91fe89768315032 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Mon, 2 Nov 2026 20:36:36 -0800 Subject: [PATCH 07/23] reduce functions in range response --- response/src/get_response.rs | 4 ++-- response/src/range_response.rs | 27 ++++++++------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/response/src/get_response.rs b/response/src/get_response.rs index 76f6f9a..7dae449 100644 --- a/response/src/get_response.rs +++ b/response/src/get_response.rs @@ -32,14 +32,14 @@ pub async fn build_response( } let encodings = get_encodings(&req, &res_params.available_encodings); - if let Some(res) = build_req_path_response(&req, &res_params.directory, &encodings).await { + if let Some(res) = build_response_from_url_path(&req, &res_params.directory, &encodings).await { return res; }; last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404) } -async fn build_req_path_response( +async fn build_response_from_url_path( req: &Request, directory: &Path, encodings: &Vec, diff --git a/response/src/range_response.rs b/response/src/range_response.rs index f82e28a..9e3850e 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -31,7 +31,8 @@ pub async fn build_response( if let Some(ranges) = get_ranges(&range_header) { if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { let encodings = get_encodings(req, &res_params.available_encodings); - if let Some(response) = build_single_range_response(&filepath, &encodings, ranges).await + if let Some(response) = + build_single_range_response(&filepath, &encodings, &ranges).await { return Some(response); } @@ -50,6 +51,8 @@ fn get_range_header(req: &Request) -> Option { _ => return None, }; + // put "get ranges here" + match accept_encoding_header.to_str() { Ok(s) => Some(s.to_string()), _ => None, @@ -140,26 +143,11 @@ fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> async fn build_single_range_response( filepath: &Path, encodings: &Vec, - ranges: Vec<(Option, Option)>, + ranges: &Vec<(Option, Option)>, ) -> Option> { let content_type = get_content_type(&filepath); - if let Some(res) = - compose_encoded_single_range_response(&filepath, content_type, &encodings, &ranges).await - { - return Some(res); - }; - - // origin target - compose_single_range_response(&filepath, content_type, None, &ranges).await -} - -async fn compose_encoded_single_range_response( - filepath: &Path, - content_type: &str, - encodings: &Vec, - ranges: &Vec<(Option, Option)>, -) -> Option> { + // JUST PUT COMPOSE ENCODED RESPONSE HERE for enc in encodings { let encoded_path = match add_extension(filepath, &enc) { Some(enc_pth) => enc_pth, @@ -173,7 +161,8 @@ async fn compose_encoded_single_range_response( } } - None + // origin target + compose_single_range_response(&filepath, content_type, None, ranges).await } async fn compose_single_range_response( From f87a8695a0592686afacb35138118966414c7ae3 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Wed, 4 Nov 2026 14:19:02 -0800 Subject: [PATCH 08/23] setup ranges for a multipart bytes range --- response/src/range_response.rs | 94 ++++++++++++++++--------------- response/src/serialize_details.rs | 33 +++++++++++ 2 files changed, 83 insertions(+), 44 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 9e3850e..b6f127c 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -22,21 +22,33 @@ pub async fn build_response( req: &Request, res_params: &ResponseParams, ) -> Option> { - let range_header = match get_range_header(req) { + let ranges = match get_ranges(req) { Some(rh) => rh, _ => return None, }; - // flatten this - if let Some(ranges) = get_ranges(&range_header) { - if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { - let encodings = get_encodings(req, &res_params.available_encodings); - if let Some(response) = - build_single_range_response(&filepath, &encodings, &ranges).await + if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { + let content_type = get_content_type(&filepath); + let encodings = get_encodings(req, &res_params.available_encodings); + + // try encoded + for enc in encodings { + let encoded_path = match add_extension(&filepath, &enc) { + Some(enc_pth) => enc_pth, + _ => continue, + }; + + if let Some(res) = + compose_range_response(&encoded_path, content_type, Some(&enc), &ranges).await { - return Some(response); + return Some(res); } } + + // origin target + if let Some(res) = compose_range_response(&filepath, content_type, None, &ranges).await { + return Some(res); + } }; Some(last_resort_response::build_response( @@ -45,23 +57,19 @@ pub async fn build_response( )) } -fn get_range_header(req: &Request) -> Option { +// on any fail return nothing +fn get_ranges(req: &Request) -> Option, Option)>> { let accept_encoding_header = match req.headers().get(RANGE) { Some(enc) => enc, _ => return None, }; - // put "get ranges here" - - match accept_encoding_header.to_str() { - Ok(s) => Some(s.to_string()), - _ => None, - } -} + let header_str = match accept_encoding_header.to_str() { + Ok(hstr) => hstr, + _ => return None, + }; -// on any fail return nothing -fn get_ranges(range_header_value: &str) -> Option, Option)>> { - let ranges_str = match range_header_value.trim().strip_prefix("bytes=") { + let ranges_str = match header_str.trim().strip_prefix("bytes=") { Some(r) => r, _ => return None, }; @@ -140,29 +148,24 @@ fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> None } -async fn build_single_range_response( +async fn compose_range_response( filepath: &Path, - encodings: &Vec, + content_type: &str, + content_encoding: Option<&str>, ranges: &Vec<(Option, Option)>, ) -> Option> { - let content_type = get_content_type(&filepath); - - // JUST PUT COMPOSE ENCODED RESPONSE HERE - for enc in encodings { - let encoded_path = match add_extension(filepath, &enc) { - Some(enc_pth) => enc_pth, - _ => continue, - }; - - if let Some(res) = - compose_single_range_response(&encoded_path, content_type, Some(enc), ranges).await - { - return Some(res); - } + // vs multi range (stretch goal) + match ranges.len() { + 0 => Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )), + 1 => compose_single_range_response(filepath, content_type, content_encoding, &ranges).await, + _ => Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )), } - - // origin target - compose_single_range_response(&filepath, content_type, None, ranges).await } async fn compose_single_range_response( @@ -185,8 +188,10 @@ async fn compose_single_range_response( return None; } + // split here for single range response let size = metadata.len() as usize; - let (start, end) = match get_start_and_end(ranges, size) { + let range = ranges.get(0); + let (start, end) = match get_start_and_end(range, size) { Some(se) => se, _ => { return Some(last_resort_response::build_response( @@ -200,16 +205,17 @@ async fn compose_single_range_response( return None; }; - let mut buffer: Vec = Vec::with_capacity(end - start); - buffer.resize(end - start, 0); + // let mut buffer: Vec = Vec::with_capacity(end - start); + // buffer.resize(end - start, 0); let content_range_header = build_content_range_header_str(start, end, size); + let size = end - start; let mut builder = Response::builder() .status(StatusCode::PARTIAL_CONTENT) .header(CONTENT_TYPE, content_type) .header(CONTENT_RANGE, content_range_header) - .header(CONTENT_LENGTH, buffer.len().to_string()); + .header(CONTENT_LENGTH, size); if let Some(enc) = content_encoding { builder = builder.header(CONTENT_ENCODING, enc); @@ -223,10 +229,10 @@ async fn compose_single_range_response( } fn get_start_and_end( - ranges: &Vec<(Option, Option)>, + range: Option<&(Option, Option)>, size: usize, ) -> Option<(usize, usize)> { - let (start, end) = match ranges.get(0) { + let (start, end) = match range { // suffix (S - N, S) Some((None, Some(end))) => (size - end, size), // prefix (N, S) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 179607d..c7750b8 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -17,6 +17,21 @@ use std::path::Path; use std::time::SystemTime; use tokio::fs; +// There are "safe" numbers in JSON (strings) +// FileDetails should preserve that + +pub struct FileDetails { + is_file: bool, + is_dir: bool, + is_symlink: bool, + file_name: String, + url_path: String, + size: u64, + created_at: Option, + accessed_at: Option, + modified_at: Option, +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct FileDetailsJson { is_file: bool, @@ -30,6 +45,24 @@ pub struct FileDetailsJson { modified_at: Option, } +impl FileDetailsJson { + fn as_file_details(&self) -> FileDetails { + // create some "to int from string" logic here + + FileDetails { + is_file: false, + is_dir: false, + is_symlink: false, + file_name: "hai".to_string(), + url_path: "hai".to_string(), + size: 1, + created_at: Some(2), + accessed_at: Some(3), + modified_at: Some(4), + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct EntryDetailsJson { details: FileDetailsJson, From cd257fc89f1b162a0a0c72b3afcf1e06a08708bf Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Wed, 4 Nov 2026 16:07:47 -0800 Subject: [PATCH 09/23] head response, get metadata first then get response --- response/src/head_response.rs | 72 +++++++++++++++++++-------------- response/src/range_response.rs | 6 +-- response/src/utils_flyweight.rs | 25 +++++++++++- 3 files changed, 68 insertions(+), 35 deletions(-) diff --git a/response/src/head_response.rs b/response/src/head_response.rs index 81056f3..8e3a566 100644 --- a/response/src/head_response.rs +++ b/response/src/head_response.rs @@ -9,6 +9,7 @@ use hyper::StatusCode; use hyper::body::Incoming; use hyper::header::{ACCEPT_RANGES, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::http::{Request, Response}; +use std::fs::Metadata; use std::path::Path; use tokio::fs; @@ -16,46 +17,57 @@ pub async fn build_response( req: Request, res_params: ResponseParams, ) -> Result { - if let Some(filepath) = get_url_path_from_request(&req, &res_params.directory, true) { - let content_type = get_content_type(&filepath); - let encodings = get_encodings(&req, &res_params.available_encodings); - - // try encoded - for encoding in encodings { - let encoded_path = match add_extension(&filepath, &encoding) { - Some(enc_pth) => enc_pth, - _ => continue, - }; + let filepath = match get_url_path_from_request(&req, &res_params.directory, true) { + Some(fp) => fp, + _ => return last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404), + }; - if let Some(res) = compose_response(&encoded_path, content_type, Some(&encoding)).await - { - return res; - } - } + let content_type = get_content_type(&filepath); + let encodings = get_encodings(&req, &res_params.available_encodings); - // try base - if let Some(res) = compose_response(&filepath, content_type, None).await { - return res; - } + if let Some((metadata, encoding_opt)) = get_metadata_and_encoding(&filepath, encodings).await { + return compose_response(&metadata, encoding_opt, content_type).await; }; last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404) } -async fn compose_response( +async fn get_metadata_and_encoding( filepath: &Path, - content_type: &str, - content_encoding: Option<&str>, -) -> Option> { + encodings: Vec, +) -> Option<(Metadata, Option)> { + for encoding in encodings { + let encoded_path = match add_extension(&filepath, &encoding) { + Some(enc_pth) => enc_pth, + _ => continue, + }; + + let metadata = match fs::metadata(encoded_path).await { + Ok(m) => m, + _ => continue, + }; + + if metadata.is_file() { + return Some((metadata, Some(encoding))); + } + } + let metadata = match fs::metadata(filepath).await { Ok(m) => m, _ => return None, }; - if !metadata.is_file() { - return None; + match metadata.is_file() { + true => Some((metadata, None)), + _ => None, } +} +async fn compose_response( + metadata: &Metadata, + content_encoding: Option, + content_type: &str, +) -> Result { let mut builder = Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, content_type) @@ -66,11 +78,9 @@ async fn compose_response( builder = builder.header(CONTENT_ENCODING, enc); } - Some( - builder.body( - Full::new(bytes::Bytes::new()) - .map_err(|e| match e {}) - .boxed(), - ), + builder.body( + Full::new(bytes::Bytes::new()) + .map_err(|e| match e {}) + .boxed(), ) } diff --git a/response/src/range_response.rs b/response/src/range_response.rs index b6f127c..5fb59c3 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -29,9 +29,8 @@ pub async fn build_response( if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { let content_type = get_content_type(&filepath); - let encodings = get_encodings(req, &res_params.available_encodings); - // try encoded + let encodings = get_encodings(req, &res_params.available_encodings); for enc in encodings { let encoded_path = match add_extension(&filepath, &enc) { Some(enc_pth) => enc_pth, @@ -58,6 +57,7 @@ pub async fn build_response( } // on any fail return nothing +// split stuff up, save the "verify" on usage fn get_ranges(req: &Request) -> Option, Option)>> { let accept_encoding_header = match req.headers().get(RANGE) { Some(enc) => enc, @@ -154,13 +154,13 @@ async fn compose_range_response( content_encoding: Option<&str>, ranges: &Vec<(Option, Option)>, ) -> Option> { - // vs multi range (stretch goal) match ranges.len() { 0 => Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE_416, )), 1 => compose_single_range_response(filepath, content_type, content_encoding, &ranges).await, + // vs multi range (stretch goal) _ => Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE_416, diff --git a/response/src/utils_flyweight.rs b/response/src/utils_flyweight.rs index b08c902..44b42a5 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/utils_flyweight.rs @@ -1,9 +1,10 @@ -use crate::available_encodings::AvailableEncodings; +use crate::available_encodings::{AvailableEncodings, add_extension}; use bytes::Bytes; use http_body_util::combinators::BoxBody; use hyper::body::Incoming; use hyper::http::{Request, Response}; use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf}; +use tokio::fs::File; use tokio::io; pub type BoxedResponse = Response>; @@ -84,3 +85,25 @@ fn normalize_path_lexicographically(path_buf: &Path) -> PathBuf { normalized } + +pub async fn get_file_and_encoding( + filepath: &Path, + encodings: Vec, +) -> Option<(File, Option)> { + for encoding in encodings { + let encoded_path = match add_extension(&filepath, &encoding) { + Some(enc_pth) => enc_pth, + _ => continue, + }; + + let file = match File::open(encoded_path).await { + Ok(f) => return Some((f, Some(encoding))), + _ => continue, + }; + } + + match File::open(filepath).await { + Ok(f) => return Some((f, None)), + _ => return None, + } +} From a711cf09ba8b3148c9b379922688d21dda29733f Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Wed, 4 Nov 2026 16:28:36 -0800 Subject: [PATCH 10/23] split responsibilities in get request --- response/src/content_type.rs | 2 ++ response/src/get_response.rs | 33 +++++++++++---------------------- response/src/utils_flyweight.rs | 8 ++++---- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/response/src/content_type.rs b/response/src/content_type.rs index 38b7fcd..de91ab4 100644 --- a/response/src/content_type.rs +++ b/response/src/content_type.rs @@ -25,11 +25,13 @@ pub fn get_content_type(target_path: &Path) -> &str { "gz" => "application/gzip", "html" => HTML, "ico" => "image/vnd.microsoft.icon", + "ics" => "text/calendar", "jpeg" => "image/jpeg", "jpg" => "image/jpeg", "js" => "text/javascript; charset=utf-8", "json" => "application/json; charset=utf-8", "webmanifest" => "application/manifest+json", + "md" => "text/markdown", "m3u8" => "application/x-mpegURL", "m3u" => "application/x-mpegURL", "midi" => "audio/midi", diff --git a/response/src/get_response.rs b/response/src/get_response.rs index 7dae449..d27ec95 100644 --- a/response/src/get_response.rs +++ b/response/src/get_response.rs @@ -1,10 +1,10 @@ -use crate::available_encodings::{add_extension, get_encodings}; +use crate::available_encodings::get_encodings; use crate::content_type::get_content_type; use crate::last_resort_response; use crate::range_response; use crate::serialize_details; use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, ResponseParams, get_url_path_from_request, + BoxedResponse, NOT_FOUND_404, ResponseParams, get_file_and_encoding, get_url_path_from_request, }; use futures_util::TryStreamExt; use http_body_util::{BodyExt, StreamBody}; @@ -13,7 +13,7 @@ use hyper::body::{Frame, Incoming}; use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::http::{Request, Response}; use std::path::Path; -use tokio::fs; +use tokio::fs::File; use tokio_util::io::ReaderStream; pub async fn build_response( @@ -21,7 +21,6 @@ pub async fn build_response( res_params: ResponseParams, ) -> Result { // check for a json details request - // should be on file if let Some(res) = serialize_details::build_response(&req, &res_params).await { return res; }; @@ -31,6 +30,7 @@ pub async fn build_response( return res; } + // a humble get request let encodings = get_encodings(&req, &res_params.available_encodings); if let Some(res) = build_response_from_url_path(&req, &res_params.directory, &encodings).await { return res; @@ -50,30 +50,19 @@ async fn build_response_from_url_path( }; let content_type = get_content_type(&filepath); - for enc in encodings { - let encoded_path = match add_extension(&filepath, enc) { - Some(enc_pth) => enc_pth, - _ => continue, - }; - - if let Some(res) = compose_response(&encoded_path, content_type, Some(enc)).await { - return Some(res); - } - } + let (file, encoding_opt) = match get_file_and_encoding(&filepath, encodings).await { + Some((f, e)) => (f, e), + _ => return None, + }; - compose_response(&filepath, content_type, None).await + compose_response(file, content_type, encoding_opt).await } async fn compose_response( - filepath: &Path, + file: File, content_type: &str, - content_encoding: Option<&str>, + content_encoding: Option, ) -> Option> { - let file = match fs::File::open(filepath).await { - Ok(m) => m, - _ => return None, - }; - let metadata = match file.metadata().await { Ok(m) => m, _ => return None, diff --git a/response/src/utils_flyweight.rs b/response/src/utils_flyweight.rs index 44b42a5..e61f5df 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/utils_flyweight.rs @@ -88,7 +88,7 @@ fn normalize_path_lexicographically(path_buf: &Path) -> PathBuf { pub async fn get_file_and_encoding( filepath: &Path, - encodings: Vec, + encodings: &Vec, ) -> Option<(File, Option)> { for encoding in encodings { let encoded_path = match add_extension(&filepath, &encoding) { @@ -96,10 +96,10 @@ pub async fn get_file_and_encoding( _ => continue, }; - let file = match File::open(encoded_path).await { - Ok(f) => return Some((f, Some(encoding))), + match File::open(encoded_path).await { + Ok(f) => return Some((f, Some(encoding.clone()))), _ => continue, - }; + } } match File::open(filepath).await { From 8b062735c3cfd800dbff524688b2eac09ed5d942 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Wed, 4 Nov 2026 16:59:26 -0800 Subject: [PATCH 11/23] range response just a little more direct --- response/src/range_response.rs | 77 ++++++++++++++++------------------ 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 5fb59c3..635a0e1 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -1,8 +1,8 @@ -use crate::available_encodings::{add_extension, get_encodings}; +use crate::available_encodings::get_encodings; use crate::content_type::get_content_type; use crate::last_resort_response; use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, RANGE_NOT_SATISFIABLE_416, ResponseParams, + BoxedResponse, NOT_FOUND_404, RANGE_NOT_SATISFIABLE_416, ResponseParams, get_file_and_encoding, get_url_path_from_request, }; use futures_util::TryStreamExt; @@ -12,8 +12,6 @@ use hyper::body::Incoming as IncomingBody; use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, RANGE}; use hyper::http::{Request, Response, StatusCode}; use std::io::SeekFrom; -use std::path::Path; -use tokio::fs; use tokio::fs::File; use tokio::io::AsyncSeekExt; use tokio_util::io::ReaderStream; @@ -27,33 +25,33 @@ pub async fn build_response( _ => return None, }; - if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { - let content_type = get_content_type(&filepath); - - let encodings = get_encodings(req, &res_params.available_encodings); - for enc in encodings { - let encoded_path = match add_extension(&filepath, &enc) { - Some(enc_pth) => enc_pth, - _ => continue, - }; + match attempt_range_response(req, res_params, &ranges).await { + Some(res) => Some(res), + _ => Some(last_resort_response::build_response( + StatusCode::NOT_FOUND, + NOT_FOUND_404, + )), + } +} - if let Some(res) = - compose_range_response(&encoded_path, content_type, Some(&enc), &ranges).await - { - return Some(res); - } - } +async fn attempt_range_response( + req: &Request, + res_params: &ResponseParams, + ranges: &Vec<(Option, Option)>, +) -> Option> { + let filepath = match get_url_path_from_request(req, &res_params.directory, true) { + Some(fp) => fp, + _ => return None, + }; - // origin target - if let Some(res) = compose_range_response(&filepath, content_type, None, &ranges).await { - return Some(res); - } + let content_type = get_content_type(&filepath); + let encodings = get_encodings(&req, &res_params.available_encodings); + let (file, encoding_opt) = match get_file_and_encoding(&filepath, &encodings).await { + Some((f, e)) => (f, e), + _ => return None, }; - Some(last_resort_response::build_response( - StatusCode::NOT_FOUND, - NOT_FOUND_404, - )) + compose_range_response(file, content_type, encoding_opt, &ranges).await } // on any fail return nothing @@ -149,9 +147,9 @@ fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> } async fn compose_range_response( - filepath: &Path, + file: File, content_type: &str, - content_encoding: Option<&str>, + content_encoding: Option, ranges: &Vec<(Option, Option)>, ) -> Option> { match ranges.len() { @@ -159,7 +157,7 @@ async fn compose_range_response( StatusCode::RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE_416, )), - 1 => compose_single_range_response(filepath, content_type, content_encoding, &ranges).await, + 1 => compose_single_range_response(file, content_type, content_encoding, &ranges).await, // vs multi range (stretch goal) _ => Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, @@ -169,17 +167,12 @@ async fn compose_range_response( } async fn compose_single_range_response( - filepath: &Path, + mut file: File, content_type: &str, - content_encoding: Option<&str>, + content_encoding: Option, ranges: &Vec<(Option, Option)>, ) -> Option> { - let mut file = match File::open(filepath).await { - Ok(m) => m, - _ => return None, - }; - - let metadata = match fs::metadata(filepath).await { + let metadata = match file.metadata().await { Ok(m) => m, _ => return None, }; @@ -205,9 +198,6 @@ async fn compose_single_range_response( return None; }; - // let mut buffer: Vec = Vec::with_capacity(end - start); - // buffer.resize(end - start, 0); - let content_range_header = build_content_range_header_str(start, end, size); let size = end - start; @@ -252,3 +242,8 @@ fn get_start_and_end( fn build_content_range_header_str(start: usize, end: usize, size: usize) -> String { "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &size.to_string() } + +// fn compose_multipart_range_request() { +// // let mut buffer: Vec = Vec::with_capacity(end - start); +// // buffer.resize(end - start, 0); +// } From 9976f9e519cb1ca330551ab82b17fc036ab64403 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Thu, 5 Nov 2026 20:40:18 -0800 Subject: [PATCH 12/23] head response responsibilities split --- response/src/head_response.rs | 45 +++--- response/src/range_response.rs | 240 ++++++++++++++++-------------- response/src/serialize_details.rs | 10 +- response/src/utils_flyweight.rs | 2 +- 4 files changed, 154 insertions(+), 143 deletions(-) diff --git a/response/src/head_response.rs b/response/src/head_response.rs index 8e3a566..684cf27 100644 --- a/response/src/head_response.rs +++ b/response/src/head_response.rs @@ -17,62 +17,61 @@ pub async fn build_response( req: Request, res_params: ResponseParams, ) -> Result { - let filepath = match get_url_path_from_request(&req, &res_params.directory, true) { - Some(fp) => fp, - _ => return last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404), - }; - - let content_type = get_content_type(&filepath); - let encodings = get_encodings(&req, &res_params.available_encodings); - - if let Some((metadata, encoding_opt)) = get_metadata_and_encoding(&filepath, encodings).await { - return compose_response(&metadata, encoding_opt, content_type).await; + if let Some(filepath) = get_url_path_from_request(&req, &res_params.directory, true) { + let encodings = get_encodings(&req, &res_params.available_encodings); + if let Some((size, encoding_opt)) = get_size_and_encoding(&filepath, encodings).await { + let content_type = get_content_type(&filepath); + return compose_response(content_type, size, encoding_opt).await; + }; }; last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404) } -async fn get_metadata_and_encoding( +async fn get_size_and_encoding( filepath: &Path, encodings: Vec, -) -> Option<(Metadata, Option)> { +) -> Option<(u64, Option)> { for encoding in encodings { let encoded_path = match add_extension(&filepath, &encoding) { Some(enc_pth) => enc_pth, _ => continue, }; - let metadata = match fs::metadata(encoded_path).await { - Ok(m) => m, - _ => continue, - }; - - if metadata.is_file() { - return Some((metadata, Some(encoding))); + if let Some(size) = get_file_size(&encoded_path).await { + return Some((size, Some(encoding.to_string()))); } } + if let Some(size) = get_file_size(filepath).await { + return Some((size, None)); + } + + None +} + +async fn get_file_size(filepath: &Path) -> Option { let metadata = match fs::metadata(filepath).await { Ok(m) => m, _ => return None, }; match metadata.is_file() { - true => Some((metadata, None)), + true => Some(metadata.len()), _ => None, } } async fn compose_response( - metadata: &Metadata, - content_encoding: Option, content_type: &str, + size: u64, + content_encoding: Option, ) -> Result { let mut builder = Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, content_type) .header(ACCEPT_RANGES, "bytes") - .header(CONTENT_LENGTH, metadata.len()); + .header(CONTENT_LENGTH, size); if let Some(enc) = content_encoding { builder = builder.header(CONTENT_ENCODING, enc); diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 635a0e1..b6c57c1 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -10,22 +10,30 @@ use http_body_util::{BodyExt, StreamBody}; use hyper::body::Frame; use hyper::body::Incoming as IncomingBody; use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, RANGE}; -use hyper::http::{Request, Response, StatusCode}; +use hyper::http::{HeaderValue, Request, Response, StatusCode}; +use std::fs::Metadata; use std::io::SeekFrom; use tokio::fs::File; use tokio::io::AsyncSeekExt; use tokio_util::io::ReaderStream; +struct FileDetails { + metadata: Metadata, + ranges: Vec, + content_type: String, + range_header: HeaderValue, +} + pub async fn build_response( req: &Request, res_params: &ResponseParams, ) -> Option> { - let ranges = match get_ranges(req) { - Some(rh) => rh, + let range_header = match req.headers().get(RANGE) { + Some(enc) => enc, _ => return None, }; - match attempt_range_response(req, res_params, &ranges).await { + match attempt_range_response(req, res_params, range_header).await { Some(res) => Some(res), _ => Some(last_resort_response::build_response( StatusCode::NOT_FOUND, @@ -34,130 +42,59 @@ pub async fn build_response( } } +// Returning None fallsback to 404 async fn attempt_range_response( req: &Request, res_params: &ResponseParams, - ranges: &Vec<(Option, Option)>, + range_header: &HeaderValue, ) -> Option> { + // THIS PART IS DOES THIS FILE EXIST let filepath = match get_url_path_from_request(req, &res_params.directory, true) { Some(fp) => fp, _ => return None, }; - let content_type = get_content_type(&filepath); + // struct with { + // filepath + // content_type + // encoding_opt + // range_header + // metadata + // + // } + let encodings = get_encodings(&req, &res_params.available_encodings); let (file, encoding_opt) = match get_file_and_encoding(&filepath, &encodings).await { Some((f, e)) => (f, e), _ => return None, }; - compose_range_response(file, content_type, encoding_opt, &ranges).await -} - -// on any fail return nothing -// split stuff up, save the "verify" on usage -fn get_ranges(req: &Request) -> Option, Option)>> { - let accept_encoding_header = match req.headers().get(RANGE) { - Some(enc) => enc, - _ => return None, - }; - - let header_str = match accept_encoding_header.to_str() { - Ok(hstr) => hstr, - _ => return None, - }; - - let ranges_str = match header_str.trim().strip_prefix("bytes=") { - Some(r) => r, + let metadata = match file.metadata().await { + Ok(m) => m, _ => return None, }; - let mut ranges: Vec<(Option, Option)> = Vec::new(); - for range_value_str in ranges_str.split(",") { - let range_str = range_value_str.trim(); - - // prefix range - if let Some(suffexless) = range_str.strip_suffix("-") { - let start_range_int: usize = match suffexless.parse() { - Ok(sri) => sri, - _ => return None, - }; - - // add verified ranges here - - ranges.push((Some(start_range_int), None)); - continue; - } - - // suffix-range - if let Some(prefixless) = range_str.strip_prefix("-") { - let end_range_int: usize = match prefixless.parse() { - Ok(sri) => sri, - _ => return None, - }; - - ranges.push((None, Some(end_range_int))); - continue; - } - - // window-range - let start_end_range = match get_window_range(range_str) { - Some(ser) => ser, - _ => return None, - }; - - ranges.push(start_end_range) - } - - if 0 < ranges.len() { - return Some(ranges); + if !metadata.is_file() { + return None; } - None -} - -fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> { - let mut values = range_chunk.split("-"); - - let start_range_str = match values.next() { - Some(start_range) => start_range, - _ => return None, - }; + // END DOLES THIS FILE EXIST - let end_range_str = match values.next() { - Some(end_range) => end_range, - _ => return None, - }; - - let start_range_int: usize = match start_range_str.parse() { - Ok(sri) => sri, - _ => return None, - }; - - let end_range_int: usize = match end_range_str.parse() { - Ok(sri) => sri, - _ => return None, - }; - - if start_range_int < end_range_int { - return Some((Some(start_range_int), Some(end_range_int))); - } + // THEN WE ANALYZE RANGES DO RANGES FIT FILE + let ranges = get_ranges(range_header, metadata.size()); + // END ANALYZE RANGES - None -} + // THEN SEND FILE AND RANGES + let content_type = get_content_type(&filepath); -async fn compose_range_response( - file: File, - content_type: &str, - content_encoding: Option, - ranges: &Vec<(Option, Option)>, -) -> Option> { match ranges.len() { 0 => Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, RANGE_NOT_SATISFIABLE_416, )), - 1 => compose_single_range_response(file, content_type, content_encoding, &ranges).await, + 1 => { + compose_single_range_response(file, metadata, content_type, encoding_opt, &ranges).await + } // vs multi range (stretch goal) _ => Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, @@ -166,21 +103,35 @@ async fn compose_range_response( } } +// on any fail return nothing +// split stuff up, save the "verify" on usage +fn get_ranges(range_header: &HeaderValue, size: u64) -> Vec { + let header_str = match range_header.to_str() { + Ok(hstr) => hstr, + _ => return ranges, + }; + + let mut ranges: Vec = Vec::new(); + let ranges_str = match header_str.trim().strip_prefix("bytes=") { + Some(r) => r, + _ => return ranges, + }; + + for range in ranges_str.split(",") { + // check ranges piece by piece + ranges.push(range.to_string()); + } + + ranges +} + async fn compose_single_range_response( mut file: File, + metadata: Metadata, content_type: &str, content_encoding: Option, - ranges: &Vec<(Option, Option)>, + ranges: &Vec, ) -> Option> { - let metadata = match file.metadata().await { - Ok(m) => m, - _ => return None, - }; - - if !metadata.is_file() { - return None; - } - // split here for single range response let size = metadata.len() as usize; let range = ranges.get(0); @@ -218,10 +169,9 @@ async fn compose_single_range_response( Some(builder.body(boxed_body)) } -fn get_start_and_end( - range: Option<&(Option, Option)>, - size: usize, -) -> Option<(usize, usize)> { +fn get_start_and_end(range: Option<&String>, size: usize) -> Option<(usize, usize)> { + // parse the string + let (start, end) = match range { // suffix (S - N, S) Some((None, Some(end))) => (size - end, size), @@ -239,6 +189,36 @@ fn get_start_and_end( None } +fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> { + let mut values = range_chunk.split("-"); + + let start_range_str = match values.next() { + Some(start_range) => start_range, + _ => return None, + }; + + let end_range_str = match values.next() { + Some(end_range) => end_range, + _ => return None, + }; + + let start_range_int: usize = match start_range_str.parse() { + Ok(sri) => sri, + _ => return None, + }; + + let end_range_int: usize = match end_range_str.parse() { + Ok(sri) => sri, + _ => return None, + }; + + if start_range_int < end_range_int { + return Some((Some(start_range_int), Some(end_range_int))); + } + + None +} + fn build_content_range_header_str(start: usize, end: usize, size: usize) -> String { "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &size.to_string() } @@ -247,3 +227,35 @@ fn build_content_range_header_str(start: usize, end: usize, size: usize) -> Stri // // let mut buffer: Vec = Vec::with_capacity(end - start); // // buffer.resize(end - start, 0); // } + +// // prefix range +// if let Some(suffexless) = range_str.strip_suffix("-") { +// let start_range_int: usize = match suffexless.parse() { +// Ok(sri) => sri, +// _ => return None, +// }; + +// // add verified ranges here + +// ranges.push((Some(start_range_int), None)); +// continue; +// } + +// // suffix-range +// if let Some(prefixless) = range_str.strip_prefix("-") { +// let end_range_int: usize = match prefixless.parse() { +// Ok(sri) => sri, +// _ => return None, +// }; + +// ranges.push((None, Some(end_range_int))); +// continue; +// } + +// // window-range +// let start_end_range = match get_window_range(range_str) { +// Some(ser) => ser, +// _ => return None, +// }; + +// ranges.push(start_end_range) diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index c7750b8..55798fc 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -50,11 +50,11 @@ impl FileDetailsJson { // create some "to int from string" logic here FileDetails { - is_file: false, - is_dir: false, - is_symlink: false, - file_name: "hai".to_string(), - url_path: "hai".to_string(), + is_file: self.is_file, + is_dir: self.is_dir, + is_symlink: self.is_symlink, + file_name: self.file_name.clone(), + url_path: self.url_path.clone(), size: 1, created_at: Some(2), accessed_at: Some(3), diff --git a/response/src/utils_flyweight.rs b/response/src/utils_flyweight.rs index e61f5df..f2cbba8 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/utils_flyweight.rs @@ -99,7 +99,7 @@ pub async fn get_file_and_encoding( match File::open(encoded_path).await { Ok(f) => return Some((f, Some(encoding.clone()))), _ => continue, - } + }; } match File::open(filepath).await { From ddd57c4fe3eb85e411a44c3b098bd2af97eac4cc Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Fri, 6 Nov 2026 19:49:27 -0800 Subject: [PATCH 13/23] range results builds again --- response/src/get_response.rs | 2 +- response/src/head_response.rs | 10 +- response/src/range_response.rs | 195 +++++++++++++-------------------- 3 files changed, 82 insertions(+), 125 deletions(-) diff --git a/response/src/get_response.rs b/response/src/get_response.rs index d27ec95..e430273 100644 --- a/response/src/get_response.rs +++ b/response/src/get_response.rs @@ -49,12 +49,12 @@ async fn build_response_from_url_path( _ => return None, }; - let content_type = get_content_type(&filepath); let (file, encoding_opt) = match get_file_and_encoding(&filepath, encodings).await { Some((f, e)) => (f, e), _ => return None, }; + let content_type = get_content_type(&filepath); compose_response(file, content_type, encoding_opt).await } diff --git a/response/src/head_response.rs b/response/src/head_response.rs index 684cf27..8ed6874 100644 --- a/response/src/head_response.rs +++ b/response/src/head_response.rs @@ -9,7 +9,6 @@ use hyper::StatusCode; use hyper::body::Incoming; use hyper::header::{ACCEPT_RANGES, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::http::{Request, Response}; -use std::fs::Metadata; use std::path::Path; use tokio::fs; @@ -28,6 +27,8 @@ pub async fn build_response( last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404) } +// just use "file" and grab metadata instead of more code? + async fn get_size_and_encoding( filepath: &Path, encodings: Vec, @@ -43,11 +44,10 @@ async fn get_size_and_encoding( } } - if let Some(size) = get_file_size(filepath).await { - return Some((size, None)); + match get_file_size(filepath).await { + Some(size) => Some((size, None)), + _ => None, } - - None } async fn get_file_size(filepath: &Path) -> Option { diff --git a/response/src/range_response.rs b/response/src/range_response.rs index b6c57c1..90a5f3e 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -13,17 +13,11 @@ use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYP use hyper::http::{HeaderValue, Request, Response, StatusCode}; use std::fs::Metadata; use std::io::SeekFrom; +use std::num::ParseIntError; use tokio::fs::File; use tokio::io::AsyncSeekExt; use tokio_util::io::ReaderStream; -struct FileDetails { - metadata: Metadata, - ranges: Vec, - content_type: String, - range_header: HeaderValue, -} - pub async fn build_response( req: &Request, res_params: &ResponseParams, @@ -54,15 +48,6 @@ async fn attempt_range_response( _ => return None, }; - // struct with { - // filepath - // content_type - // encoding_opt - // range_header - // metadata - // - // } - let encodings = get_encodings(&req, &res_params.available_encodings); let (file, encoding_opt) = match get_file_and_encoding(&filepath, &encodings).await { Some((f, e)) => (f, e), @@ -81,7 +66,17 @@ async fn attempt_range_response( // END DOLES THIS FILE EXIST // THEN WE ANALYZE RANGES DO RANGES FIT FILE - let ranges = get_ranges(range_header, metadata.size()); + let ranges = match get_ranges(range_header, metadata.len()) { + Some(rngs) => rngs, + _ => { + return Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )); + } + }; + // return RANGE_NOT_SATISFIABLE_416 + // END ANALYZE RANGES // THEN SEND FILE AND RANGES @@ -93,7 +88,7 @@ async fn attempt_range_response( RANGE_NOT_SATISFIABLE_416, )), 1 => { - compose_single_range_response(file, metadata, content_type, encoding_opt, &ranges).await + compose_single_range_response(file, metadata, content_type, encoding_opt, ranges).await } // vs multi range (stretch goal) _ => Some(last_resort_response::build_response( @@ -105,24 +100,74 @@ async fn attempt_range_response( // on any fail return nothing // split stuff up, save the "verify" on usage -fn get_ranges(range_header: &HeaderValue, size: u64) -> Vec { +// vec or error +fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> { let header_str = match range_header.to_str() { Ok(hstr) => hstr, - _ => return ranges, + _ => return None, }; - let mut ranges: Vec = Vec::new(); let ranges_str = match header_str.trim().strip_prefix("bytes=") { Some(r) => r, - _ => return ranges, + _ => return None, }; + let mut ranges: Vec<(u64, u64)> = Vec::new(); for range in ranges_str.split(",") { // check ranges piece by piece - ranges.push(range.to_string()); + match build_window_ranges(range, size) { + Some(r) => ranges.push(r), + _ => return None, + } } - ranges + Some(ranges) +} + +fn build_window_ranges(range_str: &str, size: u64) -> Option<(u64, u64)> { + let values: Vec<&str> = range_str.split("-").collect(); + + if 2 != values.len() { + return None; + } + + let first = values.get(0); + let second = values.get(1); + + let (first_str, second_str) = match (first, second) { + (Some(f), Some(s)) => (f, s), + _ => return None, + }; + + let start_range_int: Result = first_str.parse(); + let end_range_int: Result = second_str.parse(); + + match (start_range_int, end_range_int) { + (Ok(start_int), Ok(mut end_int)) => { + if size < end_int { + end_int = size; + } + + if start_int < end_int { + return Some((start_int, end_int)); + } + + None + } + (Err(_), Ok(mut end_int)) => { + if size < end_int { + end_int = size; + } + Some((0, end_int)) + } + (Ok(mut start_int), Err(_)) => { + if size < start_int { + start_int = size; + } + Some((start_int, size)) + } + _ => None, + } } async fn compose_single_range_response( @@ -130,13 +175,12 @@ async fn compose_single_range_response( metadata: Metadata, content_type: &str, content_encoding: Option, - ranges: &Vec, + ranges: Vec<(u64, u64)>, ) -> Option> { // split here for single range response - let size = metadata.len() as usize; - let range = ranges.get(0); - let (start, end) = match get_start_and_end(range, size) { - Some(se) => se, + let size = metadata.len(); + let (start, end) = match ranges.get(0) { + Some((s, e)) => (s.clone(), e.clone()), _ => { return Some(last_resort_response::build_response( StatusCode::RANGE_NOT_SATISFIABLE, @@ -145,7 +189,7 @@ async fn compose_single_range_response( } }; - if let Err(_err) = file.seek(SeekFrom::Start(start.clone() as u64)).await { + if let Err(_err) = file.seek(SeekFrom::Start(start.clone())).await { return None; }; @@ -162,100 +206,13 @@ async fn compose_single_range_response( builder = builder.header(CONTENT_ENCODING, enc); } - let reader_stream = ReaderStream::with_capacity(file, size); + let reader_stream = ReaderStream::with_capacity(file, size as usize); let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data)); let boxed_body = stream_body.boxed(); Some(builder.body(boxed_body)) } -fn get_start_and_end(range: Option<&String>, size: usize) -> Option<(usize, usize)> { - // parse the string - - let (start, end) = match range { - // suffix (S - N, S) - Some((None, Some(end))) => (size - end, size), - // prefix (N, S) - Some((Some(start), None)) => (start.clone(), size), - // windowed (N, M) - Some((Some(start), Some(end))) => (start.clone(), end.clone()), - _ => return None, - }; - - if start <= end && end <= size { - return Some((start, end)); - } - - None -} - -fn get_window_range(range_chunk: &str) -> Option<(Option, Option)> { - let mut values = range_chunk.split("-"); - - let start_range_str = match values.next() { - Some(start_range) => start_range, - _ => return None, - }; - - let end_range_str = match values.next() { - Some(end_range) => end_range, - _ => return None, - }; - - let start_range_int: usize = match start_range_str.parse() { - Ok(sri) => sri, - _ => return None, - }; - - let end_range_int: usize = match end_range_str.parse() { - Ok(sri) => sri, - _ => return None, - }; - - if start_range_int < end_range_int { - return Some((Some(start_range_int), Some(end_range_int))); - } - - None -} - -fn build_content_range_header_str(start: usize, end: usize, size: usize) -> String { +fn build_content_range_header_str(start: u64, end: u64, size: u64) -> String { "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &size.to_string() } - -// fn compose_multipart_range_request() { -// // let mut buffer: Vec = Vec::with_capacity(end - start); -// // buffer.resize(end - start, 0); -// } - -// // prefix range -// if let Some(suffexless) = range_str.strip_suffix("-") { -// let start_range_int: usize = match suffexless.parse() { -// Ok(sri) => sri, -// _ => return None, -// }; - -// // add verified ranges here - -// ranges.push((Some(start_range_int), None)); -// continue; -// } - -// // suffix-range -// if let Some(prefixless) = range_str.strip_prefix("-") { -// let end_range_int: usize = match prefixless.parse() { -// Ok(sri) => sri, -// _ => return None, -// }; - -// ranges.push((None, Some(end_range_int))); -// continue; -// } - -// // window-range -// let start_end_range = match get_window_range(range_str) { -// Some(ser) => ser, -// _ => return None, -// }; - -// ranges.push(start_end_range) From fe968eb84756c25e38a19977356fcde70892635d Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sat, 7 Nov 2026 01:30:33 -0800 Subject: [PATCH 14/23] up to multirange --- response/src/range_response.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 90a5f3e..3339e18 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -98,9 +98,8 @@ async fn attempt_range_response( } } -// on any fail return nothing -// split stuff up, save the "verify" on usage -// vec or error +// Multipart ranges must be sequential and non overlapping +// otherwise just load the entire file. fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> { let header_str = match range_header.to_str() { Ok(hstr) => hstr, @@ -112,11 +111,18 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> _ => return None, }; + let mut tail = 0; let mut ranges: Vec<(u64, u64)> = Vec::new(); for range in ranges_str.split(",") { - // check ranges piece by piece - match build_window_ranges(range, size) { - Some(r) => ranges.push(r), + match build_window_range(range, size) { + Some((start, end)) => { + if start <= tail { + return None; + } + tail = end; + + ranges.push((start, end)); + }, _ => return None, } } @@ -124,7 +130,7 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> Some(ranges) } -fn build_window_ranges(range_str: &str, size: u64) -> Option<(u64, u64)> { +fn build_window_range(range_str: &str, size: u64) -> Option<(u64, u64)> { let values: Vec<&str> = range_str.split("-").collect(); if 2 != values.len() { From c00ac28bdff0ae1872b77629df730995ecb470f5 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 11:22:58 -0800 Subject: [PATCH 15/23] ranges must be less than 25mb --- response/src/range_response.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 3339e18..a0aceba 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -11,13 +11,14 @@ use hyper::body::Frame; use hyper::body::Incoming as IncomingBody; use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, RANGE}; use hyper::http::{HeaderValue, Request, Response, StatusCode}; -use std::fs::Metadata; use std::io::SeekFrom; use std::num::ParseIntError; use tokio::fs::File; use tokio::io::AsyncSeekExt; use tokio_util::io::ReaderStream; +const MAX_RANGE_SIZE: u64 = 26214400; + pub async fn build_response( req: &Request, res_params: &ResponseParams, @@ -66,6 +67,8 @@ async fn attempt_range_response( // END DOLES THIS FILE EXIST // THEN WE ANALYZE RANGES DO RANGES FIT FILE + // + // Max range request like 25mb let ranges = match get_ranges(range_header, metadata.len()) { Some(rngs) => rngs, _ => { @@ -88,7 +91,8 @@ async fn attempt_range_response( RANGE_NOT_SATISFIABLE_416, )), 1 => { - compose_single_range_response(file, metadata, content_type, encoding_opt, ranges).await + compose_single_range_response(file, metadata.len(), content_type, encoding_opt, ranges) + .await } // vs multi range (stretch goal) _ => Some(last_resort_response::build_response( @@ -100,6 +104,7 @@ async fn attempt_range_response( // Multipart ranges must be sequential and non overlapping // otherwise just load the entire file. +// Should probably keep a "limit" ? fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> { let header_str = match range_header.to_str() { Ok(hstr) => hstr, @@ -111,18 +116,23 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> _ => return None, }; + let mut request_body_size = 0; let mut tail = 0; let mut ranges: Vec<(u64, u64)> = Vec::new(); for range in ranges_str.split(",") { match build_window_range(range, size) { Some((start, end)) => { + request_body_size += end - start; + if MAX_RANGE_SIZE < request_body_size { + return None; + } if start <= tail { return None; } tail = end; ranges.push((start, end)); - }, + } _ => return None, } } @@ -178,13 +188,12 @@ fn build_window_range(range_str: &str, size: u64) -> Option<(u64, u64)> { async fn compose_single_range_response( mut file: File, - metadata: Metadata, + size: u64, content_type: &str, content_encoding: Option, ranges: Vec<(u64, u64)>, ) -> Option> { // split here for single range response - let size = metadata.len(); let (start, end) = match ranges.get(0) { Some((s, e)) => (s.clone(), e.clone()), _ => { From 0ebcffb18762ad4f31fac49b902fdb25bd2d38d4 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 11:55:49 -0800 Subject: [PATCH 16/23] file details as json and back again --- response/src/lib.rs | 2 +- response/src/serialize_details.rs | 99 ++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/response/src/lib.rs b/response/src/lib.rs index 5e6b4ea..3254bc3 100644 --- a/response/src/lib.rs +++ b/response/src/lib.rs @@ -9,5 +9,5 @@ mod serialize_details; mod utils_flyweight; pub use crate::responses::compose_response; -pub use crate::serialize_details::{EntryDetailsJson, FileDetailsJson}; +pub use crate::serialize_details::{EntryDetailsJson, FileDetails, FileDetailsJson}; pub use crate::utils_flyweight::{BoxedResponse, ResponseParams, get_url_path_from_request}; diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 55798fc..6d64f47 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -12,24 +12,52 @@ use serde_json; use std::collections::HashMap; use std::fs::Metadata; use std::io::Error as IoError; +use std::num::ParseIntError; use std::path::Component; use std::path::Path; use std::time::SystemTime; use tokio::fs; -// There are "safe" numbers in JSON (strings) -// FileDetails should preserve that - pub struct FileDetails { - is_file: bool, - is_dir: bool, - is_symlink: bool, - file_name: String, - url_path: String, - size: u64, - created_at: Option, - accessed_at: Option, - modified_at: Option, + pub is_file: bool, + pub is_dir: bool, + pub is_symlink: bool, + pub file_name: String, + pub url_path: String, + pub size: u64, + pub created_at: Option, + pub accessed_at: Option, + pub modified_at: Option, +} + +impl FileDetails { + pub fn json_as_file_details(&self) -> FileDetailsJson { + let size = self.size.to_string(); + let created_at = get_optional_string(&self.created_at); + let accessed_at = get_optional_string(&self.accessed_at); + let modified_at = get_optional_string(&self.modified_at); + + FileDetailsJson { + is_file: self.is_file, + is_dir: self.is_dir, + is_symlink: self.is_symlink, + file_name: self.file_name.clone(), + url_path: self.url_path.clone(), + size, + created_at, + accessed_at, + modified_at, + } + } +} + +fn get_optional_string(opt_num: &Option) -> Option { + let num = match opt_num { + Some(n) => n, + _ => return None, + }; + + Some(num.to_string()) } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -46,23 +74,52 @@ pub struct FileDetailsJson { } impl FileDetailsJson { - fn as_file_details(&self) -> FileDetails { - // create some "to int from string" logic here - - FileDetails { + pub fn as_file_details(&self) -> Result { + let size: u64 = match self.size.parse() { + Ok(num) => num, + Err(e) => return Err(e), + }; + let created_at = match get_optional_number(&self.created_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + let accessed_at = match get_optional_number(&self.accessed_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + let modified_at = match get_optional_number(&self.modified_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + + Ok(FileDetails { is_file: self.is_file, is_dir: self.is_dir, is_symlink: self.is_symlink, file_name: self.file_name.clone(), url_path: self.url_path.clone(), - size: 1, - created_at: Some(2), - accessed_at: Some(3), - modified_at: Some(4), - } + size, + created_at, + accessed_at, + modified_at, + }) } } +fn get_optional_number(num_opt: &Option) -> Result, ParseIntError> { + let num_string = match num_opt { + Some(num_s) => num_s, + _ => return Ok(None), + }; + + let num: u128 = match num_string.parse() { + Ok(n) => n, + Err(e) => return Err(e), + }; + + Ok(Some(num)) +} + #[derive(Clone, Serialize, Deserialize, Debug)] pub struct EntryDetailsJson { details: FileDetailsJson, From eef44b37279703c46e4565c10e2349f03c8a13cd Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 14:05:06 -0800 Subject: [PATCH 17/23] move response_params into their own file to prevent potential circluar dependencies --- README.md | 28 ++++++--- response/src/get_response.rs | 5 +- response/src/head_response.rs | 5 +- response/src/last_resort_response.rs | 2 +- response/src/lib.rs | 6 +- response/src/range_response.rs | 17 +---- response/src/response_params.rs | 19 ++++++ response/src/responses.rs | 3 +- response/src/serialize_details.rs | 47 +++----------- .../{utils_flyweight.rs => url_path_utils.rs} | 19 +----- structures/src/lib.rs | 63 +++++++++++++++++++ 11 files changed, 125 insertions(+), 89 deletions(-) create mode 100644 response/src/response_params.rs rename response/src/{utils_flyweight.rs => url_path_utils.rs} (84%) create mode 100644 structures/src/lib.rs diff --git a/README.md b/README.md index a89e8cb..692df4f 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,9 @@ Which could produce a json object similar to the following: "file_name": "demo", "url_path": "/demo", "size": 128, - "created_at": 1788035132686, - "accessed_at": 1792381141727, - "modified_at": 1792381141619 + "created_at": "1788035132686", + "accessed_at": "1792381141727", + "modified_at": "1792381141619" }, "entries": [ { @@ -140,9 +140,9 @@ Which could produce a json object similar to the following: "file_name": "index.html.zst", "url_path": "/demo/index.html.zst", "size": 304, - "created_at": 1788035132686, - "accessed_at": 1788035156555, - "modified_at": 1788035132686 + "created_at": "1788035132686", + "accessed_at": "1788035156555", + "modified_at": "1788035132686" }, { "is_file": true, @@ -151,14 +151,24 @@ Which could produce a json object similar to the following: "file_name": "index.html", "url_path": "/demo/index.html", "size": 460, - "created_at": 1788035132686, - "accessed_at": 1788035156555, - "modified_at": 1788035132686 + "created_at": "1788035132686", + "accessed_at": "1788035156555", + "modified_at": "1788035132686" } ] } ``` +Json structs can be swapped for a more rust-specific struct: + +```rust +use response::{EntryDetailsJson, FileDetailsJson, FileDetails}; + +let file_details_json: FileDetailsJson = ... + +let file_details: Results = file_details_json::as_file_details(); +``` + ## License `file_server` is released under the BSD 3-Clause License. diff --git a/response/src/get_response.rs b/response/src/get_response.rs index e430273..11110b2 100644 --- a/response/src/get_response.rs +++ b/response/src/get_response.rs @@ -2,9 +2,10 @@ use crate::available_encodings::get_encodings; use crate::content_type::get_content_type; use crate::last_resort_response; use crate::range_response; +use crate::response_params::ResponseParams; use crate::serialize_details; -use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, ResponseParams, get_file_and_encoding, get_url_path_from_request, +use crate::url_path_utils::{ + BoxedResponse, NOT_FOUND_404, get_file_and_encoding, get_url_path_from_request, }; use futures_util::TryStreamExt; use http_body_util::{BodyExt, StreamBody}; diff --git a/response/src/head_response.rs b/response/src/head_response.rs index 8ed6874..65628a6 100644 --- a/response/src/head_response.rs +++ b/response/src/head_response.rs @@ -1,9 +1,8 @@ use crate::available_encodings::{add_extension, get_encodings}; use crate::content_type::get_content_type; use crate::last_resort_response; -use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, ResponseParams, get_url_path_from_request, -}; +use crate::response_params::ResponseParams; +use crate::url_path_utils::{BoxedResponse, NOT_FOUND_404, get_url_path_from_request}; use http_body_util::{BodyExt, Full}; use hyper::StatusCode; use hyper::body::Incoming; diff --git a/response/src/last_resort_response.rs b/response/src/last_resort_response.rs index eaf3abd..62478df 100644 --- a/response/src/last_resort_response.rs +++ b/response/src/last_resort_response.rs @@ -1,5 +1,5 @@ use crate::content_type::HTML; -use crate::utils_flyweight::BoxedResponse; +use crate::url_path_utils::BoxedResponse; use bytes; use http_body_util::{BodyExt, Full}; use hyper::StatusCode; diff --git a/response/src/lib.rs b/response/src/lib.rs index 3254bc3..4570468 100644 --- a/response/src/lib.rs +++ b/response/src/lib.rs @@ -4,10 +4,12 @@ mod get_response; mod head_response; mod last_resort_response; mod range_response; +mod response_params; mod responses; mod serialize_details; -mod utils_flyweight; +mod url_path_utils; +pub use crate::response_params::ResponseParams; pub use crate::responses::compose_response; pub use crate::serialize_details::{EntryDetailsJson, FileDetails, FileDetailsJson}; -pub use crate::utils_flyweight::{BoxedResponse, ResponseParams, get_url_path_from_request}; +pub use crate::url_path_utils::{BoxedResponse, get_url_path_from_request}; diff --git a/response/src/range_response.rs b/response/src/range_response.rs index a0aceba..cf518a2 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -1,8 +1,9 @@ use crate::available_encodings::get_encodings; use crate::content_type::get_content_type; use crate::last_resort_response; -use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, RANGE_NOT_SATISFIABLE_416, ResponseParams, get_file_and_encoding, +use crate::response_params::ResponseParams; +use crate::url_path_utils::{ + BoxedResponse, NOT_FOUND_404, RANGE_NOT_SATISFIABLE_416, get_file_and_encoding, get_url_path_from_request, }; use futures_util::TryStreamExt; @@ -64,11 +65,6 @@ async fn attempt_range_response( return None; } - // END DOLES THIS FILE EXIST - - // THEN WE ANALYZE RANGES DO RANGES FIT FILE - // - // Max range request like 25mb let ranges = match get_ranges(range_header, metadata.len()) { Some(rngs) => rngs, _ => { @@ -78,11 +74,7 @@ async fn attempt_range_response( )); } }; - // return RANGE_NOT_SATISFIABLE_416 - - // END ANALYZE RANGES - // THEN SEND FILE AND RANGES let content_type = get_content_type(&filepath); match ranges.len() { @@ -102,9 +94,6 @@ async fn attempt_range_response( } } -// Multipart ranges must be sequential and non overlapping -// otherwise just load the entire file. -// Should probably keep a "limit" ? fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> { let header_str = match range_header.to_str() { Ok(hstr) => hstr, diff --git a/response/src/response_params.rs b/response/src/response_params.rs new file mode 100644 index 0000000..0050206 --- /dev/null +++ b/response/src/response_params.rs @@ -0,0 +1,19 @@ +use crate::available_encodings::AvailableEncodings; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug)] +pub struct ResponseParams { + pub directory: PathBuf, + pub available_encodings: AvailableEncodings, +} + +impl ResponseParams { + pub fn from(directory: &Path, content_encodings: &Vec) -> ResponseParams { + let available_encodings = AvailableEncodings::from(content_encodings); + + ResponseParams { + directory: directory.to_path_buf(), + available_encodings, + } + } +} diff --git a/response/src/responses.rs b/response/src/responses.rs index 2bdd4c9..b7e36a4 100644 --- a/response/src/responses.rs +++ b/response/src/responses.rs @@ -1,7 +1,8 @@ use crate::get_response; use crate::head_response; use crate::last_resort_response; -use crate::utils_flyweight::{BoxedResponse, METHOD_NOT_ALLOWED_405, ResponseParams}; +use crate::response_params::ResponseParams; +use crate::url_path_utils::{BoxedResponse, METHOD_NOT_ALLOWED_405}; use hyper::Method; use hyper::StatusCode; use hyper::body::Incoming; diff --git a/response/src/serialize_details.rs b/response/src/serialize_details.rs index 6d64f47..8b8de12 100644 --- a/response/src/serialize_details.rs +++ b/response/src/serialize_details.rs @@ -1,7 +1,6 @@ use crate::last_resort_response; -use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, ResponseParams, get_url_path_from_request, -}; +use crate::response_params::ResponseParams; +use crate::url_path_utils::{BoxedResponse, NOT_FOUND_404, get_url_path_from_request}; use http_body_util::{BodyExt, Full}; use hyper::StatusCode; use hyper::body::Incoming; @@ -30,36 +29,6 @@ pub struct FileDetails { pub modified_at: Option, } -impl FileDetails { - pub fn json_as_file_details(&self) -> FileDetailsJson { - let size = self.size.to_string(); - let created_at = get_optional_string(&self.created_at); - let accessed_at = get_optional_string(&self.accessed_at); - let modified_at = get_optional_string(&self.modified_at); - - FileDetailsJson { - is_file: self.is_file, - is_dir: self.is_dir, - is_symlink: self.is_symlink, - file_name: self.file_name.clone(), - url_path: self.url_path.clone(), - size, - created_at, - accessed_at, - modified_at, - } - } -} - -fn get_optional_string(opt_num: &Option) -> Option { - let num = match opt_num { - Some(n) => n, - _ => return None, - }; - - Some(num.to_string()) -} - #[derive(Clone, Serialize, Deserialize, Debug)] pub struct FileDetailsJson { is_file: bool, @@ -106,6 +75,12 @@ impl FileDetailsJson { } } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EntryDetailsJson { + details: FileDetailsJson, + entries: Vec, +} + fn get_optional_number(num_opt: &Option) -> Result, ParseIntError> { let num_string = match num_opt { Some(num_s) => num_s, @@ -120,12 +95,6 @@ fn get_optional_number(num_opt: &Option) -> Result, ParseIn Ok(Some(num)) } -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct EntryDetailsJson { - details: FileDetailsJson, - entries: Vec, -} - pub async fn build_response( req: &Request, res_params: &ResponseParams, diff --git a/response/src/utils_flyweight.rs b/response/src/url_path_utils.rs similarity index 84% rename from response/src/utils_flyweight.rs rename to response/src/url_path_utils.rs index f2cbba8..4e9ebfc 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/url_path_utils.rs @@ -1,4 +1,4 @@ -use crate::available_encodings::{AvailableEncodings, add_extension}; +use crate::available_encodings::add_extension; use bytes::Bytes; use http_body_util::combinators::BoxBody; use hyper::body::Incoming; @@ -13,23 +13,6 @@ pub const NOT_FOUND_404: &str = "404 not found"; pub const METHOD_NOT_ALLOWED_405: &str = "405 method not allowed"; pub const RANGE_NOT_SATISFIABLE_416: &str = "416 range not satisfiable"; -#[derive(Clone, Debug)] -pub struct ResponseParams { - pub directory: PathBuf, - pub available_encodings: AvailableEncodings, -} - -impl ResponseParams { - pub fn from(directory: &Path, content_encodings: &Vec) -> ResponseParams { - let available_encodings = AvailableEncodings::from(content_encodings); - - ResponseParams { - directory: directory.to_path_buf(), - available_encodings, - } - } -} - // https://doc.rust-lang.org/std/path/struct.Path.html#method.normalize_lexically // normalize lexically in nightly pub fn get_url_path_from_request( diff --git a/structures/src/lib.rs b/structures/src/lib.rs new file mode 100644 index 0000000..4b7ff7d --- /dev/null +++ b/structures/src/lib.rs @@ -0,0 +1,63 @@ +pub struct FileDetails { + pub is_file: bool, + pub is_dir: bool, + pub is_symlink: bool, + pub file_name: String, + pub url_path: String, + pub size: u64, + pub created_at: Option, + pub accessed_at: Option, + pub modified_at: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct FileDetailsJson { + is_file: bool, + is_dir: bool, + is_symlink: bool, + file_name: String, + url_path: String, + size: String, + created_at: Option, + accessed_at: Option, + modified_at: Option, +} + +impl FileDetailsJson { + pub fn as_file_details(&self) -> Result { + let size: u64 = match self.size.parse() { + Ok(num) => num, + Err(e) => return Err(e), + }; + let created_at = match get_optional_number(&self.created_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + let accessed_at = match get_optional_number(&self.accessed_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + let modified_at = match get_optional_number(&self.modified_at) { + Ok(num) => num, + Err(e) => return Err(e), + }; + + Ok(FileDetails { + is_file: self.is_file, + is_dir: self.is_dir, + is_symlink: self.is_symlink, + file_name: self.file_name.clone(), + url_path: self.url_path.clone(), + size, + created_at, + accessed_at, + modified_at, + }) + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EntryDetailsJson { + details: FileDetailsJson, + entries: Vec, +} \ No newline at end of file From d688da464a831bbd6efafd6249cd2c3a93c31890 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 14:45:32 -0800 Subject: [PATCH 18/23] range requests work --- response/src/range_response.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index cf518a2..ae10703 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -105,10 +105,27 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> _ => return None, }; - let mut request_body_size = 0; - let mut tail = 0; let mut ranges: Vec<(u64, u64)> = Vec::new(); - for range in ranges_str.split(",") { + + let mut split_ranges = ranges_str.split(","); + + // get first entry for initial body size and tail / end of previous segment + let (mut request_body_size, mut tail) = match split_ranges.next() { + Some(range) => match build_window_range(range, size) { + Some((start, end)) => { + ranges.push((start, end)); + (end - start, end) + } + _ => return None, + }, + _ => return None, + }; + + if MAX_RANGE_SIZE < request_body_size { + return None; + } + + while let Some(range) = split_ranges.next() { match build_window_range(range, size) { Some((start, end)) => { request_body_size += end - start; @@ -198,19 +215,19 @@ async fn compose_single_range_response( }; let content_range_header = build_content_range_header_str(start, end, size); - let size = end - start; + let buffer_size = end - start + 1; let mut builder = Response::builder() .status(StatusCode::PARTIAL_CONTENT) .header(CONTENT_TYPE, content_type) .header(CONTENT_RANGE, content_range_header) - .header(CONTENT_LENGTH, size); + .header(CONTENT_LENGTH, buffer_size); if let Some(enc) = content_encoding { builder = builder.header(CONTENT_ENCODING, enc); } - let reader_stream = ReaderStream::with_capacity(file, size as usize); + let reader_stream = ReaderStream::with_capacity(file, buffer_size as usize); let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data)); let boxed_body = stream_body.boxed(); From 7958324073c131a9f77c0e0e39df91f0bef6bf2b Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 14:48:16 -0800 Subject: [PATCH 19/23] comments --- response/src/range_response.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index ae10703..81dc2c6 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -109,7 +109,7 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> let mut split_ranges = ranges_str.split(","); - // get first entry for initial body size and tail / end of previous segment + // get first ranges to establish initial body size and tail / end of previous segment let (mut request_body_size, mut tail) = match split_ranges.next() { Some(range) => match build_window_range(range, size) { Some((start, end)) => { @@ -125,6 +125,7 @@ fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> return None; } + // get other ranges while let Some(range) = split_ranges.next() { match build_window_range(range, size) { Some((start, end)) => { From bb75b6ce04e20800cb9b27dc52798edff8829243 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 17:33:44 -0800 Subject: [PATCH 20/23] opt for the original module structure --- response/README.md | 0 response/src/lib.rs | 8 +++--- structures/src/lib.rs | 63 ------------------------------------------- 3 files changed, 4 insertions(+), 67 deletions(-) create mode 100644 response/README.md delete mode 100644 structures/src/lib.rs diff --git a/response/README.md b/response/README.md new file mode 100644 index 0000000..e69de29 diff --git a/response/src/lib.rs b/response/src/lib.rs index 4570468..3c132ec 100644 --- a/response/src/lib.rs +++ b/response/src/lib.rs @@ -9,7 +9,7 @@ mod responses; mod serialize_details; mod url_path_utils; -pub use crate::response_params::ResponseParams; -pub use crate::responses::compose_response; -pub use crate::serialize_details::{EntryDetailsJson, FileDetails, FileDetailsJson}; -pub use crate::url_path_utils::{BoxedResponse, get_url_path_from_request}; +pub use response_params::ResponseParams; +pub use responses::compose_response; +pub use serialize_details::{EntryDetailsJson, FileDetails, FileDetailsJson}; +pub use url_path_utils::{BoxedResponse, get_url_path_from_request}; diff --git a/structures/src/lib.rs b/structures/src/lib.rs deleted file mode 100644 index 4b7ff7d..0000000 --- a/structures/src/lib.rs +++ /dev/null @@ -1,63 +0,0 @@ -pub struct FileDetails { - pub is_file: bool, - pub is_dir: bool, - pub is_symlink: bool, - pub file_name: String, - pub url_path: String, - pub size: u64, - pub created_at: Option, - pub accessed_at: Option, - pub modified_at: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct FileDetailsJson { - is_file: bool, - is_dir: bool, - is_symlink: bool, - file_name: String, - url_path: String, - size: String, - created_at: Option, - accessed_at: Option, - modified_at: Option, -} - -impl FileDetailsJson { - pub fn as_file_details(&self) -> Result { - let size: u64 = match self.size.parse() { - Ok(num) => num, - Err(e) => return Err(e), - }; - let created_at = match get_optional_number(&self.created_at) { - Ok(num) => num, - Err(e) => return Err(e), - }; - let accessed_at = match get_optional_number(&self.accessed_at) { - Ok(num) => num, - Err(e) => return Err(e), - }; - let modified_at = match get_optional_number(&self.modified_at) { - Ok(num) => num, - Err(e) => return Err(e), - }; - - Ok(FileDetails { - is_file: self.is_file, - is_dir: self.is_dir, - is_symlink: self.is_symlink, - file_name: self.file_name.clone(), - url_path: self.url_path.clone(), - size, - created_at, - accessed_at, - modified_at, - }) - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct EntryDetailsJson { - details: FileDetailsJson, - entries: Vec, -} \ No newline at end of file From 7f2a04aea2ac8329974ab75f1f8ec3eccb853de2 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 17:56:49 -0800 Subject: [PATCH 21/23] remove unused dependenciy --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d29cecf..2f07c62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,3 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tokio-util = "0.7" tokio = { version = "1.51", features = ["full"] } -tokio_stream = { version = "0.1" } \ No newline at end of file From fbef7968c728e8e7ffda731c1a4a4ebc284437a0 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 18:50:31 -0800 Subject: [PATCH 22/23] ranges show correct byte length and build --- response/src/range_response.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 81dc2c6..8cfc6b6 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -65,7 +65,7 @@ async fn attempt_range_response( return None; } - let ranges = match get_ranges(range_header, metadata.len()) { + let ranges = match get_ranges(range_header, metadata.len() - 1) { Some(rngs) => rngs, _ => { return Some(last_resort_response::build_response( @@ -195,7 +195,7 @@ fn build_window_range(range_str: &str, size: u64) -> Option<(u64, u64)> { async fn compose_single_range_response( mut file: File, - size: u64, + file_size: u64, content_type: &str, content_encoding: Option, ranges: Vec<(u64, u64)>, @@ -215,7 +215,7 @@ async fn compose_single_range_response( return None; }; - let content_range_header = build_content_range_header_str(start, end, size); + let content_range_header = build_content_range_header_str(start, end, file_size); let buffer_size = end - start + 1; let mut builder = Response::builder() @@ -235,6 +235,6 @@ async fn compose_single_range_response( Some(builder.body(boxed_body)) } -fn build_content_range_header_str(start: u64, end: u64, size: u64) -> String { - "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &size.to_string() +fn build_content_range_header_str(start: u64, end: u64, file_size: u64) -> String { + "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &file_size.to_string() } From 66d23ad8f9d6ed532ea77729bac923a091e4c0e9 Mon Sep 17 00:00:00 2001 From: Taylor Vann Date: Sun, 8 Nov 2026 18:56:21 -0800 Subject: [PATCH 23/23] get the zeroeth byte --- response/src/range_response.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/response/src/range_response.rs b/response/src/range_response.rs index 8cfc6b6..e952b2b 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -171,7 +171,7 @@ fn build_window_range(range_str: &str, size: u64) -> Option<(u64, u64)> { end_int = size; } - if start_int < end_int { + if start_int <= end_int { return Some((start_int, end_int)); } @@ -236,5 +236,10 @@ async fn compose_single_range_response( } fn build_content_range_header_str(start: u64, end: u64, file_size: u64) -> String { - "bytes ".to_string() + &start.to_string() + "-" + &end.to_string() + "/" + &file_size.to_string() + "bytes ".to_string() + + &start.to_string() + + "-" + + &end.to_string() + + "/" + + &file_size.to_string() }