1use std::sync::Arc;
4
5use serde::Deserialize;
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::acl::ObjectAcl;
11use crate::types::bucket::BucketName;
12use crate::types::object::ObjectKey;
13use crate::types::storage::StorageClass;
14use crate::util::uri::oss_endpoint_url;
15
16#[derive(Debug, Clone, Deserialize)]
17#[serde(rename = "SymlinkResult")]
18struct SymlinkResult {
19 #[serde(rename = "SymlinkTarget")]
20 target: String,
21 #[serde(rename = "ETag")]
22 etag: String,
23}
24
25pub struct PutSymlinkBuilder {
26 client: Arc<OSSClientInner>,
27 key: ObjectKey,
28 bucket: BucketName,
29 target: String,
30 acl: Option<ObjectAcl>,
31 storage_class: Option<StorageClass>,
32 metadata: Vec<(String, String)>,
33}
34
35impl PutSymlinkBuilder {
36 pub(crate) fn new(
37 client: Arc<OSSClientInner>,
38 bucket: BucketName,
39 key: ObjectKey,
40 target: impl Into<String>,
41 ) -> Self {
42 Self {
43 client,
44 bucket,
45 key,
46 target: target.into(),
47 acl: None,
48 storage_class: None,
49 metadata: Vec::new(),
50 }
51 }
52
53 pub fn acl(mut self, acl: ObjectAcl) -> Self {
54 self.acl = Some(acl);
55 self
56 }
57 pub fn storage_class(mut self, sc: StorageClass) -> Self {
58 self.storage_class = Some(sc);
59 self
60 }
61 pub fn metadata(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
62 self.metadata.push((k.into(), v.into()));
63 self
64 }
65
66 pub async fn send(self) -> Result<PutSymlinkOutput> {
67 let endpoint = self.client.endpoint.clone();
68 let uri = oss_endpoint_url(
69 &endpoint,
70 Some(self.bucket.as_str()),
71 Some(self.key.as_str()),
72 );
73 let full_uri = format!("{}?symlink", uri);
74 let query_params: Vec<(String, String)> = vec![("symlink".into(), String::new())];
75
76 let mut req = HttpRequest::builder()
77 .method(http::Method::PUT)
78 .uri(&full_uri);
79 req = req.header(
80 http::HeaderName::from_static("x-oss-symlink-target"),
81 http::HeaderValue::from_str(&self.target).map_err(|e| OssError {
82 kind: OssErrorKind::ValidationError,
83 context: Box::new(ErrorContext {
84 operation: Some("set symlink target".into()),
85 bucket: Some(self.bucket.to_string()),
86 object_key: Some(self.key.to_string()),
87 ..Default::default()
88 }),
89 source: Some(Box::new(e)),
90 })?,
91 );
92
93 if let Some(acl) = self.acl {
94 req = req.header(
95 http::HeaderName::from_static("x-oss-object-acl"),
96 http::HeaderValue::from_str(acl.as_str()).map_err(|e| OssError {
97 kind: OssErrorKind::ValidationError,
98 context: Box::new(ErrorContext {
99 operation: Some("set acl".into()),
100 bucket: Some(self.bucket.to_string()),
101 object_key: Some(self.key.to_string()),
102 ..Default::default()
103 }),
104 source: Some(Box::new(e)),
105 })?,
106 );
107 }
108 if let Some(sc) = self.storage_class {
109 req = req.header(
110 http::HeaderName::from_static("x-oss-storage-class"),
111 http::HeaderValue::from_str(sc.as_str()).map_err(|e| OssError {
112 kind: OssErrorKind::ValidationError,
113 context: Box::new(ErrorContext {
114 operation: Some("set storage class".into()),
115 bucket: Some(self.bucket.to_string()),
116 object_key: Some(self.key.to_string()),
117 ..Default::default()
118 }),
119 source: Some(Box::new(e)),
120 })?,
121 );
122 }
123
124 let response = self
125 .client
126 .send_signed(req.build(), Some(&self.bucket), query_params)
127 .await
128 .map_err(|e| OssError {
129 kind: OssErrorKind::TransportError,
130 context: Box::new(ErrorContext {
131 operation: Some("PutSymlink".into()),
132 bucket: Some(self.bucket.to_string()),
133 object_key: Some(self.key.to_string()),
134 endpoint: Some(endpoint),
135 ..Default::default()
136 }),
137 source: Some(Box::new(e)),
138 })?;
139
140 if response.is_success() {
141 Ok(PutSymlinkOutput {
142 request_id: response
143 .headers
144 .get("x-oss-request-id")
145 .and_then(|v| v.to_str().ok())
146 .unwrap_or("")
147 .to_string(),
148 })
149 } else {
150 Err(OssError {
151 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
152 status_code: response.status().as_u16(),
153 code: String::new(),
154 message: String::new(),
155 request_id: String::new(),
156 host_id: String::new(),
157 resource: Some(self.key.to_string()),
158 string_to_sign: None,
159 })),
160 context: Box::new(ErrorContext {
161 operation: Some("PutSymlink".into()),
162 bucket: Some(self.bucket.to_string()),
163 object_key: Some(self.key.to_string()),
164 ..Default::default()
165 }),
166 source: None,
167 })
168 }
169 }
170}
171
172#[derive(Debug, Clone)]
173pub struct PutSymlinkOutput {
174 pub request_id: String,
175}
176
177pub struct GetSymlinkBuilder {
178 client: Arc<OSSClientInner>,
179 bucket: BucketName,
180 key: ObjectKey,
181}
182
183impl GetSymlinkBuilder {
184 pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, key: ObjectKey) -> Self {
185 Self {
186 client,
187 bucket,
188 key,
189 }
190 }
191
192 pub async fn send(self) -> Result<GetSymlinkOutput> {
193 let endpoint = self.client.endpoint.clone();
194 let uri = oss_endpoint_url(
195 &endpoint,
196 Some(self.bucket.as_str()),
197 Some(self.key.as_str()),
198 );
199 let full_uri = format!("{}?symlink", uri);
200 let query_params: Vec<(String, String)> = vec![("symlink".into(), String::new())];
201
202 let request = HttpRequest::builder()
203 .method(http::Method::GET)
204 .uri(&full_uri)
205 .build();
206
207 let response = self
208 .client
209 .send_signed(request, Some(&self.bucket), query_params)
210 .await
211 .map_err(|e| OssError {
212 kind: OssErrorKind::TransportError,
213 context: Box::new(ErrorContext {
214 operation: Some("GetSymlink".into()),
215 bucket: Some(self.bucket.to_string()),
216 object_key: Some(self.key.to_string()),
217 endpoint: Some(endpoint),
218 ..Default::default()
219 }),
220 source: Some(Box::new(e)),
221 })?;
222
223 if response.is_success() {
224 let body_str = response.body_as_str().unwrap_or("");
225 let result: SymlinkResult =
226 crate::util::xml::from_xml(body_str).map_err(|e| OssError {
227 kind: OssErrorKind::DeserializationError,
228 context: Box::new(ErrorContext {
229 operation: Some("GetSymlink: parse XML".into()),
230 bucket: Some(self.bucket.to_string()),
231 object_key: Some(self.key.to_string()),
232 ..Default::default()
233 }),
234 source: Some(Box::new(e)),
235 })?;
236
237 Ok(GetSymlinkOutput {
238 target: result.target,
239 etag: result.etag.trim_matches('"').to_string(),
240 })
241 } else {
242 Err(OssError {
243 kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
244 status_code: response.status().as_u16(),
245 code: String::new(),
246 message: String::new(),
247 request_id: String::new(),
248 host_id: String::new(),
249 resource: Some(self.key.to_string()),
250 string_to_sign: None,
251 })),
252 context: Box::new(ErrorContext {
253 operation: Some("GetSymlink".into()),
254 bucket: Some(self.bucket.to_string()),
255 object_key: Some(self.key.to_string()),
256 ..Default::default()
257 }),
258 source: None,
259 })
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
265pub struct GetSymlinkOutput {
266 pub target: String,
267 pub etag: String,
268}
269
270impl BucketOperations {
271 pub fn put_symlink(
272 &self,
273 key: impl Into<String>,
274 target: impl Into<String>,
275 ) -> Result<PutSymlinkBuilder> {
276 Ok(PutSymlinkBuilder::new(
277 self.client_inner().clone(),
278 self.bucket_name().clone(),
279 ObjectKey::new(key.into())?,
280 target,
281 ))
282 }
283 pub fn get_symlink(&self, key: impl Into<String>) -> Result<GetSymlinkBuilder> {
284 Ok(GetSymlinkBuilder::new(
285 self.client_inner().clone(),
286 self.bucket_name().clone(),
287 ObjectKey::new(key.into())?,
288 ))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::client::OSSClientInner;
296 use crate::config::credentials::Credentials;
297 use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
298 use crate::types::region::Region;
299 use std::sync::Mutex;
300
301 struct RecordingHttpClient {
302 requests: Arc<Mutex<Vec<HttpRequest>>>,
303 status_code: http::StatusCode,
304 response_body: bytes::Bytes,
305 }
306 #[async_trait::async_trait]
307 impl HttpClient for RecordingHttpClient {
308 async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
309 self.requests.lock().unwrap().push(request);
310 let mut headers = http::HeaderMap::new();
311 headers.insert(
312 "x-oss-request-id",
313 http::HeaderValue::from_static("rid-symlink"),
314 );
315 Ok(HttpResponse {
316 status: self.status_code,
317 headers,
318 body: self.response_body.clone(),
319 })
320 }
321 }
322
323 fn create_inner(body: bytes::Bytes) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
324 let requests = Arc::new(Mutex::new(Vec::new()));
325 let http = Arc::new(RecordingHttpClient {
326 requests: requests.clone(),
327 status_code: http::StatusCode::OK,
328 response_body: body,
329 });
330 let creds = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
331 Credentials::builder()
332 .access_key_id("ak")
333 .access_key_secret("sk")
334 .build()
335 .unwrap(),
336 ));
337 (
338 Arc::new(OSSClientInner {
339 http,
340 credentials: creds,
341 signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
342 region: Region::CnHangzhou,
343 endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
344 }),
345 requests,
346 )
347 }
348
349 #[tokio::test]
350 async fn put_symlink_sets_target_header() {
351 let (inner, requests) = create_inner(bytes::Bytes::new());
352 let b = PutSymlinkBuilder::new(
353 inner,
354 BucketName::new("test-bucket").unwrap(),
355 ObjectKey::new("link").unwrap(),
356 "target.txt",
357 );
358 b.send().await.unwrap();
359 let c = requests.lock().unwrap();
360 assert_eq!(c[0].method, http::Method::PUT);
361 assert!(c[0].uri.contains("?symlink"));
362 }
363
364 #[tokio::test]
365 async fn get_symlink_parses_xml() {
366 let xml = r#"<?xml version="1.0"?><SymlinkResult><SymlinkTarget>target.txt</SymlinkTarget><ETag>"abc"</ETag></SymlinkResult>"#;
367 let (inner, _) = create_inner(bytes::Bytes::from(xml));
368 let b = GetSymlinkBuilder::new(
369 inner,
370 BucketName::new("test-bucket").unwrap(),
371 ObjectKey::new("link").unwrap(),
372 );
373 let o = b.send().await.unwrap();
374 assert_eq!(o.target, "target.txt");
375 }
376
377 #[tokio::test]
378 #[ignore = "requires valid OSS credentials"]
379 async fn e2e_symlink() {
380 let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
381 let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
382 let rs = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
383 let bs = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
384 let region = std::str::FromStr::from_str(&rs).unwrap_or_else(|_| Region::Custom {
385 endpoint: format!("oss-{}.aliyuncs.com", rs),
386 region_id: rs.clone(),
387 });
388 let client = crate::client::OSSClient::builder()
389 .region(region)
390 .credentials(ak, sk)
391 .build()
392 .unwrap();
393 let key = format!("symlink-{}.txt", chrono::Utc::now().timestamp());
394 let b = client.bucket(&bs).unwrap();
395 b.put_object(&key)
396 .unwrap()
397 .body(bytes::Bytes::from("target"))
398 .send()
399 .await
400 .unwrap();
401 let o = b
402 .put_symlink(format!("link-{}", chrono::Utc::now().timestamp()), &key)
403 .unwrap()
404 .send()
405 .await
406 .unwrap();
407 eprintln!("PutSymlink: rid={}", o.request_id);
408 }
409}