1use std::{
2 collections::{HashMap, HashSet},
3 fmt::Display,
4 ops::Range,
5 path::PathBuf,
6};
7
8use crate::{common, util};
9
10#[derive(Debug, Default, Clone)]
12pub enum RequestBody {
13 #[default]
14 Empty,
15 Text(String),
16 Bytes(Vec<u8>),
17
18 File(PathBuf, Option<Range<u64>>),
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum RequestMethod {
25 Get,
26 Put,
27 Post,
28 Delete,
29 Head,
30}
31
32impl Display for RequestMethod {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 RequestMethod::Get => write!(f, "GET"),
36 RequestMethod::Put => write!(f, "PUT"),
37 RequestMethod::Post => write!(f, "POST"),
38 RequestMethod::Delete => write!(f, "DELETE"),
39 RequestMethod::Head => write!(f, "HEAD"),
40 }
41 }
42}
43
44impl From<RequestMethod> for reqwest::Method {
45 fn from(value: RequestMethod) -> Self {
46 match value {
47 RequestMethod::Get => reqwest::Method::GET,
48 RequestMethod::Put => reqwest::Method::PUT,
49 RequestMethod::Post => reqwest::Method::POST,
50 RequestMethod::Delete => reqwest::Method::DELETE,
51 RequestMethod::Head => reqwest::Method::HEAD,
52 }
53 }
54}
55
56pub struct OssRequest {
58 pub bucket_name: String,
59 pub object_key: String,
60 pub method: RequestMethod,
61 pub headers: HashMap<String, String>,
62
63 pub additional_headers: HashSet<String>,
67 pub query: HashMap<String, String>,
68
69 pub body: RequestBody,
70}
71
72impl Default for OssRequest {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl OssRequest {
79 pub fn new() -> Self {
85 let date_time_string = util::get_iso8601_date_time_string();
86 Self {
87 bucket_name: "".to_string(),
88 object_key: "".to_string(),
89 method: RequestMethod::Get,
90 headers: HashMap::from([
91 ("x-sdk-client".to_string(), format!("ali-oss-rs/{}", common::VERSION)),
92 ("x-oss-content-sha256".to_string(), "UNSIGNED-PAYLOAD".to_string()),
93 ("x-oss-date".to_string(), date_time_string),
94 ]),
95 additional_headers: HashSet::new(),
96 query: HashMap::new(),
97 body: RequestBody::Empty,
98 }
99 }
100
101 pub fn method(mut self, m: RequestMethod) -> Self {
103 self.method = m;
104 self
105 }
106
107 pub fn bucket<S: Into<String>>(mut self, bucket_name: S) -> Self {
109 self.bucket_name = bucket_name.into();
110 self
111 }
112
113 pub fn object<S: Into<String>>(mut self, object_key: S) -> Self {
115 self.object_key = object_key.into();
116 self
117 }
118
119 pub fn add_header<S1, S2>(self, k: S1, v: S2) -> Self
122 where
123 S1: AsRef<str>,
124 S2: AsRef<str>,
125 {
126 self.add_header_ext(k, v, false)
127 }
128
129 pub fn add_header_ext<S1, S2>(mut self, k: S1, v: S2, addtional_header: bool) -> Self
134 where
135 S1: AsRef<str>,
136 S2: AsRef<str>,
137 {
138 self.headers.insert(k.as_ref().to_string(), v.as_ref().to_string());
139 if addtional_header {
140 self.additional_headers.insert(k.as_ref().to_lowercase());
141 }
142
143 self
144 }
145
146 pub fn add_query<S1, S2>(mut self, k: S1, v: S2) -> Self
150 where
151 S1: AsRef<str>,
152 S2: AsRef<str>,
153 {
154 self.query.insert(k.as_ref().to_string(), v.as_ref().to_string());
155 self
156 }
157
158 #[allow(dead_code)]
160 pub fn add_additional_header_name<S>(mut self, name: S) -> Self
161 where
162 S: AsRef<str>,
163 {
164 self.additional_headers.insert(name.as_ref().to_lowercase());
165 self
166 }
167
168 pub fn body(mut self, body: RequestBody) -> Self {
170 self.body = body;
171 self
172 }
173
174 pub fn text_body(self, text: impl Into<String>) -> Self {
176 self.body(RequestBody::Text(text.into()))
177 }
178
179 #[allow(dead_code)]
180 pub fn bytes_body(self, bytes: impl Into<Vec<u8>>) -> Self {
182 self.body(RequestBody::Bytes(bytes.into()))
183 }
184
185 #[allow(dead_code)]
186 pub fn file_body(self, file_path: impl Into<PathBuf>) -> Self {
188 self.body(RequestBody::File(file_path.into(), None))
189 }
190
191 pub fn content_type(mut self, content_type: &str) -> Self {
193 self.headers.insert("content-type".to_string(), content_type.to_string());
194 self
195 }
196
197 pub fn content_length(mut self, len: u64) -> Self {
199 self.headers.insert("content-length".to_string(), len.to_string());
200 self
201 }
202
203 #[allow(dead_code)]
204 pub fn headers_mut(&mut self) -> &mut HashMap<String, String> {
205 &mut self.headers
206 }
207
208 #[allow(dead_code)]
209 pub fn additional_headers_mut(&mut self) -> &mut HashSet<String> {
210 &mut self.additional_headers
211 }
212
213 #[allow(dead_code)]
214 pub fn query_mut(&mut self) -> &mut HashMap<String, String> {
215 &mut self.query
216 }
217
218 pub(crate) fn build_canonical_uri(&self) -> String {
226 match (self.bucket_name.is_empty(), self.object_key.is_empty()) {
227 (true, true) => "/".to_string(),
228 (true, false) => format!("/{}/", urlencoding::encode(&self.bucket_name)),
229 (_, _) => {
230 format!(
231 "/{}/{}",
232 urlencoding::encode(&self.bucket_name),
233 self.object_key.split("/").map(|s| urlencoding::encode(s)).collect::<Vec<_>>().join("/")
234 )
235 }
236 }
237 }
238
239 pub(crate) fn build_request_uri(&self) -> String {
242 if self.object_key.is_empty() {
243 return "/".to_string();
244 }
245
246 let s = self.object_key.split("/").collect::<Vec<_>>().join("/");
247 format!("/{}", s)
248 }
249
250 pub(crate) fn build_canonical_query_string(&self) -> String {
259 if self.query.is_empty() {
260 return "".to_string();
261 }
262
263 let mut pairs = self
264 .query
265 .iter()
266 .map(|(k, v)| (urlencoding::encode(k), urlencoding::encode(v)))
267 .collect::<Vec<(_, _)>>();
268
269 pairs.sort_by(|e1, e2| e1.0.cmp(&e2.0));
270
271 pairs
272 .iter()
273 .map(|(k, v)| if v.is_empty() { k.to_string() } else { format!("{}={}", k, v) })
274 .collect::<Vec<_>>()
275 .join("&")
276 }
277
278 pub(crate) fn build_canonical_headers(&self) -> String {
295 if self.headers.is_empty() {
297 return "".to_string();
298 }
299
300 let mut pairs = self
301 .headers
302 .iter()
303 .map(|(k, v)| (k.to_lowercase(), v))
304 .filter(|(k, _)| k == "content-type" || k == "content-md5" || k.starts_with("x-oss-") || self.additional_headers.contains(k))
305 .collect::<Vec<_>>();
306
307 pairs.sort_by(|a, b| a.0.cmp(&b.0));
308
309 let s = pairs.iter().map(|(k, v)| format!("{}:{}", k, v.trim())).collect::<Vec<_>>().join("\n");
310
311 format!("{}\n", s)
323 }
324
325 pub(crate) fn build_additional_headers(&self) -> String {
326 if self.additional_headers.is_empty() {
327 return "".to_string();
328 }
329
330 let mut keys = self.additional_headers.iter().map(|k| k.to_lowercase()).collect::<Vec<_>>();
331
332 keys.sort();
333
334 keys.join(";")
335 }
336
337 pub(crate) fn build_canonical_request(&self) -> String {
338 let canonical_uri = self.build_canonical_uri();
339 let canonical_query = self.build_canonical_query_string();
340 let canonical_headers = self.build_canonical_headers();
341 let additional_headers = self.build_additional_headers();
342 let method = self.method.to_string();
343
344 format!("{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n{additional_headers}\nUNSIGNED-PAYLOAD")
345 }
346
347 pub(crate) fn build_string_to_sign(&self, region: &str) -> String {
349 let date_time_string = self.headers.get("x-oss-date").unwrap();
350 let date_string = &date_time_string[..8];
351
352 let canonical_request = self.build_canonical_request();
353
354 log::debug!("canonical request: \n--------\n{}\n--------", canonical_request);
355
356 let canonical_request_hash = util::sha256(canonical_request.as_bytes());
357
358 format!(
359 "OSS4-HMAC-SHA256\n{}\n{}/{}/oss/aliyun_v4_request\n{}",
360 date_time_string,
361 date_string,
362 region,
363 hex::encode(&canonical_request_hash)
364 )
365 }
366}