ali_oss/types/
oss_config.rs

1use crate::SignatureAble;
2use base64::prelude::*;
3use percent_encoding::percent_decode_str;
4use reqwest::{header, Method, Url};
5use std::borrow::Cow;
6
7#[derive(Debug, Clone)]
8pub struct OssConfig {
9	pub access_key_id: String,
10	pub access_key_secret: String,
11	pub bucket_name: String,
12	pub bucket_location: crate::types::BucketLocation,
13	pub path: String,
14	pub is_internal: bool,
15}
16
17impl OssConfig {
18	pub fn from_env() -> anyhow::Result<Self> {
19		let access_key_id = std::env::var("ALI_OSS_ACCESS_KEY_ID")?;
20		let access_key_secret = std::env::var("ALI_OSS_ACCESS_KEY_SECRET")?;
21		let bucket_name = std::env::var("ALI_OSS_BUCKET")?;
22		let bucket_location = std::env::var("ALI_OSS_LOCATION")?;
23		let path = std::env::var("ALI_OSS_PATH").unwrap_or("".to_owned());
24		let internal = std::env::var("ALI_OSS_INTERNAL")?;
25
26		// let bucket = crate::Bucket::new(bucket_name, bucket_location, "".to_owned(), None);
27		let client = Self::new(access_key_id, access_key_secret, bucket_name, bucket_location, path, internal == "true");
28		Ok(client)
29	}
30	pub fn new(access_key_id: String, access_key_secret: String, bucket_name: String, bucket_location: String, path: String, is_internal: bool) -> Self {
31		let path = if path.starts_with("/") { path[1..].to_string() } else { path };
32		let path = if path.ends_with("/") { path[..path.len() - 1].to_string() } else { path };
33		Self {
34			access_key_id: access_key_id.to_string(),
35			access_key_secret: access_key_secret.to_string(),
36			bucket_name: bucket_name.to_string(),
37			bucket_location: crate::types::BucketLocation::new(bucket_location),
38			path,
39			is_internal,
40		}
41	}
42	/// # 返回 endpoint 对应的链接地址
43	/// 可以是内网地址,默认为外网地址
44	/// ```
45	/// # use ali_oss::OssConfig;
46	/// # use reqwest::Url;
47	/// assert_eq!(OssConfig::generate_endpoint_url("oss-cn-hangzhou", true).unwrap(), Url::parse("https://oss-cn-hangzhou-internal.aliyuncs.com").unwrap());
48	/// assert_eq!(OssConfig::generate_endpoint_url("oss-cn-hangzhou", false).unwrap(), Url::parse("https://oss-cn-hangzhou.aliyuncs.com").unwrap());
49	/// ```
50	pub fn generate_endpoint_url(bucket_location: &str, is_internal: bool) -> anyhow::Result<Url> {
51		let src = format!("https://{}{}.aliyuncs.com", bucket_location, if is_internal { "-internal" } else { "" });
52		Ok(Url::parse(&src)?)
53	}
54	/// # 返回 bucket 对应的链接地址
55	/// 可以是内网地址,默认为外网地址
56	/// ```
57	/// # use ali_oss::OssConfig;
58	/// # use reqwest::Url;
59	/// assert_eq!(OssConfig::generate_bucket_url("hello", "oss-cn-hangzhou", true).unwrap(), Url::parse("https://hello.oss-cn-hangzhou-internal.aliyuncs.com").unwrap());
60	/// assert_eq!(OssConfig::generate_bucket_url("hello", "oss-cn-hangzhou", false).unwrap(), Url::parse("https://hello.oss-cn-hangzhou.aliyuncs.com").unwrap());
61	/// ```
62	pub fn generate_bucket_url(backet_name: &str, bucket_location: &str, is_internal: bool) -> anyhow::Result<Url> {
63		let src = format!("https://{}.{}{}.aliyuncs.com", backet_name, bucket_location, if is_internal { "-internal" } else { "" });
64		Ok(Url::parse(&src)?)
65	}
66}
67
68impl OssConfig {
69	pub fn get_object_name<'a>(&self, object_name: &'a str) -> Cow<'a, str> {
70		if self.path.is_empty() {
71			if object_name.starts_with("/") {
72				object_name[1..].into()
73			} else {
74				object_name.into()
75			}
76		} else {
77			format!("{}/{}", self.path, if object_name.starts_with("/") { &object_name[1..] } else { object_name }).into()
78		}
79	}
80	pub fn get_encoded_object_name<'a>(&self, object_name: &'a str) -> Cow<'a, str> {
81		let object_name = self.get_object_name(object_name);
82		let url = Url::parse(format!("https://localhost/{}", object_name).as_str()).unwrap();
83		url.path().trim_start_matches("/").to_owned().into()
84	}
85	pub fn get_decoded_object_name<'a>(&self, object_name: &'a str) -> Cow<'a, str> {
86		let object_name = self.get_object_name(object_name);
87		decode_if_encoded(object_name.as_ref()).into()
88	}
89
90	pub fn get_endpoint_url(&self) -> anyhow::Result<Url> {
91		Self::generate_endpoint_url(&self.bucket_location.as_str(), self.is_internal)
92	}
93
94	pub fn get_endpoint_request(&self, method: Method) -> anyhow::Result<reqwest::Request> {
95		let url = self.get_endpoint_url()?;
96		let request = reqwest::Request::new(method, url);
97		// request.headers_mut().insert(CONTENT_TYPE, "application/xml".try_into()?);
98		Ok(request)
99	}
100
101	pub fn get_bucket_url(&self) -> anyhow::Result<Url> {
102		Self::generate_bucket_url(&self.bucket_name, &self.bucket_location.as_str(), self.is_internal)
103	}
104
105	pub fn get_bucket_request(&self, method: Method, body: Option<bytes::Bytes>) -> anyhow::Result<reqwest::Request> {
106		let url = self.get_bucket_url()?;
107		let mut request = reqwest::Request::new(method, url);
108		if let Some(body) = body {
109			match infer::get(&body) {
110				Some(mime) => {
111					request.headers_mut().insert(header::CONTENT_TYPE, mime.mime_type().try_into()?);
112				}
113				None => {
114					request.headers_mut().insert(header::CONTENT_TYPE, "text/plain".try_into()?);
115				}
116			}
117			// 计算md5
118			request.headers_mut().insert("Content-MD5", {
119				let md5_hash = md5::compute(&body);
120				base64::engine::general_purpose::STANDARD.encode(md5_hash.as_slice()).try_into()?
121			});
122			request.headers_mut().insert(header::CONTENT_LENGTH, body.len().try_into()?);
123			*request.body_mut() = Some(reqwest::Body::from(body));
124		}
125		// request.headers_mut().insert(header::CONTENT_TYPE, "text/plain".try_into()?);
126		Ok(request)
127	}
128
129	pub(crate) fn sign_header_request(&self, request: &mut reqwest::Request) -> anyhow::Result<()> {
130		let content_md5 = {
131			let content_md5 = request.headers().get("Content-MD5");
132			if let Some(content_md5) = content_md5 {
133				Some(content_md5.to_str()?.to_owned())
134			} else {
135				None
136			}
137		};
138		let content_type = {
139			let content_type = request.headers().get("Content-Type");
140			if let Some(content_type) = content_type {
141				Some(content_type.to_str()?.to_owned())
142			} else {
143				None
144			}
145		};
146		let canonicalized_oss_headers: crate::types::CanonicalizedHeaders = (&*request).into();
147		let canonicalized_resource = {
148			let host = request.url().host_str().ok_or(anyhow::anyhow!("host not found"))?;
149			if host.starts_with(self.bucket_location.as_str()) {
150				crate::types::CanonicalizedResource::default()
151			} else {
152				let path = decode_if_encoded(request.url().path()); // decode_if_encoded 解决路径带中文问题
153				let query = request.url().query();
154				let mut resource = format!("/{}{}", self.bucket_name, path);
155				if let Some(query) = query {
156					if let Some(first_query) = query.split('&').next() {
157						if !first_query.contains("=") {
158							resource.push_str(&format!("?{}", query));
159						}
160					}
161				}
162				crate::types::CanonicalizedResource::new(resource)
163			}
164		};
165		let header_signature = crate::types::HeaderSignature::new(request.method().clone(), content_md5, content_type, chrono::Utc::now(), canonicalized_oss_headers, canonicalized_resource);
166		let signatured_string = header_signature.get_signature_string(self);
167		let authorization = format!("OSS {}:{}", self.access_key_id, signatured_string);
168		request.headers_mut().insert("Authorization", authorization.try_into()?);
169		request.headers_mut().insert("Date", header_signature.get_date_string().try_into()?);
170		Ok(())
171	}
172	pub(crate) fn get_request_builder(&self, request: reqwest::Request) -> anyhow::Result<reqwest::RequestBuilder> {
173		let req_client = reqwest::Client::new();
174		Ok(reqwest::RequestBuilder::from_parts(req_client, request))
175	}
176}
177
178fn decode_if_encoded(input: &str) -> String {
179	// 尝试解码 URL 编码的字符串
180	let decoded = percent_decode_str(input).decode_utf8();
181	match decoded {
182		Ok(decoded_str) => decoded_str.to_string(), // 解码成功,返回解码后的字符串
183		Err(_) => input.to_string(),                // 解码失败,返回原始字符串
184	}
185}