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)) => {
132 log::error!("failed to read file ({}): {}", self.file_path, err);
133 internal_server_error_response(
134 "failed to read response file",
135 &self.request_headers,
136 )
137 }
138 Err(err) => {
139 log::error!("async task failed ({}): {}", self.file_path, err);
140 internal_server_error_response(
141 "failed to read response file",
142 &self.request_headers,
143 )
144 }
145 }
146 }
147 Err(err) => {
148 log::error!("async task failed ({}): {}", self.file_path, err);
149 internal_server_error_response(
150 "failed to read response file",
151 &self.request_headers,
152 )
153 }
154 }
155 }
156
157 fn text_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
167 match file_extension(self.file_path.as_str()) {
168 Some(ext) => match ext.as_str() {
169 "json" | "json5" => self.json_file_content_response(),
170 "csv" => self.csv_file_content_response(),
171 _ => text_response(
172 self.text_content.clone().unwrap_or_default().as_str(),
173 Some(text_file_content_type(ext).as_str()),
174 self.custom_headers.as_ref(),
175 &self.request_headers,
176 ),
177 },
178 None => text_response(
179 self.text_content.clone().unwrap_or_default().as_str(),
180 None,
181 self.custom_headers.as_ref(),
182 &self.request_headers,
183 ),
184 }
185 }
186
187 fn json_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
189 let json_str = self.text_content.clone().unwrap_or_default();
190 json_response(
191 json_str.as_str(),
192 None,
193 self.custom_headers.as_ref(),
194 &self.request_headers,
195 Some(self.file_path.as_str()),
196 )
197 }
198
199 fn csv_file_content_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
201 let text_content = self.text_content.clone().unwrap_or_default();
202 let mut rdr = csv::ReaderBuilder::new()
203 .has_headers(true)
204 .from_reader(text_content.as_bytes());
205
206 let csv_headers = if let Ok(csv_headers) = rdr.headers() {
207 csv_headers.clone()
208 } else {
209 log::error!(
210 "failed to analyze csv headers ({})",
211 self.file_path.as_str()
212 );
213 return internal_server_error_response(
214 "failed to analyze csv headers",
215 &self.request_headers,
216 );
217 };
218
219 let rows = rdr
220 .records()
221 .map(|result| {
222 let record = result?;
223 let obj = csv_headers
224 .iter()
225 .zip(record.iter())
226 .map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
227 .collect::<Map<_, _>>();
228 Ok(Value::Object(obj))
229 })
230 .collect::<Result<Vec<Value>, csv::Error>>();
231
232 match rows {
233 Ok(rows) => {
234 let jsonpath_key = if let Some(csv_records_key) = self.csv_records_key.as_ref() {
235 csv_records_key.as_str()
236 } else {
237 CSV_RECORDS_DEFAULT_KEY
238 };
239 let json_value = json_value_with_jsonpath_key(jsonpath_key, Value::from(rows));
240
241 let body = serde_json::to_string(&json_value);
242 match body {
243 Ok(body) => json_response(
244 body.as_str(),
245 None,
246 self.custom_headers.as_ref(),
247 &self.request_headers,
248 Some(self.file_path.as_str()),
249 ),
250 Err(err) => {
251 log::error!(
252 "failed to convert csv records to json response ({}): {}",
253 self.file_path.as_str(),
254 err
255 );
256 internal_server_error_response(
257 "failed to convert csv records to json response",
258 &self.request_headers,
259 )
260 }
261 }
262 }
263 Err(err) => {
264 log::error!(
265 "failed to analyze csv records ({}): {}",
266 self.file_path.as_str(),
267 err
268 );
269 internal_server_error_response(
270 "failed to analyze csv records",
271 &self.request_headers,
272 )
273 }
274 }
275 }
276
277 fn binary_content_type_response(&self) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
286 let content = self.binary_content.clone().unwrap_or_default().to_owned();
287 let content_type = binary_content_type(self.file_path.as_str());
288 ResponseHandler::default()
289 .with_binary_body(content, Some(content_type))
290 .with_custom_headers(self.custom_headers.as_ref())
291 .into_response(&self.request_headers)
292 }
293}