files_sdk/files/
files.rs

1//! File operations
2//!
3//! This module provides core file operations including:
4//! - Download files
5//! - Upload files (using the two-stage upload process)
6//! - Update file metadata
7//! - Delete files
8//!
9//! Note: File uploads in Files.com use a two-stage process:
10//! 1. Call `FileActionHandler::begin_upload()` to get upload URLs
11//! 2. Use this handler's `upload_file()` to complete the upload
12
13use crate::files::FileActionHandler;
14use crate::types::FileEntity;
15use crate::{FilesClient, Result};
16use serde_json::json;
17use std::collections::HashMap;
18
19/// Handler for file operations
20///
21/// Provides methods for downloading, uploading, updating, and deleting files.
22#[derive(Debug, Clone)]
23pub struct FileHandler {
24    client: FilesClient,
25}
26
27impl FileHandler {
28    /// Creates a new FileHandler
29    ///
30    /// # Arguments
31    ///
32    /// * `client` - FilesClient instance
33    pub fn new(client: FilesClient) -> Self {
34        Self { client }
35    }
36
37    /// Download a file or get file information
38    ///
39    /// # Arguments
40    ///
41    /// * `path` - File path to download
42    ///
43    /// # Returns
44    ///
45    /// Returns a `FileEntity` containing file information including a
46    /// `download_uri` for the actual file download.
47    ///
48    /// # Examples
49    ///
50    /// ```rust,no_run
51    /// use files_sdk::{FilesClient, FileHandler};
52    ///
53    /// # #[tokio::main]
54    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
55    /// let client = FilesClient::builder()
56    ///     .api_key("your-api-key")
57    ///     .build()?;
58    ///
59    /// let handler = FileHandler::new(client);
60    /// let file = handler.download_file("/path/to/file.txt").await?;
61    ///
62    /// if let Some(uri) = file.download_uri {
63    ///     println!("Download from: {}", uri);
64    /// }
65    /// # Ok(())
66    /// # }
67    /// ```
68    pub async fn download_file(&self, path: &str) -> Result<FileEntity> {
69        let endpoint = format!("/files{}", path);
70        let response = self.client.get_raw(&endpoint).await?;
71        Ok(serde_json::from_value(response)?)
72    }
73
74    /// Get file metadata only (no download URL, no logging)
75    ///
76    /// This is a convenience method that calls `FileActionHandler::get_metadata()`
77    ///
78    /// # Arguments
79    ///
80    /// * `path` - File path
81    pub async fn get_metadata(&self, path: &str) -> Result<FileEntity> {
82        let file_action = FileActionHandler::new(self.client.clone());
83        file_action.get_metadata(path).await
84    }
85
86    /// Upload a file (complete two-stage upload process)
87    ///
88    /// This method handles the complete upload process:
89    /// 1. Calls begin_upload to get upload URLs
90    /// 2. Uploads the file data
91    /// 3. Finalizes the upload
92    ///
93    /// # Arguments
94    ///
95    /// * `path` - Destination path for the file
96    /// * `data` - File contents as bytes
97    ///
98    /// # Examples
99    ///
100    /// ```rust,no_run
101    /// # use files_sdk::{FilesClient, FileHandler};
102    /// # #[tokio::main]
103    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
104    /// # let client = FilesClient::builder().api_key("key").build()?;
105    /// let handler = FileHandler::new(client);
106    ///
107    /// let data = b"Hello, Files.com!";
108    /// let file = handler.upload_file("/uploads/test.txt", data).await?;
109    /// println!("Uploaded: {:?}", file.path);
110    /// # Ok(())
111    /// # }
112    /// ```
113    pub async fn upload_file(&self, path: &str, data: &[u8]) -> Result<FileEntity> {
114        // Stage 1: Begin upload
115        let file_action = FileActionHandler::new(self.client.clone());
116        let upload_parts = file_action
117            .begin_upload(path, Some(data.len() as i64), true)
118            .await?;
119
120        if upload_parts.is_empty() {
121            return Err(crate::FilesError::ApiError {
122                code: 500,
123                message: "No upload parts returned from begin_upload".to_string(),
124            });
125        }
126
127        let upload_part = &upload_parts[0];
128
129        // Stage 2: Upload file data to the provided URL
130        // This is an external URL (not Files.com API), typically to cloud storage
131        let _etag = if let Some(upload_uri) = &upload_part.upload_uri {
132            let http_client = reqwest::Client::new();
133            let http_method = upload_part
134                .http_method
135                .as_deref()
136                .unwrap_or("PUT")
137                .to_uppercase();
138
139            let mut request = match http_method.as_str() {
140                "PUT" => http_client.put(upload_uri),
141                "POST" => http_client.post(upload_uri),
142                _ => http_client.put(upload_uri),
143            };
144
145            // Add any custom headers
146            if let Some(headers) = &upload_part.headers {
147                for (key, value) in headers {
148                    request = request.header(key, value);
149                }
150            }
151
152            // Upload the file data and capture the response
153            let upload_response = request.body(data.to_vec()).send().await?;
154
155            // Extract ETag from response headers
156            upload_response
157                .headers()
158                .get("etag")
159                .and_then(|v| v.to_str().ok())
160                .map(|s| s.trim_matches('"').to_string())
161        } else {
162            None
163        };
164
165        // Stage 3: Finalize upload with Files.com
166        let endpoint = format!("/files{}", path);
167
168        // Build the finalization request as form data
169        let mut form = vec![("action", "end".to_string())];
170
171        // Add ref (upload reference) - this is required to identify the upload
172        if let Some(ref_value) = &upload_part.ref_ {
173            form.push(("ref", ref_value.clone()));
174        }
175
176        // Note: etags might not be needed when ref is provided
177        // Commenting out for now to test
178        // if let Some(etag_value) = etag {
179        //     let part_number = upload_part.part_number.unwrap_or(1);
180        //     form.push(("etags[etag]", etag_value));
181        //     form.push(("etags[part]", part_number.to_string()));
182        // }
183
184        let response = self.client.post_form(&endpoint, &form).await?;
185        Ok(serde_json::from_value(response)?)
186    }
187
188    /// Update file metadata
189    ///
190    /// # Arguments
191    ///
192    /// * `path` - File path
193    /// * `custom_metadata` - Custom metadata key-value pairs (optional)
194    /// * `provided_mtime` - Custom modification time (optional)
195    /// * `priority_color` - Priority color (optional)
196    ///
197    /// # Examples
198    ///
199    /// ```rust,no_run
200    /// # use files_sdk::{FilesClient, FileHandler};
201    /// # use std::collections::HashMap;
202    /// # #[tokio::main]
203    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
204    /// # let client = FilesClient::builder().api_key("key").build()?;
205    /// let handler = FileHandler::new(client);
206    ///
207    /// let mut metadata = HashMap::new();
208    /// metadata.insert("category".to_string(), "reports".to_string());
209    ///
210    /// handler.update_file("/path/to/file.txt", Some(metadata), None, None).await?;
211    /// # Ok(())
212    /// # }
213    /// ```
214    pub async fn update_file(
215        &self,
216        path: &str,
217        custom_metadata: Option<HashMap<String, String>>,
218        provided_mtime: Option<String>,
219        priority_color: Option<String>,
220    ) -> Result<FileEntity> {
221        let mut body = json!({});
222
223        if let Some(metadata) = custom_metadata {
224            body["custom_metadata"] = json!(metadata);
225        }
226
227        if let Some(mtime) = provided_mtime {
228            body["provided_mtime"] = json!(mtime);
229        }
230
231        if let Some(color) = priority_color {
232            body["priority_color"] = json!(color);
233        }
234
235        let endpoint = format!("/files{}", path);
236        let response = self.client.patch_raw(&endpoint, body).await?;
237        Ok(serde_json::from_value(response)?)
238    }
239
240    /// Delete a file
241    ///
242    /// # Arguments
243    ///
244    /// * `path` - File path to delete
245    /// * `recursive` - If path is a folder, delete recursively
246    ///
247    /// # Examples
248    ///
249    /// ```rust,no_run
250    /// # use files_sdk::{FilesClient, FileHandler};
251    /// # #[tokio::main]
252    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
253    /// # let client = FilesClient::builder().api_key("key").build()?;
254    /// let handler = FileHandler::new(client);
255    /// handler.delete_file("/path/to/file.txt", false).await?;
256    /// # Ok(())
257    /// # }
258    /// ```
259    pub async fn delete_file(&self, path: &str, recursive: bool) -> Result<()> {
260        let endpoint = if recursive {
261            format!("/files{}?recursive=true", path)
262        } else {
263            format!("/files{}", path)
264        };
265
266        self.client.delete_raw(&endpoint).await?;
267        Ok(())
268    }
269
270    /// Copy a file
271    ///
272    /// This is a convenience method that calls `FileActionHandler::copy_file()`
273    ///
274    /// # Arguments
275    ///
276    /// * `source` - Source file path
277    /// * `destination` - Destination path
278    pub async fn copy_file(&self, source: &str, destination: &str) -> Result<()> {
279        let file_action = FileActionHandler::new(self.client.clone());
280        file_action.copy_file(source, destination).await
281    }
282
283    /// Move a file
284    ///
285    /// This is a convenience method that calls `FileActionHandler::move_file()`
286    ///
287    /// # Arguments
288    ///
289    /// * `source` - Source file path
290    /// * `destination` - Destination path
291    pub async fn move_file(&self, source: &str, destination: &str) -> Result<()> {
292        let file_action = FileActionHandler::new(self.client.clone());
293        file_action.move_file(source, destination).await
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_handler_creation() {
303        let client = FilesClient::builder().api_key("test-key").build().unwrap();
304        let _handler = FileHandler::new(client);
305    }
306}