files_sdk/files/
folders.rs

1//! Folder operations
2//!
3//! Provides directory management and navigation operations for Files.com. Folders are
4//! represented as `FileEntity` objects with `type="directory"`.
5//!
6//! # Features
7//!
8//! - List folder contents with pagination
9//! - Create folders (with parent directory creation)
10//! - Delete folders (recursive or non-recursive)
11//! - Search files within folders
12//! - Automatic pagination for large directories
13//!
14//! # Example
15//!
16//! ```no_run
17//! use files_sdk::{FilesClient, FolderHandler};
18//!
19//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! let client = FilesClient::builder()
21//!     .api_key("your-api-key")
22//!     .build()?;
23//!
24//! let handler = FolderHandler::new(client);
25//!
26//! // List root directory
27//! let (files, pagination) = handler.list_folder("/", None, None).await?;
28//! for file in files {
29//!     println!("{}: {}",
30//!         file.file_type.unwrap_or_default(),
31//!         file.path.unwrap_or_default());
32//! }
33//!
34//! // Create a new folder with parent directories
35//! handler.create_folder("/projects/2024/q4", true).await?;
36//!
37//! // Search for files
38//! let (results, _) = handler.search_folder("/", "report", None).await?;
39//! println!("Found {} matching files", results.len());
40//! # Ok(())
41//! # }
42//! ```
43
44use crate::utils::encode_path;
45use crate::{FileEntity, FilesClient, PaginationInfo, Result};
46use futures::stream::Stream;
47use serde_json::json;
48
49/// Handler for folder operations
50///
51/// Provides methods for listing, creating, searching, and managing folders
52/// (directories) in Files.com.
53#[derive(Debug, Clone)]
54pub struct FolderHandler {
55    client: FilesClient,
56}
57
58impl FolderHandler {
59    /// Creates a new FolderHandler
60    ///
61    /// # Arguments
62    ///
63    /// * `client` - FilesClient instance
64    pub fn new(client: FilesClient) -> Self {
65        Self { client }
66    }
67
68    /// List folder contents
69    ///
70    /// Returns files and subdirectories within the specified folder.
71    ///
72    /// # Arguments
73    ///
74    /// * `path` - Folder path to list (empty string for root)
75    /// * `per_page` - Number of items per page (optional, max 10,000)
76    /// * `cursor` - Pagination cursor (optional)
77    ///
78    /// # Returns
79    ///
80    /// Returns a tuple of (files, pagination_info)
81    ///
82    /// # Examples
83    ///
84    /// ```rust,no_run
85    /// use files_sdk::{FilesClient, FolderHandler};
86    ///
87    /// # #[tokio::main]
88    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
89    /// let client = FilesClient::builder()
90    ///     .api_key("your-api-key")
91    ///     .build()?;
92    ///
93    /// let handler = FolderHandler::new(client);
94    /// let (files, pagination) = handler.list_folder("/", None, None).await?;
95    ///
96    /// for file in files {
97    ///     println!("{}: {}", file.file_type.unwrap_or_default(), file.path.unwrap_or_default());
98    /// }
99    ///
100    /// if pagination.has_next() {
101    ///     println!("More results available");
102    /// }
103    /// # Ok(())
104    /// # }
105    /// ```
106    pub async fn list_folder(
107        &self,
108        path: &str,
109        per_page: Option<i32>,
110        cursor: Option<String>,
111    ) -> Result<(Vec<FileEntity>, PaginationInfo)> {
112        let encoded_path = encode_path(path);
113        let mut endpoint = format!("/folders{}", encoded_path);
114        let mut query_params = Vec::new();
115
116        if let Some(per_page) = per_page {
117            query_params.push(format!("per_page={}", per_page));
118        }
119
120        if let Some(cursor) = cursor {
121            query_params.push(format!("cursor={}", cursor));
122        }
123
124        if !query_params.is_empty() {
125            endpoint.push('?');
126            endpoint.push_str(&query_params.join("&"));
127        }
128
129        // Need to get the raw response to access headers
130        let url = format!("{}{}", self.client.inner.base_url, endpoint);
131        let response = reqwest::Client::new()
132            .get(&url)
133            .header("X-FilesAPI-Key", &self.client.inner.api_key)
134            .send()
135            .await?;
136
137        let headers = response.headers().clone();
138        let pagination = PaginationInfo::from_headers(&headers);
139
140        let status = response.status();
141        if !status.is_success() {
142            return Err(crate::FilesError::ApiError {
143                endpoint: None,
144                code: status.as_u16(),
145                message: response.text().await.unwrap_or_default(),
146            });
147        }
148
149        let files: Vec<FileEntity> = response.json().await?;
150
151        Ok((files, pagination))
152    }
153
154    /// List all folder contents (auto-pagination)
155    ///
156    /// Automatically handles pagination to retrieve all items in a folder.
157    ///
158    /// # Arguments
159    ///
160    /// * `path` - Folder path to list
161    ///
162    /// # Examples
163    ///
164    /// ```rust,no_run
165    /// # use files_sdk::{FilesClient, FolderHandler};
166    /// # #[tokio::main]
167    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
168    /// # let client = FilesClient::builder().api_key("key").build()?;
169    /// let handler = FolderHandler::new(client);
170    /// let all_files = handler.list_folder_all("/uploads").await?;
171    /// println!("Total files: {}", all_files.len());
172    /// # Ok(())
173    /// # }
174    /// ```
175    pub async fn list_folder_all(&self, path: &str) -> Result<Vec<FileEntity>> {
176        let mut all_files = Vec::new();
177        let mut cursor = None;
178
179        loop {
180            let (mut files, pagination) = self.list_folder(path, Some(1000), cursor).await?;
181            all_files.append(&mut files);
182
183            if pagination.has_next() {
184                cursor = pagination.cursor_next;
185            } else {
186                break;
187            }
188        }
189
190        Ok(all_files)
191    }
192
193    /// Stream folder contents with automatic pagination
194    ///
195    /// Returns a stream that automatically handles pagination, yielding
196    /// individual files as they are fetched. This is more memory-efficient
197    /// than `list_folder_all()` for large directories.
198    ///
199    /// # Arguments
200    ///
201    /// * `path` - Folder path to list
202    /// * `per_page` - Number of items per page (optional, default 1000)
203    ///
204    /// # Examples
205    ///
206    /// ```rust,no_run
207    /// # use files_sdk::{FilesClient, FolderHandler};
208    /// # use futures::stream::StreamExt;
209    /// # #[tokio::main]
210    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
211    /// # let client = FilesClient::builder().api_key("key").build()?;
212    /// let handler = FolderHandler::new(client);
213    /// let stream = handler.list_stream("/uploads", Some(100));
214    ///
215    /// tokio::pin!(stream);
216    ///
217    /// while let Some(file) = stream.next().await {
218    ///     let file = file?;
219    ///     println!("{}", file.path.unwrap_or_default());
220    /// }
221    /// # Ok(())
222    /// # }
223    /// ```
224    pub fn list_stream(
225        &self,
226        path: &str,
227        per_page: Option<i32>,
228    ) -> impl Stream<Item = Result<FileEntity>> + '_ {
229        let path = path.to_string();
230        let per_page = per_page.unwrap_or(1000);
231
232        async_stream::try_stream! {
233            let mut cursor: Option<String> = None;
234
235            loop {
236                let (files, pagination) = self
237                    .list_folder(&path, Some(per_page), cursor.clone())
238                    .await?;
239
240                for file in files {
241                    yield file;
242                }
243
244                match pagination.cursor_next {
245                    Some(next) => cursor = Some(next),
246                    None => break,
247                }
248            }
249        }
250    }
251
252    /// Create a new folder
253    ///
254    /// Note: In Files.com, folders are created implicitly when uploading files
255    /// with `mkdir_parents=true`. This method creates an empty folder.
256    ///
257    /// # Arguments
258    ///
259    /// * `path` - Folder path to create
260    /// * `mkdir_parents` - Create parent directories if they don't exist
261    ///
262    /// # Examples
263    ///
264    /// ```rust,no_run
265    /// # use files_sdk::{FilesClient, FolderHandler};
266    /// # #[tokio::main]
267    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
268    /// # let client = FilesClient::builder().api_key("key").build()?;
269    /// let handler = FolderHandler::new(client);
270    /// handler.create_folder("/new/folder", true).await?;
271    /// # Ok(())
272    /// # }
273    /// ```
274    pub async fn create_folder(&self, path: &str, mkdir_parents: bool) -> Result<FileEntity> {
275        let body = json!({
276            "path": path,
277            "mkdir_parents": mkdir_parents,
278        });
279
280        let encoded_path = encode_path(path);
281        let endpoint = format!("/folders{}", encoded_path);
282        let response = self.client.post_raw(&endpoint, body).await?;
283        Ok(serde_json::from_value(response)?)
284    }
285
286    /// Delete a folder
287    ///
288    /// # Arguments
289    ///
290    /// * `path` - Folder path to delete
291    /// * `recursive` - Delete folder and all contents recursively
292    ///
293    /// # Examples
294    ///
295    /// ```rust,no_run
296    /// # use files_sdk::{FilesClient, FolderHandler};
297    /// # #[tokio::main]
298    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
299    /// # let client = FilesClient::builder().api_key("key").build()?;
300    /// let handler = FolderHandler::new(client);
301    /// handler.delete_folder("/old/folder", true).await?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    pub async fn delete_folder(&self, path: &str, recursive: bool) -> Result<()> {
306        let encoded_path = encode_path(path);
307        let endpoint = if recursive {
308            format!("/folders{}?recursive=true", encoded_path)
309        } else {
310            format!("/folders{}", encoded_path)
311        };
312
313        self.client.delete_raw(&endpoint).await?;
314        Ok(())
315    }
316
317    /// Search for files within a folder
318    ///
319    /// # Arguments
320    ///
321    /// * `path` - Folder path to search in
322    /// * `search` - Search query string
323    /// * `per_page` - Number of results per page (optional)
324    ///
325    /// # Examples
326    ///
327    /// ```rust,no_run
328    /// # use files_sdk::{FilesClient, FolderHandler};
329    /// # #[tokio::main]
330    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
331    /// # let client = FilesClient::builder().api_key("key").build()?;
332    /// let handler = FolderHandler::new(client);
333    /// let (results, _) = handler.search_folder("/", "report", None).await?;
334    /// println!("Found {} files", results.len());
335    /// # Ok(())
336    /// # }
337    /// ```
338    pub async fn search_folder(
339        &self,
340        path: &str,
341        search: &str,
342        per_page: Option<i32>,
343    ) -> Result<(Vec<FileEntity>, PaginationInfo)> {
344        let encoded_path = encode_path(path);
345        let mut endpoint = format!("/folders{}?search={}", encoded_path, search);
346
347        if let Some(per_page) = per_page {
348            endpoint.push_str(&format!("&per_page={}", per_page));
349        }
350
351        let url = format!("{}{}", self.client.inner.base_url, endpoint);
352        let response = reqwest::Client::new()
353            .get(&url)
354            .header("X-FilesAPI-Key", &self.client.inner.api_key)
355            .send()
356            .await?;
357
358        let headers = response.headers().clone();
359        let pagination = PaginationInfo::from_headers(&headers);
360
361        let status = response.status();
362        if !status.is_success() {
363            return Err(crate::FilesError::ApiError {
364                endpoint: None,
365                code: status.as_u16(),
366                message: response.text().await.unwrap_or_default(),
367            });
368        }
369
370        let files: Vec<FileEntity> = response.json().await?;
371
372        Ok((files, pagination))
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_handler_creation() {
382        let client = FilesClient::builder().api_key("test-key").build().unwrap();
383        let _handler = FolderHandler::new(client);
384    }
385}