Skip to main content

opendal_service_pcloud/
backend.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 bytes::Buf;
22use http::StatusCode;
23use log::debug;
24use opendal_core::raw::*;
25use opendal_core::*;
26
27use super::PCLOUD_SCHEME;
28use super::config::PcloudConfig;
29use super::core::PcloudError;
30use super::core::parse_error;
31use super::core::*;
32use super::deleter::PcloudDeleter;
33use super::lister::PcloudLister;
34use super::reader::*;
35use super::writer::PcloudWriter;
36use super::writer::PcloudWriters;
37
38/// [pCloud](https://www.pcloud.com/) services support.
39#[doc = include_str!("docs.md")]
40#[derive(Default)]
41pub struct PcloudBuilder {
42    pub(super) config: PcloudConfig,
43}
44
45impl Debug for PcloudBuilder {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("PcloudBuilder")
48            .field("config", &self.config)
49            .finish_non_exhaustive()
50    }
51}
52
53impl PcloudBuilder {
54    /// Set root of this backend.
55    ///
56    /// All operations will happen under this root.
57    pub fn root(mut self, root: &str) -> Self {
58        self.config.root = if root.is_empty() {
59            None
60        } else {
61            Some(root.to_string())
62        };
63
64        self
65    }
66
67    /// Pcloud endpoint.
68    /// <https://api.pcloud.com> for United States and <https://eapi.pcloud.com> for Europe
69    /// ref to [doc.pcloud.com](https://docs.pcloud.com/)
70    ///
71    /// It is required. e.g. `https://api.pcloud.com`
72    pub fn endpoint(mut self, endpoint: &str) -> Self {
73        self.config.endpoint = endpoint.to_string();
74
75        self
76    }
77
78    /// Pcloud username.
79    ///
80    /// It is required. your pCloud login email, e.g. `example@gmail.com`
81    pub fn username(mut self, username: &str) -> Self {
82        self.config.username = if username.is_empty() {
83            None
84        } else {
85            Some(username.to_string())
86        };
87
88        self
89    }
90
91    /// Pcloud password.
92    ///
93    /// It is required. your pCloud login password, e.g. `password`
94    pub fn password(mut self, password: &str) -> Self {
95        self.config.password = if password.is_empty() {
96            None
97        } else {
98            Some(password.to_string())
99        };
100
101        self
102    }
103}
104
105impl Builder for PcloudBuilder {
106    type Config = PcloudConfig;
107
108    /// Builds the backend and returns the result of PcloudBackend.
109    fn build(self) -> Result<impl Service> {
110        debug!("backend build started: {:?}", self);
111
112        let root = normalize_root(&self.config.root.clone().unwrap_or_default());
113        debug!("backend use root {}", root);
114
115        // Handle endpoint.
116        if self.config.endpoint.is_empty() {
117            return Err(Error::new(ErrorKind::ConfigInvalid, "endpoint is empty")
118                .with_operation("Builder::build")
119                .with_context("service", PCLOUD_SCHEME));
120        }
121
122        debug!("backend use endpoint {}", self.config.endpoint);
123
124        let username = match &self.config.username {
125            Some(username) => Ok(username.clone()),
126            None => Err(Error::new(ErrorKind::ConfigInvalid, "username is empty")
127                .with_operation("Builder::build")
128                .with_context("service", PCLOUD_SCHEME)),
129        }?;
130
131        let password = match &self.config.password {
132            Some(password) => Ok(password.clone()),
133            None => Err(Error::new(ErrorKind::ConfigInvalid, "password is empty")
134                .with_operation("Builder::build")
135                .with_context("service", PCLOUD_SCHEME)),
136        }?;
137
138        Ok(PcloudBackend {
139            core: Arc::new(PcloudCore {
140                info: ServiceInfo::new(PCLOUD_SCHEME, &root, ""),
141                capability: Capability {
142                    stat: true,
143
144                    create_dir: true,
145
146                    read: true,
147                    read_with_suffix: true,
148
149                    write: true,
150
151                    delete: true,
152                    rename: true,
153                    copy: true,
154
155                    list: true,
156
157                    shared: true,
158
159                    ..Default::default()
160                },
161                root,
162                endpoint: self.config.endpoint.clone(),
163                username,
164                password,
165            }),
166        })
167    }
168}
169
170/// Backend for Pcloud services.
171#[derive(Debug, Clone)]
172pub struct PcloudBackend {
173    pub(crate) core: Arc<PcloudCore>,
174}
175
176impl Service for PcloudBackend {
177    type Reader = oio::StreamReader<PcloudReader>;
178    type Writer = PcloudWriters;
179    type Lister = oio::PageLister<PcloudLister>;
180    type Deleter = oio::OneShotDeleter<PcloudDeleter>;
181    type Copier = oio::OneShotCopier;
182
183    fn info(&self) -> ServiceInfo {
184        self.core.info.clone()
185    }
186
187    fn capability(&self) -> Capability {
188        self.core.capability
189    }
190
191    async fn create_dir(
192        &self,
193        ctx: &OperationContext,
194        path: &str,
195        _: OpCreateDir,
196    ) -> Result<RpCreateDir> {
197        self.core.ensure_dir_exists(ctx, path).await?;
198        Ok(RpCreateDir::default())
199    }
200
201    async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result<RpStat> {
202        let resp = self.core.stat(ctx, path).await?;
203
204        let status = resp.status();
205
206        match status {
207            StatusCode::OK => {
208                let bs = resp.into_body();
209                let resp: StatResponse =
210                    serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
211                let result = resp.result;
212                if result == 2010 || result == 2055 || result == 2002 {
213                    return Err(Error::new(ErrorKind::NotFound, format!("{resp:?}")));
214                }
215                if result != 0 {
216                    return Err(Error::new(ErrorKind::Unexpected, format!("{resp:?}")));
217                }
218
219                if let Some(md) = resp.metadata {
220                    let md = parse_stat_metadata(md);
221                    return md.map(RpStat::new);
222                }
223
224                Err(Error::new(ErrorKind::Unexpected, format!("{resp:?}")))
225            }
226            _ => Err(parse_error(resp)),
227        }
228    }
229    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
230        let output: oio::StreamReader<PcloudReader> = {
231            Ok(oio::StreamReader::new(PcloudReader::new(
232                self.clone(),
233                ctx.clone(),
234                path,
235                args,
236            )))
237        }?;
238
239        Ok(output)
240    }
241
242    fn write(&self, ctx: &OperationContext, path: &str, _args: OpWrite) -> Result<Self::Writer> {
243        let output: PcloudWriters = {
244            let writer = PcloudWriter::new(self.core.clone(), ctx.clone(), path.to_string());
245
246            let w = oio::OneShotWriter::new(writer);
247
248            Ok(w)
249        }?;
250
251        Ok(output)
252    }
253
254    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
255        let output: oio::OneShotDeleter<PcloudDeleter> = {
256            Ok(oio::OneShotDeleter::new(PcloudDeleter::new(
257                self.core.clone(),
258                ctx.clone(),
259            )))
260        }?;
261
262        Ok(output)
263    }
264
265    fn list(&self, ctx: &OperationContext, path: &str, _args: OpList) -> Result<Self::Lister> {
266        let output: oio::PageLister<PcloudLister> = {
267            let l = PcloudLister::new(self.core.clone(), ctx.clone(), path);
268            Ok(oio::PageLister::new(l))
269        }?;
270
271        Ok(output)
272    }
273
274    fn copy(
275        &self,
276        ctx: &OperationContext,
277        from: &str,
278        to: &str,
279        _args: OpCopy,
280        _opts: OpCopier,
281    ) -> Result<Self::Copier> {
282        let core = self.core.clone();
283        let ctx = ctx.clone();
284        let from = from.to_string();
285        let to = to.to_string();
286
287        Ok(oio::OneShotCopier::new(async move {
288            core.ensure_dir_exists(&ctx, &to).await?;
289
290            let resp = if from.ends_with('/') {
291                core.copy_folder(&ctx, &from, &to).await?
292            } else {
293                core.copy_file(&ctx, &from, &to).await?
294            };
295
296            let status = resp.status();
297
298            match status {
299                StatusCode::OK => {
300                    let bs = resp.into_body();
301                    let resp: PcloudError =
302                        serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
303                    let result = resp.result;
304                    if result == 2009 || result == 2010 || result == 2055 || result == 2002 {
305                        Err(Error::new(ErrorKind::NotFound, format!("{resp:?}")))
306                    } else if result != 0 {
307                        Err(Error::new(ErrorKind::Unexpected, format!("{resp:?}")))
308                    } else {
309                        Ok(Metadata::default())
310                    }
311                }
312                _ => Err(parse_error(resp)),
313            }
314        }))
315    }
316
317    async fn rename(
318        &self,
319        ctx: &OperationContext,
320        from: &str,
321        to: &str,
322        _args: OpRename,
323    ) -> Result<RpRename> {
324        self.core.ensure_dir_exists(ctx, to).await?;
325
326        let resp = if from.ends_with('/') {
327            self.core.rename_folder(ctx, from, to).await?
328        } else {
329            self.core.rename_file(ctx, from, to).await?
330        };
331
332        let status = resp.status();
333
334        match status {
335            StatusCode::OK => {
336                let bs = resp.into_body();
337                let resp: PcloudError =
338                    serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
339                let result = resp.result;
340                if result == 2009 || result == 2010 || result == 2055 || result == 2002 {
341                    return Err(Error::new(ErrorKind::NotFound, format!("{resp:?}")));
342                }
343                if result != 0 {
344                    return Err(Error::new(ErrorKind::Unexpected, format!("{resp:?}")));
345                }
346
347                Ok(RpRename::default())
348            }
349            _ => Err(parse_error(resp)),
350        }
351    }
352
353    async fn presign(
354        &self,
355        _ctx: &OperationContext,
356        _path: &str,
357        _args: OpPresign,
358    ) -> Result<RpPresign> {
359        Err(Error::new(
360            ErrorKind::Unsupported,
361            "operation is not supported",
362        ))
363    }
364}