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}