Skip to main content

opendal_service_dropbox/
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::sync::Arc;
19
20use bytes::Buf;
21use http::StatusCode;
22
23use super::core::*;
24use super::deleter::DropboxDeleter;
25use super::lister::DropboxLister;
26use super::reader::*;
27use super::writer::DropboxWriter;
28use opendal_core::raw::*;
29use opendal_core::*;
30
31use std::fmt::Debug;
32
33use mea::mutex::Mutex;
34
35use super::DROPBOX_SCHEME;
36use super::config::DropboxConfig;
37use super::core::DropboxCore;
38use super::core::DropboxSigner;
39
40/// [Dropbox](https://www.dropbox.com/) backend support.
41#[doc = include_str!("docs.md")]
42#[derive(Default)]
43pub struct DropboxBuilder {
44    pub(super) config: DropboxConfig,
45}
46
47impl Debug for DropboxBuilder {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("Builder")
50            .field("config", &self.config)
51            .finish_non_exhaustive()
52    }
53}
54
55impl DropboxBuilder {
56    /// Set the root directory for dropbox.
57    ///
58    /// Default to `/` if not set.
59    pub fn root(mut self, root: &str) -> Self {
60        self.config.root = if root.is_empty() {
61            None
62        } else {
63            Some(root.to_string())
64        };
65
66        self
67    }
68
69    /// Access token is used for temporary access to the Dropbox API.
70    ///
71    /// You can get the access token from [Dropbox App Console](https://www.dropbox.com/developers/apps)
72    ///
73    /// NOTE: this token will be expired in 4 hours.
74    /// If you are trying to use the Dropbox service in a long time, please set a refresh_token instead.
75    pub fn access_token(mut self, access_token: &str) -> Self {
76        self.config.access_token = Some(access_token.to_string());
77        self
78    }
79
80    /// Refresh token is used for long term access to the Dropbox API.
81    ///
82    /// You can get the refresh token via OAuth 2.0 Flow of Dropbox.
83    ///
84    /// OpenDAL will use this refresh token to get a new access token when the old one is expired.
85    pub fn refresh_token(mut self, refresh_token: &str) -> Self {
86        self.config.refresh_token = Some(refresh_token.to_string());
87        self
88    }
89
90    /// Set the client id for Dropbox.
91    ///
92    /// This is required for OAuth 2.0 Flow to refresh the access token.
93    pub fn client_id(mut self, client_id: &str) -> Self {
94        self.config.client_id = Some(client_id.to_string());
95        self
96    }
97
98    /// Set the client secret for Dropbox.
99    ///
100    /// This is required for OAuth 2.0 Flow with refresh the access token.
101    pub fn client_secret(mut self, client_secret: &str) -> Self {
102        self.config.client_secret = Some(client_secret.to_string());
103        self
104    }
105}
106
107impl Builder for DropboxBuilder {
108    type Config = DropboxConfig;
109
110    fn build(self) -> Result<impl Service> {
111        let root = normalize_root(&self.config.root.unwrap_or_default());
112
113        let signer = match (self.config.access_token, self.config.refresh_token) {
114            (Some(access_token), None) => DropboxSigner {
115                access_token,
116                // We will never expire user specified token.
117                expires_in: Timestamp::MAX,
118                ..Default::default()
119            },
120            (None, Some(refresh_token)) => {
121                let client_id = self.config.client_id.ok_or_else(|| {
122                    Error::new(
123                        ErrorKind::ConfigInvalid,
124                        "client_id must be set when refresh_token is set",
125                    )
126                    .with_context("service", DROPBOX_SCHEME)
127                })?;
128                let client_secret = self.config.client_secret.ok_or_else(|| {
129                    Error::new(
130                        ErrorKind::ConfigInvalid,
131                        "client_secret must be set when refresh_token is set",
132                    )
133                    .with_context("service", DROPBOX_SCHEME)
134                })?;
135
136                DropboxSigner {
137                    refresh_token,
138                    client_id,
139                    client_secret,
140                    ..Default::default()
141                }
142            }
143            (Some(_), Some(_)) => {
144                return Err(Error::new(
145                    ErrorKind::ConfigInvalid,
146                    "access_token and refresh_token can not be set at the same time",
147                )
148                .with_context("service", DROPBOX_SCHEME));
149            }
150            (None, None) => {
151                return Err(Error::new(
152                    ErrorKind::ConfigInvalid,
153                    "access_token or refresh_token must be set",
154                )
155                .with_context("service", DROPBOX_SCHEME));
156            }
157        };
158
159        Ok(DropboxBackend {
160            core: Arc::new(DropboxCore {
161                info: ServiceInfo::new(DROPBOX_SCHEME, &root, ""),
162                capability: Capability {
163                    stat: true,
164
165                    read: true,
166                    read_with_suffix: true,
167
168                    write: true,
169
170                    create_dir: true,
171
172                    delete: true,
173
174                    list: true,
175                    list_with_recursive: true,
176
177                    copy: true,
178
179                    rename: true,
180
181                    shared: true,
182
183                    ..Default::default()
184                },
185                root,
186                signer: Arc::new(Mutex::new(signer)),
187            }),
188        })
189    }
190}
191
192#[derive(Clone, Debug)]
193pub struct DropboxBackend {
194    pub core: Arc<DropboxCore>,
195}
196
197impl Service for DropboxBackend {
198    type Reader = oio::StreamReader<DropboxReader>;
199    type Writer = oio::OneShotWriter<DropboxWriter>;
200    type Lister = oio::PageLister<DropboxLister>;
201    type Deleter = oio::OneShotDeleter<DropboxDeleter>;
202    type Copier = oio::OneShotCopier;
203
204    fn info(&self) -> ServiceInfo {
205        self.core.info.clone()
206    }
207
208    fn capability(&self) -> Capability {
209        self.core.capability
210    }
211
212    async fn create_dir(
213        &self,
214        ctx: &OperationContext,
215        path: &str,
216        _args: OpCreateDir,
217    ) -> Result<RpCreateDir> {
218        // Check if the folder already exists.
219        let resp = self.core.dropbox_get_metadata(ctx, path).await?;
220        if StatusCode::OK == resp.status() {
221            let bytes = resp.into_body();
222            let decoded_response: DropboxMetadataResponse =
223                serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
224            if "folder" == decoded_response.tag {
225                return Ok(RpCreateDir::default());
226            }
227            if "file" == decoded_response.tag {
228                return Err(Error::new(
229                    ErrorKind::NotADirectory,
230                    format!("it's not a directory {path}"),
231                ));
232            }
233        }
234
235        let res = self.core.dropbox_create_folder(ctx, path).await?;
236        Ok(res)
237    }
238
239    async fn stat(&self, ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
240        let resp = self.core.dropbox_get_metadata(ctx, path).await?;
241        let status = resp.status();
242        match status {
243            StatusCode::OK => {
244                let bytes = resp.into_body();
245                let decoded_response: DropboxMetadataResponse =
246                    serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
247                let entry_mode: EntryMode = match decoded_response.tag.as_str() {
248                    "file" => EntryMode::FILE,
249                    "folder" => EntryMode::DIR,
250                    _ => EntryMode::Unknown,
251                };
252
253                let mut metadata = Metadata::new(entry_mode);
254                // Only set last_modified and size if entry_mode is FILE, because Dropbox API
255                // returns last_modified and size only for files.
256                // FYI: https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
257                if entry_mode == EntryMode::FILE {
258                    let date_utc_last_modified =
259                        decoded_response.client_modified.parse::<Timestamp>()?;
260                    metadata.set_last_modified(date_utc_last_modified);
261
262                    if let Some(size) = decoded_response.size {
263                        metadata.set_content_length(size);
264                    } else {
265                        return Err(Error::new(
266                            ErrorKind::Unexpected,
267                            format!("no size found for file {path}"),
268                        ));
269                    }
270                }
271                Ok(RpStat::new(metadata))
272            }
273            _ => Err(parse_error(resp)),
274        }
275    }
276    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
277        let output: oio::StreamReader<DropboxReader> = {
278            Ok(oio::StreamReader::new(DropboxReader::new(
279                self.clone(),
280                ctx.clone(),
281                path,
282                args,
283            )))
284        }?;
285
286        Ok(output)
287    }
288
289    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
290        let output: oio::OneShotWriter<DropboxWriter> = {
291            Ok(oio::OneShotWriter::new(DropboxWriter::new(
292                self.core.clone(),
293                ctx.clone(),
294                args,
295                String::from(path),
296            )))
297        }?;
298
299        Ok(output)
300    }
301
302    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
303        let output: oio::OneShotDeleter<DropboxDeleter> = {
304            Ok(oio::OneShotDeleter::new(DropboxDeleter::new(
305                self.core.clone(),
306                ctx.clone(),
307            )))
308        }?;
309
310        Ok(output)
311    }
312
313    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
314        let output: oio::PageLister<DropboxLister> = {
315            Ok(oio::PageLister::new(DropboxLister::new(
316                self.core.clone(),
317                ctx.clone(),
318                path.to_string(),
319                args.recursive(),
320                args.limit(),
321            )))
322        }?;
323
324        Ok(output)
325    }
326
327    fn copy(
328        &self,
329        ctx: &OperationContext,
330        from: &str,
331        to: &str,
332        _: OpCopy,
333        _opts: OpCopier,
334    ) -> Result<Self::Copier> {
335        let core = self.core.clone();
336        let ctx = ctx.clone();
337        let from = from.to_string();
338        let to = to.to_string();
339
340        Ok(oio::OneShotCopier::new(async move {
341            let resp = core.dropbox_copy(&ctx, &from, &to).await?;
342            let status = resp.status();
343
344            match status {
345                StatusCode::OK => Ok(Metadata::default()),
346                _ => {
347                    let err = parse_error(resp);
348                    match err.kind() {
349                        ErrorKind::NotFound => Ok(Metadata::default()),
350                        _ => Err(err),
351                    }
352                }
353            }
354        }))
355    }
356
357    async fn rename(
358        &self,
359        ctx: &OperationContext,
360        from: &str,
361        to: &str,
362        _: OpRename,
363    ) -> Result<RpRename> {
364        let resp = self.core.dropbox_move(ctx, from, to).await?;
365
366        let status = resp.status();
367
368        match status {
369            StatusCode::OK => Ok(RpRename::default()),
370            _ => {
371                let err = parse_error(resp);
372                match err.kind() {
373                    ErrorKind::NotFound => Ok(RpRename::default()),
374                    _ => Err(err),
375                }
376            }
377        }
378    }
379
380    async fn presign(
381        &self,
382        _ctx: &OperationContext,
383        _path: &str,
384        _args: OpPresign,
385    ) -> Result<RpPresign> {
386        Err(Error::new(
387            ErrorKind::Unsupported,
388            "operation is not supported",
389        ))
390    }
391}