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 asyncband::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    type Composer = ();
204
205    fn info(&self) -> ServiceInfo {
206        self.core.info.clone()
207    }
208
209    fn capability(&self) -> Capability {
210        self.core.capability
211    }
212
213    async fn create_dir(
214        &self,
215        ctx: &OperationContext,
216        path: &str,
217        _args: OpCreateDir,
218    ) -> Result<RpCreateDir> {
219        // Check if the folder already exists.
220        let resp = self.core.dropbox_get_metadata(ctx, path).await?;
221        if StatusCode::OK == resp.status() {
222            let bytes = resp.into_body();
223            let decoded_response: DropboxMetadataResponse =
224                serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
225            if "folder" == decoded_response.tag {
226                return Ok(RpCreateDir::default());
227            }
228            if "file" == decoded_response.tag {
229                return Err(Error::new(
230                    ErrorKind::NotADirectory,
231                    format!("it's not a directory {path}"),
232                ));
233            }
234        }
235
236        let res = self.core.dropbox_create_folder(ctx, path).await?;
237        Ok(res)
238    }
239
240    async fn stat(&self, ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
241        let resp = self.core.dropbox_get_metadata(ctx, path).await?;
242        let status = resp.status();
243        match status {
244            StatusCode::OK => {
245                let bytes = resp.into_body();
246                let decoded_response: DropboxMetadataResponse =
247                    serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
248                let entry_mode: EntryMode = match decoded_response.tag.as_str() {
249                    "file" => EntryMode::FILE,
250                    "folder" => EntryMode::DIR,
251                    _ => EntryMode::Unknown,
252                };
253
254                let mut metadata = match entry_mode {
255                    EntryMode::FILE => {
256                        MetadataBuilder::file(decoded_response.size.ok_or_else(|| {
257                            Error::new(
258                                ErrorKind::Unexpected,
259                                format!("no size found for file {path}"),
260                            )
261                        })?)
262                    }
263                    EntryMode::DIR => MetadataBuilder::dir(),
264                    EntryMode::Unknown => MetadataBuilder::unknown(),
265                };
266                // Only set last_modified and size if entry_mode is FILE, because Dropbox API
267                // returns last_modified and size only for files.
268                // FYI: https://www.dropbox.com/developers/documentation/http/documentation#files-get_metadata
269                if entry_mode == EntryMode::FILE {
270                    let date_utc_last_modified =
271                        decoded_response.client_modified.parse::<Timestamp>()?;
272                    metadata.last_modified(date_utc_last_modified);
273                }
274                Ok(RpStat::new(metadata.build()))
275            }
276            _ => Err(parse_error(
277                ErrorContext::new(ServiceOperation("GetMetadata")),
278                resp,
279            )),
280        }
281    }
282    fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
283        let output: oio::StreamReader<DropboxReader> = {
284            Ok(oio::StreamReader::new(DropboxReader::new(
285                self.clone(),
286                ctx.clone(),
287                path,
288                args,
289            )))
290        }?;
291
292        Ok(output)
293    }
294
295    fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
296        let output: oio::OneShotWriter<DropboxWriter> = {
297            Ok(oio::OneShotWriter::new(DropboxWriter::new(
298                self.core.clone(),
299                ctx.clone(),
300                args,
301                String::from(path),
302            )))
303        }?;
304
305        Ok(output)
306    }
307
308    fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
309        let output: oio::OneShotDeleter<DropboxDeleter> = {
310            Ok(oio::OneShotDeleter::new(DropboxDeleter::new(
311                self.core.clone(),
312                ctx.clone(),
313            )))
314        }?;
315
316        Ok(output)
317    }
318
319    fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
320        let output: oio::PageLister<DropboxLister> = {
321            Ok(oio::PageLister::new(DropboxLister::new(
322                self.core.clone(),
323                ctx.clone(),
324                path.to_string(),
325                args.recursive(),
326                args.limit(),
327            )))
328        }?;
329
330        Ok(output)
331    }
332
333    fn copy(
334        &self,
335        ctx: &OperationContext,
336        from: &str,
337        to: &str,
338        _: OpCopy,
339    ) -> Result<Self::Copier> {
340        let core = self.core.clone();
341        let ctx = ctx.clone();
342        let from = from.to_string();
343        let to = to.to_string();
344
345        Ok(oio::OneShotCopier::new(async move {
346            let resp = core.dropbox_copy(&ctx, &from, &to).await?;
347            let status = resp.status();
348
349            match status {
350                StatusCode::OK => {
351                    let decoded_response: DropboxMetadataResponse =
352                        serde_json::from_reader(resp.into_body().reader())
353                            .map_err(new_json_deserialize_error)?;
354                    DropboxWriter::parse_metadata(decoded_response)
355                }
356                _ => Err(parse_error(
357                    ErrorContext::new(ServiceOperation("CopyFile")),
358                    resp,
359                )),
360            }
361        }))
362    }
363
364    async fn rename(
365        &self,
366        ctx: &OperationContext,
367        from: &str,
368        to: &str,
369        _: OpRename,
370    ) -> Result<RpRename> {
371        let resp = self.core.dropbox_move(ctx, from, to).await?;
372
373        let status = resp.status();
374
375        match status {
376            StatusCode::OK => Ok(RpRename::default()),
377            _ => {
378                let err = parse_error(ErrorContext::new(ServiceOperation("MoveFile")), resp);
379                match err.kind() {
380                    ErrorKind::NotFound => Ok(RpRename::default()),
381                    _ => Err(err),
382                }
383            }
384        }
385    }
386
387    async fn presign(
388        &self,
389        _ctx: &OperationContext,
390        _path: &str,
391        _args: OpPresign,
392    ) -> Result<RpPresign> {
393        Err(Error::new(
394            ErrorKind::Unsupported,
395            "operation is not supported",
396        ))
397    }
398}