atlas_http/
body.rs

1
2use crate::error::Error;
3use rand::{distributions::Alphanumeric, thread_rng, Rng};
4use std::collections::HashMap;
5use std::fs;
6use std::fs::File;
7use std::path::Path;
8use urlencoding::{decode, encode};
9
10#[derive(Clone, Debug)]
11pub struct HttpBody {
12    is_form_post: bool,
13    params: HashMap<String, String>,
14    raw: Vec<u8>,
15    boundary: String,
16    files: HashMap<String, String>,
17}
18
19
20impl HttpBody {
21    // Instantiate new body
22    pub fn new(params: &HashMap<String, String>, raw: &[u8]) -> Self {
23        let boundary: String = thread_rng()
24            .sample_iter(&Alphanumeric)
25            .take(30)
26            .map(|c| c as char)
27            .collect();
28
29        Self {
30            is_form_post: params.keys().len() > 0 || raw.len() > 0,
31            params: params.clone(),
32            raw: raw.clone().to_vec(),
33            boundary,
34            files: HashMap::new(),
35        }
36    }
37
38    /// Instantiate an empty body.
39    pub fn empty() -> Self {
40        Self::new(&HashMap::new(), &Vec::new())
41    }
42
43    /// Generate body from str
44    pub fn from_string(data: &str) -> Self {
45        // Create pairs
46        let mut params: HashMap<String, String> = HashMap::new();
47        for pair in data.split('&') {
48            if let Some(index) = pair.find('=') {
49                params.insert(
50                    pair[..index].to_string(),
51                    decode(pair[index + 1..].trim()).unwrap().to_string(),
52                );
53            }
54        }
55
56        Self::new(&params, &Vec::new())
57    }
58
59    /// Generate body from hashmap
60    pub fn from_map(params: &HashMap<&str, &str>) -> Self {
61        let formatted_params = params
62            .iter()
63            .map(|(key, value)| (key.to_string(), value.to_string()))
64            .collect();
65        Self::new(&formatted_params, &Vec::new())
66    }
67
68    // Generate body with raw vec<u8> (eg. JSON object).  Body is not split and formatted into post params.
69    pub fn from_raw(data: &[u8]) -> Self {
70        Self::new(&HashMap::new(), data)
71    }
72
73    // Generate body with raw str (eg. JSON object).  Body is not split and formatted into post params.
74    pub fn from_raw_str(data: &str) -> Self {
75        Self::new(&HashMap::new(), data.as_bytes())
76    }
77
78    /// Add post parameter
79    pub fn set_param(&mut self, key: &str, value: &str) {
80        *self
81            .params
82            .entry(key.to_string())
83            .or_insert(value.to_string()) = value.to_string();
84        self.is_form_post = true;
85    }
86
87    // Upload a file
88    pub fn upload_file(&mut self, param_name: &str, file_path: &str) -> Result<(), Error> {
89        // Ensure file exists
90        if !Path::new(&file_path).exists() {
91            return Err(Error::FileNotExists(file_path.to_string()));
92        }
93        *self
94            .files
95            .entry(param_name.to_string())
96            .or_insert(file_path.to_string()) = file_path.to_string();
97
98        Ok(())
99    }
100
101    /// Format body for HTTP message
102    pub fn format(&self) -> Vec<u8> {
103        if !self.files.is_empty() {
104            return self.format_multipart();
105        } else if self.raw.len() > 0 {
106            return self.raw.clone();
107        } else if !self.is_form_post {
108            return Vec::new();
109        }
110
111        let body = self
112            .params
113            .iter()
114            .map(|(key, value)| format!("{}={}", key, encode(value)))
115            .collect::<Vec<String>>()
116            .join("&");
117
118        body.as_bytes().to_vec()
119    }
120
121    /// Format multipart message, used for uploading files
122    fn format_multipart(&self) -> Vec<u8> {
123
124        // Go through params
125        let mut body: Vec<u8> = Vec::new();
126        for (key, value) in self.params.iter() {
127            let section = format!(
128                "--{}\r\nContent-Disposition: form-data; name=\"{}\"\r\n\r\n{}\r\n",
129                self.boundary, key, value
130            );
131            body.extend_from_slice(section.as_bytes());
132        }
133
134        // Go through files
135        for (key, filepath) in self.files.iter() {
136            let (filename, mime_type, contents) = self.get_file_info(filepath);
137            let section = format!("--{}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n", self.boundary, key, filename, mime_type);
138            body.extend_from_slice(section.as_bytes());
139            body.extend_from_slice(&contents);
140            body.extend_from_slice("\r\n".as_bytes());
141        }
142        body.extend_from_slice(format!("--{}--\r\n", self.boundary).as_bytes());
143
144        body
145    }
146
147    /// Get info for uploaded file
148    fn get_file_info(&self, filepath: &String) -> (String, String, Vec<u8>) {
149        // Get filename
150        let pos = filepath
151            .rfind('/')
152            .or_else(|| filepath.rfind('\\'))
153            .unwrap();
154        let filename = filepath[pos + 1..].to_string();
155
156        // Get mime type
157        let mime_guess = mime_guess::from_path(filepath);
158        let mime_type = if mime_guess.count() > 0 {
159            mime_guess.first().unwrap().to_string()
160        } else {
161            "application/octet-stream".to_string()
162        };
163
164        let _file = File::open(filepath).unwrap();
165        let content =
166            fs::read(filepath).unwrap_or_else(|_| panic!("Unable to read file at, {}", filepath));
167
168        (filename, mime_type, content)
169    }
170    /// Get is_form_post
171    pub fn is_form_post(&self) -> bool {
172        self.is_form_post
173    }
174
175    /// Get params
176    pub fn params(&self) -> HashMap<String, String> {
177        self.params.clone()
178    }
179
180    /// Add param
181    pub fn add_param(&mut self, key: &str, value: &str) {
182        *self.params.entry(key.to_string()).or_default() = value.to_string();
183        self.is_form_post = true;
184    }
185
186    /// Get raw data
187    pub fn get_raw(&self) -> Vec<u8> {
188        self.raw.clone()
189    }
190
191    /// Get boundary
192    pub fn boundary(&self) -> String {
193        self.boundary.clone()
194    }
195
196    /// Get uploaded files
197    pub fn files(&self) -> HashMap<String, String> {
198        self.files.clone()
199    }
200}