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 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/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/README.md b/response/README.md new file mode 100644 index 0000000..e69de29 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 76f6f9a..11110b2 100644 --- a/response/src/get_response.rs +++ b/response/src/get_response.rs @@ -1,10 +1,11 @@ -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::response_params::ResponseParams; use crate::serialize_details; -use crate::utils_flyweight::{ - BoxedResponse, NOT_FOUND_404, ResponseParams, 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}; @@ -13,7 +14,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 +22,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,15 +31,16 @@ 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_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, @@ -49,31 +50,20 @@ async fn build_req_path_response( _ => return None, }; - 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 + let content_type = get_content_type(&filepath); + 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/head_response.rs b/response/src/head_response.rs index 81056f3..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; @@ -17,60 +16,69 @@ pub async fn build_response( 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); + 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; + }; + }; - // try encoded - for encoding in encodings { - let encoded_path = match add_extension(&filepath, &encoding) { - Some(enc_pth) => enc_pth, - _ => continue, - }; + 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; - } - } +// just use "file" and grab metadata instead of more code? - // try base - if let Some(res) = compose_response(&filepath, content_type, None).await { - return res; +async fn get_size_and_encoding( + filepath: &Path, + encodings: Vec, +) -> Option<(u64, Option)> { + for encoding in encodings { + let encoded_path = match add_extension(&filepath, &encoding) { + Some(enc_pth) => enc_pth, + _ => continue, + }; + + if let Some(size) = get_file_size(&encoded_path).await { + return Some((size, Some(encoding.to_string()))); } - }; + } - last_resort_response::build_response(StatusCode::NOT_FOUND, NOT_FOUND_404) + match get_file_size(filepath).await { + Some(size) => Some((size, None)), + _ => None, + } } -async fn compose_response( - filepath: &Path, - content_type: &str, - content_encoding: Option<&str>, -) -> Option> { +async fn get_file_size(filepath: &Path) -> Option { 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.len()), + _ => None, } +} +async fn compose_response( + 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); } - 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/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 8027b82..3c132ec 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::responses::compose_response; -pub use crate::serialize_details::{EntryDetails, FileDetails}; -pub use crate::utils_flyweight::{BoxedResponse, ResponseParams, 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/response/src/range_response.rs b/response/src/range_response.rs index 4968737..e952b2b 100644 --- a/response/src/range_response.rs +++ b/response/src/range_response.rs @@ -1,8 +1,9 @@ -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, +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; @@ -10,196 +11,198 @@ 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::io::SeekFrom; -use std::path::Path; -use tokio::fs; +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, ) -> Option> { - let range_header = match get_range_header(req) { - Some(rh) => rh, + let range_header = match req.headers().get(RANGE) { + Some(enc) => enc, _ => return None, }; - // flatten this - if let Some(filepath) = get_url_path_from_request(req, &res_params.directory, true) { - if let Some(ranges) = get_ranges(&range_header) { - let encodings = get_encodings(req, &res_params.available_encodings); - - if let Some(response) = build_single_range_response(&filepath, &encodings, ranges).await - { - return Some(response); - } - }; + match attempt_range_response(req, res_params, range_header).await { + Some(res) => Some(res), + _ => Some(last_resort_response::build_response( + StatusCode::NOT_FOUND, + NOT_FOUND_404, + )), } - - Some(last_resort_response::build_response( - StatusCode::NOT_FOUND, - NOT_FOUND_404, - )) } -fn get_range_header(req: &Request) -> Option { - let accept_encoding_header = match req.headers().get(RANGE) { - Some(enc) => enc, +// Returning None fallsback to 404 +async fn attempt_range_response( + req: &Request, + res_params: &ResponseParams, + 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, }; - match accept_encoding_header.to_str() { - Ok(s) => Some(s.to_string()), - _ => 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=") { - Some(r) => r, + 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, }; - 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, - }; + let metadata = match file.metadata().await { + Ok(m) => m, + _ => return None, + }; - // add verified ranges here + if !metadata.is_file() { + return None; + } - ranges.push((Some(start_range_int), None)); - continue; + let ranges = match get_ranges(range_header, metadata.len() - 1) { + Some(rngs) => rngs, + _ => { + return Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )); } + }; - // suffix-range - if let Some(prefixless) = range_str.strip_prefix("-") { - let end_range_int: usize = match prefixless.parse() { - Ok(sri) => sri, - _ => return None, - }; + let content_type = get_content_type(&filepath); - ranges.push((None, Some(end_range_int))); - continue; + match ranges.len() { + 0 => Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )), + 1 => { + compose_single_range_response(file, metadata.len(), content_type, encoding_opt, ranges) + .await } - - // window-range - let start_end_range = match get_window_range(range_str) { - Some(ser) => ser, - _ => return None, - }; - - ranges.push(start_end_range) + // vs multi range (stretch goal) + _ => Some(last_resort_response::build_response( + StatusCode::RANGE_NOT_SATISFIABLE, + RANGE_NOT_SATISFIABLE_416, + )), } - - if 0 < ranges.len() { - return Some(ranges); - } - - 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, +fn get_ranges(range_header: &HeaderValue, size: u64) -> Option> { + let header_str = match range_header.to_str() { + Ok(hstr) => hstr, _ => return None, }; - let end_range_str = match values.next() { - Some(end_range) => end_range, + let ranges_str = match header_str.trim().strip_prefix("bytes=") { + Some(r) => r, _ => return None, }; - let start_range_int: usize = match start_range_str.parse() { - Ok(sri) => sri, - _ => return None, - }; + let mut ranges: Vec<(u64, u64)> = Vec::new(); + + let mut split_ranges = ranges_str.split(","); - let end_range_int: usize = match end_range_str.parse() { - Ok(sri) => sri, + // 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)) => { + ranges.push((start, end)); + (end - start, end) + } + _ => return None, + }, _ => return None, }; - if start_range_int < end_range_int { - return Some((Some(start_range_int), Some(end_range_int))); + if MAX_RANGE_SIZE < request_body_size { + return None; } - None + // get other ranges + while let Some(range) = split_ranges.next() { + 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, + } + } + + Some(ranges) } -async fn build_single_range_response( - filepath: &Path, - encodings: &Vec, - ranges: Vec<(Option, Option)>, -) -> Option> { - let content_type = get_content_type(&filepath); +fn build_window_range(range_str: &str, size: u64) -> Option<(u64, u64)> { + let values: Vec<&str> = range_str.split("-").collect(); - if let Some(res) = - compose_encoded_single_range_response(&filepath, content_type, &encodings, &ranges).await - { - return Some(res); + 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, }; - // origin target - compose_single_range_response(&filepath, content_type, None, &ranges).await -} + let start_range_int: Result = first_str.parse(); + let end_range_int: Result = second_str.parse(); -async fn compose_encoded_single_range_response( - filepath: &Path, - content_type: &str, - encodings: &Vec, - ranges: &Vec<(Option, Option)>, -) -> Option> { - 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); + 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, } - - None } async fn compose_single_range_response( - filepath: &Path, + mut file: File, + file_size: u64, content_type: &str, - content_encoding: Option<&str>, - ranges: &Vec<(Option, Option)>, + content_encoding: Option, + ranges: Vec<(u64, u64)>, ) -> Option> { - let mut file = match File::open(filepath).await { - Ok(m) => m, - _ => return None, - }; - - let metadata = match fs::metadata(filepath).await { - Ok(m) => m, - _ => return None, - }; - - if !metadata.is_file() { - return None; - } - - let size = metadata.len() as usize; - let (start, end) = match get_start_and_end(ranges, size) { - Some(se) => se, + // split here for single range response + 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, @@ -208,53 +211,35 @@ 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; }; - 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 content_range_header = build_content_range_header_str(start, end, file_size); + 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, buffer.len().to_string()); + .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); + 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(); Some(builder.body(boxed_body)) } -fn get_start_and_end( - ranges: &Vec<(Option, Option)>, - size: usize, -) -> Option<(usize, usize)> { - let (start, end) = match ranges.get(0) { - // 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 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 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() } 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 3354344..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; @@ -12,59 +11,132 @@ 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; -// 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 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: u64, - created_at: Option, - accessed_at: Option, - modified_at: Option, + 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 EntryDetails { - details: FileDetails, - entries: Vec, +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, + _ => return Ok(None), + }; + + let num: u128 = match num_string.parse() { + Ok(n) => n, + Err(e) => return Err(e), + }; + + Ok(Some(num)) } 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() { - return None; + let details_as = match params_map.get("details_as") { + Some(srlz) => srlz, + _ => 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); + } } - let params_map = form_urlencoded::parse(params.as_bytes()) - .into_owned() - .collect::>(); + Some(last_resort_response::build_response( + StatusCode::NOT_FOUND, + NOT_FOUND_404, + )) +} - 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( @@ -81,7 +153,7 @@ async fn compose_entry_details_response( _ => return None, }; - // immediately drop symlink, feels dangerous + // Drop symlink, feels dangerous if metadata.is_symlink() { return None; } @@ -91,36 +163,35 @@ 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 >>>???? 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(), - }) +) -> Option { + match create_entry_details(metadata, req_path, base_path) { + Some(details) => Some(EntryDetailsJson { + details, + entries: Vec::new(), + }), + _ => None, + } } async fn build_directory_entry( metadata: &Metadata, req_path: &Path, base_path: &Path, -) -> Option { - let details = create_entry_details(metadata, req_path, base_path); +) -> 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(), }; @@ -137,9 +208,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; } @@ -150,25 +221,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(FileDetailsJson { is_dir: metadata.is_dir(), is_file: metadata.is_file(), is_symlink: metadata.is_symlink(), @@ -177,12 +249,12 @@ fn create_entry_details(metadata: &Metadata, req_path: &Path, base_path: &Path) created_at, accessed_at, modified_at, - size: metadata.len(), - } + size: metadata.len().to_string(), + }) } -async fn compose_response( - details_opt: Option, +fn compose_response( + details_opt: Option, ) -> Option> { let details = match details_opt { Some(deets) => deets, @@ -206,17 +278,16 @@ async 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()); } } 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/url_path_utils.rs similarity index 62% rename from response/src/utils_flyweight.rs rename to response/src/url_path_utils.rs index f9f9f01..4e9ebfc 100644 --- a/response/src/utils_flyweight.rs +++ b/response/src/url_path_utils.rs @@ -1,9 +1,10 @@ -use crate::available_encodings::AvailableEncodings; +use crate::available_encodings::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>; @@ -12,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( @@ -37,13 +21,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 +31,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), @@ -71,17 +49,14 @@ fn convert_to_os_path(path_buf_str: &str, add_index_on_directory: bool) -> PathB normalized_uri_path } -// 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 +66,27 @@ fn normalize_path_lexicographically(path_buf: &Path) -> Option { normalized.push(part); } - Some(normalized) + 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, + }; + + match File::open(encoded_path).await { + Ok(f) => return Some((f, Some(encoding.clone()))), + _ => continue, + }; + } + + match File::open(filepath).await { + Ok(f) => return Some((f, None)), + _ => return None, + } }