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}