opendal/services/onedrive/
lister.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;
21
22use super::core::OneDriveCore;
23use super::error::parse_error;
24use super::graph_model::GraphApiOneDriveListResponse;
25use super::graph_model::ItemType;
26use super::graph_model::GENERAL_SELECT_PARAM;
27use crate::raw::oio;
28use crate::raw::*;
29use crate::*;
30
31pub struct OneDriveLister {
32    core: Arc<OneDriveCore>,
33    path: String,
34    op: OpList,
35}
36
37impl OneDriveLister {
38    const DRIVE_ROOT_PREFIX: &'static str = "/drive/root:";
39
40    pub(crate) fn new(path: String, core: Arc<OneDriveCore>, args: &OpList) -> Self {
41        Self {
42            core,
43            path,
44            op: args.clone(),
45        }
46    }
47}
48
49impl oio::PageList for OneDriveLister {
50    async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
51        let request_url = if ctx.token.is_empty() {
52            let base = format!(
53                "{}:/children?{}",
54                self.core.onedrive_item_url(&self.path, true),
55                GENERAL_SELECT_PARAM
56            );
57            if let Some(limit) = self.op.limit() {
58                base + &format!("&$top={}", limit)
59            } else {
60                base
61            }
62        } else {
63            ctx.token.clone()
64        };
65
66        let response = self.core.onedrive_get_next_list_page(&request_url).await?;
67
68        let status_code = response.status();
69        if !status_code.is_success() {
70            if status_code == http::StatusCode::NOT_FOUND {
71                ctx.done = true;
72                return Ok(());
73            }
74            return Err(parse_error(response));
75        }
76
77        let bytes = response.into_body();
78        let decoded_response: GraphApiOneDriveListResponse =
79            serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
80
81        let list_with_versions = self.core.info.native_capability().list_with_versions;
82
83        // Include the current directory itself when handling the first page of the listing.
84        if ctx.token.is_empty() && !ctx.done {
85            // TODO: when listing a directory directly, we could reuse the stat result,
86            // cache the result when listing nested directory
87            let path = if self.path == "/" {
88                "".to_string()
89            } else {
90                self.path.clone()
91            };
92
93            let meta = self.core.onedrive_stat(&path, OpStat::default()).await?;
94
95            // skip `list_with_versions` intentionally because a folder doesn't have versions
96
97            let entry = oio::Entry::new(&path, meta);
98            ctx.entries.push_back(entry);
99        }
100
101        if let Some(next_link) = decoded_response.next_link {
102            ctx.token = next_link;
103        } else {
104            ctx.done = true;
105        }
106
107        for drive_item in decoded_response.value {
108            let name = drive_item.name;
109            let parent_path = drive_item.parent_reference.path;
110            let parent_path = parent_path
111                .strip_prefix(Self::DRIVE_ROOT_PREFIX)
112                .unwrap_or("");
113
114            let path = format!("{}/{}", parent_path, name);
115            let mut normalized_path = build_rel_path(self.core.root.as_str(), path.as_str());
116            let entry_mode = match drive_item.item_type {
117                ItemType::Folder { .. } => EntryMode::DIR,
118                ItemType::File { .. } => EntryMode::FILE,
119            };
120
121            // Add the trailing `/` because OneDrive returns a directory with the name
122            if entry_mode == EntryMode::DIR {
123                normalized_path.push('/');
124            }
125
126            let mut meta = Metadata::new(entry_mode)
127                .with_etag(drive_item.e_tag)
128                .with_content_length(drive_item.size.max(0) as u64);
129            let last_modified =
130                parse_datetime_from_rfc3339(drive_item.last_modified_date_time.as_str())?;
131            meta.set_last_modified(last_modified);
132
133            // When listing a directory with `$expand=versions`, OneDrive returns 400 "Operation not supported".
134            // Thus, `list_with_versions` induces N+1 requests. This N+1 is intentional.
135            // N+1 is horrendous but we can't do any better without OneDrive's API support.
136            // When OneDrive supports listing with versions API, remove this.
137            if list_with_versions {
138                let versions = self.core.onedrive_list_versions(&path).await?;
139                if let Some(version) = versions.first() {
140                    meta.set_version(&version.id);
141                }
142            }
143
144            let entry = oio::Entry::new(&normalized_path, meta);
145            ctx.entries.push_back(entry)
146        }
147
148        Ok(())
149    }
150}