pcloud/folder/
list.rs

1use super::{Folder, FolderIdentifier, FolderResponse};
2
3/// Options for customizing folder listing behavior.
4///
5/// This struct allows you to control recursion, visibility of deleted items,
6/// and whether to include files and shared items in the response from `listfolder`.
7#[derive(Default, serde::Serialize)]
8pub struct ListFolderOptions {
9    /// Whether to list the contents of subfolders recursively.
10    #[serde(
11        skip_serializing_if = "crate::request::is_false",
12        serialize_with = "crate::request::serialize_bool"
13    )]
14    recursive: bool,
15
16    /// Whether to include deleted files and folders in the listing.
17    #[serde(
18        rename = "showdeleted",
19        skip_serializing_if = "crate::request::is_false",
20        serialize_with = "crate::request::serialize_bool"
21    )]
22    show_deleted: bool,
23
24    /// Whether to exclude files from the listing (only folders will be returned).
25    #[serde(
26        skip_serializing_if = "crate::request::is_false",
27        serialize_with = "crate::request::serialize_bool"
28    )]
29    no_files: bool,
30
31    /// Whether to exclude shared items from the listing.
32    #[serde(
33        skip_serializing_if = "crate::request::is_false",
34        serialize_with = "crate::request::serialize_bool"
35    )]
36    no_shares: bool,
37}
38
39impl ListFolderOptions {
40    /// Enables recursive listing of folder contents.
41    pub fn with_recursive(mut self) -> Self {
42        self.recursive = true;
43        self
44    }
45
46    /// Enables showing deleted files and folders in the response.
47    pub fn with_show_deleted(mut self) -> Self {
48        self.show_deleted = true;
49        self
50    }
51
52    /// Excludes files from the listing (folders only).
53    pub fn with_no_files(mut self) -> Self {
54        self.no_files = true;
55        self
56    }
57
58    /// Excludes shared items from the listing.
59    pub fn with_no_shares(mut self) -> Self {
60        self.no_shares = true;
61        self
62    }
63}
64
65/// Internal parameter bundle for listing folders.
66#[derive(serde::Serialize)]
67struct Params<'a> {
68    #[serde(flatten)]
69    identifier: FolderIdentifier<'a>,
70    #[serde(flatten)]
71    options: ListFolderOptions,
72}
73
74impl crate::Client {
75    /// Lists the contents of a folder on pCloud.
76    ///
77    /// This is a convenience method that calls [`crate::Client::list_folder_with_options`] with default options.
78    /// It will list the folder's immediate contents, including files and subfolders.
79    ///
80    /// # Arguments
81    ///
82    /// * `identifier` - A value convertible into a [`FolderIdentifier`] (e.g., path or folder ID).
83    ///
84    /// # Returns
85    ///
86    /// A [`Folder`] struct containing metadata and child entries.
87    ///
88    /// # Errors
89    ///
90    /// Returns a [`crate::Error`] if the folder cannot be listed.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// # async fn example(client: &pcloud::Client) -> Result<(), pcloud::Error> {
96    /// let folder = client.list_folder("/Documents").await?;
97    /// println!("Folder has {:?} entries", folder.contents.map(|res| res.len()).unwrap_or(0));
98    /// # Ok(())
99    /// # }
100    /// ```
101    pub async fn list_folder(
102        &self,
103        identifier: impl Into<FolderIdentifier<'_>>,
104    ) -> crate::Result<Folder> {
105        self.list_folder_with_options(identifier, Default::default())
106            .await
107    }
108
109    /// Lists the contents of a folder with custom options.
110    ///
111    /// This method gives fine-grained control over how the folder contents are returned
112    /// by the `listfolder` endpoint, such as recursive listing and filtering.
113    ///
114    /// # Arguments
115    ///
116    /// * `identifier` - A value convertible into a [`FolderIdentifier`] (e.g., path or folder ID).
117    /// * `options` - A [`ListFolderOptions`] struct specifying what to include in the listing.
118    ///
119    /// # Returns
120    ///
121    /// A [`Folder`] with metadata and contents matching the provided options.
122    ///
123    /// # Errors
124    ///
125    /// Returns a [`crate::Error`] if the folder is inaccessible or the API call fails.
126    ///
127    /// # Examples
128    ///
129    /// ```rust,no_run
130    /// use pcloud::folder::list::ListFolderOptions;
131    ///
132    /// # async fn example(client: &pcloud::Client) -> Result<(), pcloud::Error> {
133    /// let options = ListFolderOptions::default()
134    ///     .with_recursive()
135    ///     .with_show_deleted();
136    /// let folder = client.list_folder_with_options("/Backup", options).await?;
137    /// println!("Listed folder: {:?}", folder.base.name);
138    /// # Ok(())
139    /// # }
140    /// ```
141    pub async fn list_folder_with_options(
142        &self,
143        identifier: impl Into<FolderIdentifier<'_>>,
144        options: ListFolderOptions,
145    ) -> crate::Result<Folder> {
146        let params = Params {
147            identifier: identifier.into(),
148            options,
149        };
150        self.get_request::<FolderResponse, _>("listfolder", params)
151            .await
152            .map(|res| res.metadata)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::{Client, Credentials};
159    use mockito::Matcher;
160
161    #[tokio::test]
162    async fn success_with_options() {
163        let mut server = mockito::Server::new_async().await;
164        let m = server
165            .mock("GET", "/listfolder")
166            .match_query(Matcher::AllOf(vec![
167                Matcher::UrlEncoded("access_token".into(), "access-token".into()),
168                Matcher::UrlEncoded("folderid".into(), "0".into()),
169                Matcher::UrlEncoded("recursive".into(), "1".into()),
170                Matcher::UrlEncoded("showdeleted".into(), "1".into()),
171            ]))
172            .with_status(200)
173            .with_body(
174                r#"{
175    "result": 0,
176    "metadata": {
177        "path": "\/testing",
178        "name": "testing",
179        "created": "Fri, 23 Jul 2021 19:39:09 +0000",
180        "ismine": true,
181        "thumb": false,
182        "modified": "Fri, 23 Jul 2021 19:39:09 +0000",
183        "id": "d10",
184        "isshared": false,
185        "icon": "folder",
186        "isfolder": true,
187        "parentfolderid": 0,
188        "folderid": 10
189    }
190}"#,
191            )
192            .create();
193        let client = Client::new(server.url(), Credentials::access_token("access-token")).unwrap();
194        let payload = client
195            .list_folder_with_options(
196                0,
197                super::ListFolderOptions::default()
198                    .with_recursive()
199                    .with_show_deleted(),
200            )
201            .await
202            .unwrap();
203        assert_eq!(payload.base.parent_folder_id, Some(0));
204        m.assert();
205    }
206
207    #[tokio::test]
208    async fn success() {
209        let mut server = mockito::Server::new_async().await;
210        let m = server
211            .mock("GET", "/listfolder")
212            .match_query(Matcher::AllOf(vec![
213                Matcher::UrlEncoded("access_token".into(), "access-token".into()),
214                Matcher::UrlEncoded("folderid".into(), "0".into()),
215            ]))
216            .with_status(200)
217            .with_body(
218                r#"{
219    "result": 0,
220    "metadata": {
221        "path": "\/testing",
222        "name": "testing",
223        "created": "Fri, 23 Jul 2021 19:39:09 +0000",
224        "ismine": true,
225        "thumb": false,
226        "modified": "Fri, 23 Jul 2021 19:39:09 +0000",
227        "id": "d10",
228        "isshared": false,
229        "icon": "folder",
230        "isfolder": true,
231        "parentfolderid": 0,
232        "folderid": 10
233    }
234}"#,
235            )
236            .create();
237        let client = Client::new(server.url(), Credentials::access_token("access-token")).unwrap();
238        let payload = client.list_folder(0).await.unwrap();
239        assert_eq!(payload.base.parent_folder_id, Some(0));
240        m.assert();
241    }
242
243    #[tokio::test]
244    async fn error() {
245        let mut server = mockito::Server::new_async().await;
246        let m = server
247            .mock("GET", "/listfolder")
248            .match_query(Matcher::AllOf(vec![
249                Matcher::UrlEncoded("access_token".into(), "access-token".into()),
250                Matcher::UrlEncoded("folderid".into(), "0".into()),
251            ]))
252            .with_status(200)
253            .with_body(r#"{ "result": 1020, "error": "something went wrong" }"#)
254            .create();
255        let client = Client::new(server.url(), Credentials::access_token("access-token")).unwrap();
256        let error = client.list_folder(0).await.unwrap_err();
257        assert!(matches!(error, crate::Error::Protocol(_, _)));
258        m.assert();
259    }
260}