1use console::style;
2use hyper::HeaderMap;
3use serde_json::{Map, Value};
4use tokio::task;
5
6use std::{collections::HashMap, fs, path::Path};
7
8use crate::{
9 constant::CSV_RECORDS_DEFAULT_KEY,
10 json_path_util::resolve_with_json_compatible_extensions,
11 response::{
12 confine::confine, error_response::not_found_response, json_response::json_response,
13 },
14 response_handler::ResponseHandler,
15 types::BoxBody,
16};
17
18use super::{
19 error_response::internal_server_error_response,
20 text_response::text_response,
21 util::{
22 binary_content_type, file_extension, json_value_with_jsonpath_key, text_file_content_type,
23 },
24};
25
26pub struct FileResponse {
27 file_path: String,
28 csv_records_key: Option<String>,
29 text_content: Option<String>,
30 binary_content: Option<Vec<u8>>,
31 custom_headers: Option<HashMap<String, Option<String>>>,
32 request_headers: HeaderMap,
33 confine_to: Option<std::path::PathBuf>,
38}
39
40impl FileResponse {
41 pub fn new(
43 file_path: &str,
44 custom_headers: Option<&HashMap<String, Option<String>>>,
45 request_headers: &HeaderMap,
46 confine_to: Option<&Path>,
47 ) -> Self {
48 FileResponse {
49 file_path: file_path.to_owned(),
50 csv_records_key: None,
51 text_content: None,
52 binary_content: None,
53 custom_headers: custom_headers.cloned(),
54 request_headers: request_headers.clone(),
55 confine_to: confine_to.map(Path::to_path_buf),
56 }
57 }
58
59 pub fn new_with_csv_records_jsonpath(
61 file_path: &str,
62 custom_headers: Option<&HashMap<String, Option<String>>>,
63 csv_records_key: Option<String>,
64 request_headers: &HeaderMap,
65 confine_to: Option<&Path>,
66 ) -> Self {
67 let mut ret = FileResponse::new(file_path, custom_headers, request_headers, confine_to);
68 ret.csv_records_key = csv_records_key;
69 ret
70 }
71
72 pub async fn file_content_response(
74 &mut self,
75 ) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
76 let file_path = match resolve_with_json_compatible_extensions(self.file_path.as_str()) {
77 Some(x) => x,
78 None => {
79 log::warn!(
80 "{}:\n{} (missing or a directory)",
81 style("file not found").red(),
82 self.file_path
83 );
84 return not_found_response(&self.request_headers);
85 }
86 };
87
88 let file_path = match confine(file_path.as_str(), self.confine_to.as_deref()) {
94 Some(canonical) => match canonical.to_str() {
95 Some(x) => x.to_owned(),
96 None => {
97 log::error!(
98 "{} to get str from canonicalized file path:\n{}",
99 style("failed").red(),
100 file_path
101 );
102 return not_found_response(&self.request_headers);
103 }
104 },
105 None => {
106 return not_found_response(&self.request_headers);
107 }
108 };
109 self.file_path = file_path.clone();
110
111 let file_path_to_read_text_file = file_path.clone();
113 let content =
114 task::spawn_blocking(move || fs::read_to_string(file_path_to_read_text_file)).await;
115
116 match content {
117 Ok(Ok(content)) => {
118 self.text_content = Some(content);
119 self.text_file_content_response()
120 }
121 Ok(Err(_)) => {
122 let file_path_to_read_binary = file_path.clone();
124 let content =
125 task::spawn_blocking(move || fs::read(file_path_to_read_binary)).await;
126 match content {
127 Ok(Ok(content)) => {
128 self.binary_content = Some(content);
129 self.binary_content_type_response()
130 }
131 Ok(Err(err)) => internal_server_error_response(
132 &format!("{}: failed to read file - {}", self.file_path, err),
133 &self.request_headers,
134 ),
135 Err(err) => internal_server_error_response(
136 &format!("{}: async task failed - {}", self.file_path, err),
137 &self.request_headers,
138 ),
139 }
140 }
141 Err(err) => internal_server_error_response(
142 &format!("{}: async task failed - {}", self.file_path, err),
143 &self.request_headers,
144 ),
145 }
146 }
147
148 fn text_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
158 match file_extension(self.file_path.as_str()) {
159 Some(ext) => match ext.as_str() {
160 "json" | "json5" => self.json_file_content_response(),
161 "csv" => self.csv_file_content_response(),
162 _ => text_response(
163 self.text_content.clone().unwrap_or_default().as_str(),
164 Some(text_file_content_type(ext).as_str()),
165 self.custom_headers.as_ref(),
166 &self.request_headers,
167 ),
168 },
169 None => text_response(
170 self.text_content.clone().unwrap_or_default().as_str(),
171 None,
172 self.custom_headers.as_ref(),
173 &self.request_headers,
174 ),
175 }
176 }
177
178 fn json_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
180 let json_str = self.text_content.clone().unwrap_or_default();
181 json_response(
182 json_str.as_str(),
183 self.custom_headers.as_ref(),
184 &self.request_headers,
185 self.file_path.as_str(),
186 )
187 }
188
189 fn csv_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
191 let text_content = self.text_content.clone().unwrap_or_default();
192 let mut rdr = csv::ReaderBuilder::new()
193 .has_headers(true)
194 .from_reader(text_content.as_bytes());
195
196 let csv_headers = if let Ok(csv_headers) = rdr.headers() {
197 csv_headers.clone()
198 } else {
199 return internal_server_error_response(
200 &format!("{}: failed to analyze csv headers", self.file_path.as_str()),
201 &self.request_headers,
202 );
203 };
204
205 let rows = rdr
206 .records()
207 .map(|result| {
208 let record = result?;
209 let obj = csv_headers
210 .iter()
211 .zip(record.iter())
212 .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
213 .collect::<Map<_, _>>();
214 Ok(Value::Object(obj))
215 })
216 .collect::<Result<Vec<Value>, csv::Error>>();
217
218 match rows {
219 Ok(rows) => {
220 let jsonpath_key = if let Some(csv_records_key) = self.csv_records_key.as_ref() {
221 csv_records_key.as_str()
222 } else {
223 CSV_RECORDS_DEFAULT_KEY
224 };
225 let json_value = json_value_with_jsonpath_key(jsonpath_key, Value::from(rows));
226
227 let body = serde_json::to_string(&json_value);
228 match body {
229 Ok(body) => json_response(
230 body.as_str(),
231 self.custom_headers.as_ref(),
232 &self.request_headers,
233 self.file_path.as_str(),
234 ),
235 Err(err) => internal_server_error_response(
236 &format!(
237 "{}: failed to convert csv records to json response - {}",
238 self.file_path.as_str(),
239 err
240 ),
241 &self.request_headers,
242 ),
243 }
244 }
245 Err(err) => internal_server_error_response(
246 &format!(
247 "{}: failed to analyze csv records - {}",
248 self.file_path.as_str(),
249 err
250 ),
251 &self.request_headers,
252 ),
253 }
254 }
255
256 fn binary_content_type_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
258 let mut response_handler = ResponseHandler::default();
259
260 if let Some(custom_headers) = self.custom_headers.clone() {
261 response_handler = response_handler.with_headers(custom_headers);
262 }
263
264 let content = self.binary_content.clone().unwrap_or_default().to_owned();
265 let content_type = binary_content_type(self.file_path.as_str());
266 response_handler
267 .with_binary_body(content, Some(content_type))
268 .into_response(&self.request_headers)
269 }
270}