opendal/services/swift/
core.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use http::header;
19use http::Request;
20use http::Response;
21use serde::Deserialize;
22use std::fmt::Debug;
23use std::sync::Arc;
24
25use crate::raw::*;
26use crate::*;
27
28pub struct SwiftCore {
29    pub info: Arc<AccessorInfo>,
30    pub root: String,
31    pub endpoint: String,
32    pub container: String,
33    pub token: String,
34}
35
36impl Debug for SwiftCore {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.debug_struct("SwiftCore")
39            .field("root", &self.root)
40            .field("endpoint", &self.endpoint)
41            .field("container", &self.container)
42            .finish_non_exhaustive()
43    }
44}
45
46impl SwiftCore {
47    pub async fn swift_delete(&self, path: &str) -> Result<Response<Buffer>> {
48        let p = build_abs_path(&self.root, path);
49
50        let url = format!(
51            "{}/{}/{}",
52            &self.endpoint,
53            &self.container,
54            percent_encode_path(&p)
55        );
56
57        let mut req = Request::delete(&url);
58
59        req = req.header("X-Auth-Token", &self.token);
60
61        let body = Buffer::new();
62
63        let req = req
64            .extension(Operation::Delete)
65            .body(body)
66            .map_err(new_request_build_error)?;
67
68        self.info.http_client().send(req).await
69    }
70
71    pub async fn swift_list(
72        &self,
73        path: &str,
74        delimiter: &str,
75        limit: Option<usize>,
76        marker: &str,
77    ) -> Result<Response<Buffer>> {
78        let p = build_abs_path(&self.root, path);
79
80        // The delimiter is used to disable recursive listing.
81        // Swift returns a 200 status code when there is no such pseudo directory in prefix.
82        let mut url = QueryPairsWriter::new(&format!("{}/{}/", &self.endpoint, &self.container,))
83            .push("prefix", &percent_encode_path(&p))
84            .push("delimiter", delimiter)
85            .push("format", "json");
86
87        if let Some(limit) = limit {
88            url = url.push("limit", &limit.to_string());
89        }
90        if !marker.is_empty() {
91            url = url.push("marker", marker);
92        }
93
94        let mut req = Request::get(url.finish());
95
96        req = req.header("X-Auth-Token", &self.token);
97
98        let req = req
99            .extension(Operation::List)
100            .body(Buffer::new())
101            .map_err(new_request_build_error)?;
102
103        self.info.http_client().send(req).await
104    }
105
106    pub async fn swift_create_object(
107        &self,
108        path: &str,
109        length: u64,
110        args: &OpWrite,
111        body: Buffer,
112    ) -> Result<Response<Buffer>> {
113        let p = build_abs_path(&self.root, path);
114        let url = format!(
115            "{}/{}/{}",
116            &self.endpoint,
117            &self.container,
118            percent_encode_path(&p)
119        );
120
121        let mut req = Request::put(&url);
122
123        // Set user metadata headers.
124        if let Some(user_metadata) = args.user_metadata() {
125            for (k, v) in user_metadata {
126                req = req.header(format!("X-Object-Meta-{}", k), v);
127            }
128        }
129
130        req = req.header("X-Auth-Token", &self.token);
131        req = req.header(header::CONTENT_LENGTH, length);
132
133        let req = req
134            .extension(Operation::Write)
135            .body(body)
136            .map_err(new_request_build_error)?;
137
138        self.info.http_client().send(req).await
139    }
140
141    pub async fn swift_read(
142        &self,
143        path: &str,
144        range: BytesRange,
145        _arg: &OpRead,
146    ) -> Result<Response<HttpBody>> {
147        let p = build_abs_path(&self.root, path)
148            .trim_end_matches('/')
149            .to_string();
150
151        let url = format!(
152            "{}/{}/{}",
153            &self.endpoint,
154            &self.container,
155            percent_encode_path(&p)
156        );
157
158        let mut req = Request::get(&url);
159
160        req = req.header("X-Auth-Token", &self.token);
161
162        if !range.is_full() {
163            req = req.header(header::RANGE, range.to_header());
164        }
165
166        let req = req
167            .extension(Operation::Read)
168            .body(Buffer::new())
169            .map_err(new_request_build_error)?;
170
171        self.info.http_client().fetch(req).await
172    }
173
174    pub async fn swift_copy(&self, src_p: &str, dst_p: &str) -> Result<Response<Buffer>> {
175        // NOTE: current implementation is limited to same container and root
176
177        let src_p = format!(
178            "/{}/{}",
179            self.container,
180            build_abs_path(&self.root, src_p).trim_end_matches('/')
181        );
182
183        let dst_p = build_abs_path(&self.root, dst_p)
184            .trim_end_matches('/')
185            .to_string();
186
187        let url = format!(
188            "{}/{}/{}",
189            &self.endpoint,
190            &self.container,
191            percent_encode_path(&dst_p)
192        );
193
194        // Request method doesn't support for COPY, we use PUT instead.
195        // Reference: https://docs.openstack.org/api-ref/object-store/#copy-object
196        let mut req = Request::put(&url);
197
198        req = req.header("X-Auth-Token", &self.token);
199        req = req.header("X-Copy-From", percent_encode_path(&src_p));
200
201        // if use PUT method, we need to set the content-length to 0.
202        req = req.header("Content-Length", "0");
203
204        let body = Buffer::new();
205
206        let req = req
207            .extension(Operation::Copy)
208            .body(body)
209            .map_err(new_request_build_error)?;
210
211        self.info.http_client().send(req).await
212    }
213
214    pub async fn swift_get_metadata(&self, path: &str) -> Result<Response<Buffer>> {
215        let p = build_abs_path(&self.root, path);
216
217        let url = format!(
218            "{}/{}/{}",
219            &self.endpoint,
220            &self.container,
221            percent_encode_path(&p)
222        );
223
224        let mut req = Request::head(&url);
225
226        req = req.header("X-Auth-Token", &self.token);
227
228        let req = req
229            .extension(Operation::Stat)
230            .body(Buffer::new())
231            .map_err(new_request_build_error)?;
232
233        self.info.http_client().send(req).await
234    }
235}
236
237#[derive(Debug, Eq, PartialEq, Deserialize)]
238#[serde(untagged)]
239pub enum ListOpResponse {
240    Subdir {
241        subdir: String,
242    },
243    FileInfo {
244        bytes: u64,
245        hash: String,
246        name: String,
247        last_modified: String,
248        content_type: Option<String>,
249    },
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn parse_list_response_test() -> Result<()> {
258        let resp = bytes::Bytes::from(
259            r#"
260            [
261                {
262                    "subdir": "animals/"
263                },
264                {
265                    "subdir": "fruit/"
266                },
267                {
268                    "bytes": 147,
269                    "hash": "5e6b5b70b0426b1cc1968003e1afa5ad",
270                    "name": "test.txt",
271                    "content_type": "text/plain",
272                    "last_modified": "2023-11-01T03:00:23.147480"
273                }
274            ]
275            "#,
276        );
277
278        let mut out = serde_json::from_slice::<Vec<ListOpResponse>>(&resp)
279            .map_err(new_json_deserialize_error)?;
280
281        assert_eq!(out.len(), 3);
282        assert_eq!(
283            out.pop().unwrap(),
284            ListOpResponse::FileInfo {
285                bytes: 147,
286                hash: "5e6b5b70b0426b1cc1968003e1afa5ad".to_string(),
287                name: "test.txt".to_string(),
288                last_modified: "2023-11-01T03:00:23.147480".to_string(),
289                content_type: Some("text/plain".to_string()),
290            }
291        );
292
293        assert_eq!(
294            out.pop().unwrap(),
295            ListOpResponse::Subdir {
296                subdir: "fruit/".to_string()
297            }
298        );
299
300        assert_eq!(
301            out.pop().unwrap(),
302            ListOpResponse::Subdir {
303                subdir: "animals/".to_string()
304            }
305        );
306
307        Ok(())
308    }
309}