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