opendal/services/azfile/
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;
21use http::StatusCode;
22use quick_xml::de;
23use serde::Deserialize;
24
25use super::core::AzfileCore;
26use super::error::parse_error;
27use crate::raw::*;
28use crate::*;
29
30pub struct AzfileLister {
31    core: Arc<AzfileCore>,
32    path: String,
33    limit: Option<usize>,
34}
35
36impl AzfileLister {
37    pub fn new(core: Arc<AzfileCore>, path: String, limit: Option<usize>) -> Self {
38        Self { core, path, limit }
39    }
40}
41
42impl oio::PageList for AzfileLister {
43    async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
44        let resp = self
45            .core
46            .azfile_list(&self.path, &self.limit, &ctx.token)
47            .await?;
48
49        let status = resp.status();
50
51        if status != StatusCode::OK {
52            if status == StatusCode::NOT_FOUND {
53                ctx.done = true;
54                return Ok(());
55            }
56            return Err(parse_error(resp));
57        }
58
59        // Return self at the first page.
60        if ctx.token.is_empty() && !ctx.done {
61            let e = oio::Entry::new(&self.path, Metadata::new(EntryMode::DIR));
62            ctx.entries.push_back(e);
63        }
64
65        let bs = resp.into_body();
66
67        let results: EnumerationResults =
68            de::from_reader(bs.reader()).map_err(new_xml_deserialize_error)?;
69
70        if results.next_marker.is_empty() {
71            ctx.done = true;
72        } else {
73            ctx.token = results.next_marker;
74        }
75
76        for file in results.entries.file {
77            let meta = Metadata::new(EntryMode::FILE)
78                .with_etag(file.properties.etag)
79                .with_content_length(file.properties.content_length.unwrap_or(0))
80                .with_last_modified(parse_datetime_from_rfc2822(&file.properties.last_modified)?);
81            let path = self.path.clone().trim_start_matches('/').to_string() + &file.name;
82            ctx.entries.push_back(oio::Entry::new(&path, meta));
83        }
84
85        for dir in results.entries.directory {
86            let meta = Metadata::new(EntryMode::DIR)
87                .with_etag(dir.properties.etag)
88                .with_last_modified(parse_datetime_from_rfc2822(&dir.properties.last_modified)?);
89            let path = self.path.clone().trim_start_matches('/').to_string() + &dir.name + "/";
90            ctx.entries.push_back(oio::Entry::new(&path, meta));
91        }
92
93        Ok(())
94    }
95}
96
97#[derive(Debug, Deserialize, PartialEq)]
98#[serde(rename_all = "PascalCase")]
99struct EnumerationResults {
100    marker: Option<String>,
101    prefix: Option<String>,
102    max_results: Option<u32>,
103    directory_id: Option<String>,
104    entries: Entries,
105    #[serde(default)]
106    next_marker: String,
107}
108
109#[derive(Debug, Deserialize, PartialEq)]
110#[serde(rename_all = "PascalCase")]
111struct Entries {
112    #[serde(default)]
113    file: Vec<File>,
114    #[serde(default)]
115    directory: Vec<Directory>,
116}
117
118#[derive(Debug, Deserialize, PartialEq)]
119#[serde(rename_all = "PascalCase")]
120struct File {
121    #[serde(rename = "FileId")]
122    file_id: String,
123    name: String,
124    properties: Properties,
125}
126
127#[derive(Debug, Deserialize, PartialEq)]
128#[serde(rename_all = "PascalCase")]
129struct Directory {
130    #[serde(rename = "FileId")]
131    file_id: String,
132    name: String,
133    properties: Properties,
134}
135
136#[derive(Debug, Deserialize, PartialEq)]
137#[serde(rename_all = "PascalCase")]
138struct Properties {
139    #[serde(rename = "Content-Length")]
140    content_length: Option<u64>,
141    #[serde(rename = "CreationTime")]
142    creation_time: String,
143    #[serde(rename = "LastAccessTime")]
144    last_access_time: String,
145    #[serde(rename = "LastWriteTime")]
146    last_write_time: String,
147    #[serde(rename = "ChangeTime")]
148    change_time: String,
149    #[serde(rename = "Last-Modified")]
150    last_modified: String,
151    #[serde(rename = "Etag")]
152    etag: String,
153}
154
155#[cfg(test)]
156mod tests {
157    use quick_xml::de::from_str;
158
159    use super::*;
160
161    #[test]
162    fn test_parse_list_result() {
163        let xml = r#"
164<?xml version="1.0" encoding="utf-8"?>
165<EnumerationResults ServiceEndpoint="https://myaccount.file.core.windows.net/" ShareName="myshare" ShareSnapshot="date-time" DirectoryPath="directory-path">
166  <Marker>string-value</Marker>
167  <Prefix>string-value</Prefix>
168  <MaxResults>100</MaxResults>
169  <DirectoryId>directory-id</DirectoryId>
170  <Entries>
171     <File>
172        <Name>Rust By Example.pdf</Name>
173        <FileId>13835093239654252544</FileId>
174        <Properties>
175            <Content-Length>5832374</Content-Length>
176            <CreationTime>2023-09-25T12:43:05.8483527Z</CreationTime>
177            <LastAccessTime>2023-09-25T12:43:05.8483527Z</LastAccessTime>
178            <LastWriteTime>2023-09-25T12:43:08.6337775Z</LastWriteTime>
179            <ChangeTime>2023-09-25T12:43:08.6337775Z</ChangeTime>
180            <Last-Modified>Mon, 25 Sep 2023 12:43:08 GMT</Last-Modified>
181            <Etag>\"0x8DBBDC4F8AC4AEF\"</Etag>
182        </Properties>
183    </File>
184    <Directory>
185        <Name>test_list_rich_dir</Name>
186        <FileId>12105702186650959872</FileId>
187        <Properties>
188            <CreationTime>2023-10-15T12:03:40.7194774Z</CreationTime>
189            <LastAccessTime>2023-10-15T12:03:40.7194774Z</LastAccessTime>
190            <LastWriteTime>2023-10-15T12:03:40.7194774Z</LastWriteTime>
191            <ChangeTime>2023-10-15T12:03:40.7194774Z</ChangeTime>
192            <Last-Modified>Sun, 15 Oct 2023 12:03:40 GMT</Last-Modified>
193            <Etag>\"0x8DBCD76C58C3E96\"</Etag>
194        </Properties>
195    </Directory>
196  </Entries>
197  <NextMarker />
198</EnumerationResults>
199        "#;
200
201        let results: EnumerationResults = from_str(xml).unwrap();
202
203        assert_eq!(results.entries.file[0].name, "Rust By Example.pdf");
204
205        assert_eq!(
206            results.entries.file[0].properties.etag,
207            "\\\"0x8DBBDC4F8AC4AEF\\\""
208        );
209
210        assert_eq!(results.entries.directory[0].name, "test_list_rich_dir");
211
212        assert_eq!(
213            results.entries.directory[0].properties.etag,
214            "\\\"0x8DBCD76C58C3E96\\\""
215        );
216    }
217}