1use 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 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 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 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 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 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}