Skip to main content

aliyun_oss/operations/
object_symlink.rs

1//! Symbolic link operations.
2
3use 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}