daytona_client/
files.rs

1//! File operations within sandboxes
2
3use tracing::{debug, info};
4use uuid::Uuid;
5
6use crate::{
7    client::DaytonaClient,
8    error::{DaytonaError, Result},
9    models::{FileInfo, FindResult},
10};
11
12/// File manager for sandbox file operations
13pub struct FileManager<'a> {
14    client: &'a DaytonaClient,
15}
16
17impl<'a> FileManager<'a> {
18    /// Create a new file manager
19    pub fn new(client: &'a DaytonaClient) -> Self {
20        Self { client }
21    }
22
23    /// Upload a file to the sandbox
24    pub async fn upload(&self, sandbox_id: &Uuid, path: &str, content: &[u8]) -> Result<()> {
25        info!("Uploading file to sandbox {}: {}", sandbox_id, path);
26
27        // Create multipart form for file upload using bulk-upload endpoint
28        // The API expects files[0].path and files[0].file fields
29        let form = reqwest::multipart::Form::new()
30            .text("files[0].path", path.to_string())
31            .part(
32                "files[0].file",
33                reqwest::multipart::Part::bytes(content.to_vec())
34                    .file_name(path.to_string())
35                    .mime_str("application/octet-stream")?,
36            );
37
38        let response = self
39            .client
40            .build_multipart_request(
41                reqwest::Method::POST,
42                &format!("/toolbox/{}/toolbox/files/bulk-upload", sandbox_id),
43            )
44            .multipart(form)
45            .send()
46            .await?;
47
48        if response.status().is_success() {
49            debug!("File uploaded successfully: {}", path);
50            Ok(())
51        } else {
52            let error_text = response.text().await?;
53            Err(DaytonaError::FileError(format!(
54                "Failed to upload file: {}",
55                error_text
56            )))
57        }
58    }
59
60    /// Upload text content as a file
61    pub async fn upload_text(&self, sandbox_id: &Uuid, path: &str, content: &str) -> Result<()> {
62        self.upload(sandbox_id, path, content.as_bytes()).await
63    }
64
65    /// Download a file from the sandbox
66    pub async fn download(&self, sandbox_id: &Uuid, path: &str) -> Result<Vec<u8>> {
67        info!("Downloading file from sandbox {}: {}", sandbox_id, path);
68
69        let response = self
70            .client
71            .build_request(
72                reqwest::Method::GET,
73                &format!("/toolbox/{}/toolbox/files/download", sandbox_id),
74            )
75            .query(&[("path", path)])
76            .send()
77            .await?;
78
79        if response.status().is_success() {
80            let content = response.bytes().await?.to_vec();
81            debug!(
82                "File downloaded successfully: {} ({} bytes)",
83                path,
84                content.len()
85            );
86            Ok(content)
87        } else {
88            let error_text = response.text().await?;
89            Err(DaytonaError::FileError(format!(
90                "Failed to download file: {}",
91                error_text
92            )))
93        }
94    }
95
96    /// Download a text file from the sandbox
97    pub async fn download_text(&self, sandbox_id: &Uuid, path: &str) -> Result<String> {
98        let bytes = self.download(sandbox_id, path).await?;
99        String::from_utf8(bytes)
100            .map_err(|e| DaytonaError::FileError(format!("Invalid UTF-8 in file: {}", e)))
101    }
102
103    /// Delete a file from the sandbox
104    pub async fn delete(&self, sandbox_id: &Uuid, path: &str) -> Result<()> {
105        info!("Deleting file from sandbox {}: {}", sandbox_id, path);
106
107        let response = self
108            .client
109            .build_request(
110                reqwest::Method::DELETE,
111                &format!("/toolbox/{}/toolbox/files", sandbox_id),
112            )
113            .query(&[("path", path)])
114            .send()
115            .await?;
116
117        if response.status().is_success() {
118            debug!("File deleted successfully: {}", path);
119            Ok(())
120        } else {
121            let error_text = response.text().await?;
122            Err(DaytonaError::FileError(format!(
123                "Failed to delete file: {}",
124                error_text
125            )))
126        }
127    }
128
129    /// List files in a directory
130    pub async fn list(&self, sandbox_id: &Uuid, path: &str) -> Result<Vec<FileInfo>> {
131        info!("Listing files in sandbox {}: {}", sandbox_id, path);
132
133        let response = self
134            .client
135            .build_request(
136                reqwest::Method::GET,
137                &format!("/toolbox/{}/toolbox/files", sandbox_id),
138            )
139            .query(&[("path", path)])
140            .send()
141            .await?;
142
143        self.client.handle_response(response).await
144    }
145
146    /// Get file information
147    pub async fn get_file_info(&self, sandbox_id: &Uuid, path: &str) -> Result<FileInfo> {
148        debug!("Getting file info in sandbox {}: {}", sandbox_id, path);
149
150        let response = self
151            .client
152            .build_request(
153                reqwest::Method::GET,
154                &format!("/toolbox/{}/toolbox/files/info", sandbox_id),
155            )
156            .query(&[("path", path)])
157            .send()
158            .await?;
159
160        self.client.handle_response(response).await
161    }
162
163    /// Check if a file exists
164    pub async fn exists(&self, sandbox_id: &Uuid, path: &str) -> Result<bool> {
165        self.get_file_info(sandbox_id, path)
166            .await
167            .map(|_| true)
168            .or_else(|e| match e {
169                DaytonaError::RequestFailed(msg) if msg.contains("404") => Ok(false),
170                _ => Err(e),
171            })
172    }
173
174    /// Create a directory
175    pub async fn create_directory(&self, sandbox_id: &Uuid, path: &str) -> Result<()> {
176        info!("Creating directory in sandbox {}: {}", sandbox_id, path);
177
178        let response = self
179            .client
180            .build_request(
181                reqwest::Method::POST,
182                &format!("/toolbox/{}/toolbox/files/folder", sandbox_id),
183            )
184            .query(&[("path", path), ("mode", "755")])
185            .send()
186            .await?;
187
188        if response.status().is_success() {
189            debug!("Directory created successfully: {}", path);
190            Ok(())
191        } else {
192            let error_text = response.text().await?;
193            Err(DaytonaError::FileError(format!(
194                "Failed to create directory: {}",
195                error_text
196            )))
197        }
198    }
199
200    /// Copy a file within the sandbox
201    pub async fn copy(&self, sandbox_id: &Uuid, source: &str, destination: &str) -> Result<()> {
202        info!(
203            "Copying file in sandbox {}: {} -> {}",
204            sandbox_id, source, destination
205        );
206
207        let process = crate::process::ProcessExecutor::new(self.client);
208        let result = process
209            .execute_command(sandbox_id, &format!("cp {} {}", source, destination))
210            .await?;
211
212        if result.exit_code == 0 {
213            Ok(())
214        } else {
215            Err(DaytonaError::FileError(format!(
216                "Failed to copy file: {}",
217                result.result
218            )))
219        }
220    }
221
222    /// Move/rename a file within the sandbox
223    pub async fn rename(&self, sandbox_id: &Uuid, source: &str, destination: &str) -> Result<()> {
224        info!(
225            "Moving file in sandbox {}: {} -> {}",
226            sandbox_id, source, destination
227        );
228
229        let response = self
230            .client
231            .build_request(
232                reqwest::Method::POST,
233                &format!("/toolbox/{}/toolbox/files/move", sandbox_id),
234            )
235            .query(&[("source", source), ("destination", destination)])
236            .send()
237            .await?;
238
239        if response.status().is_success() {
240            debug!("File moved successfully: {} -> {}", source, destination);
241            Ok(())
242        } else {
243            let error_text = response.text().await?;
244            Err(DaytonaError::FileError(format!(
245                "Failed to move file: {}",
246                error_text
247            )))
248        }
249    }
250
251    /// Find text in files
252    pub async fn find_in_files(
253        &self,
254        sandbox_id: &Uuid,
255        pattern: &str,
256        path: &str,
257    ) -> Result<Vec<FindResult>> {
258        info!(
259            "Finding '{}' in files at {} in sandbox {}",
260            pattern, path, sandbox_id
261        );
262
263        let response = self
264            .client
265            .build_request(
266                reqwest::Method::GET,
267                &format!("/toolbox/{}/toolbox/files/find", sandbox_id),
268            )
269            .query(&[("path", path), ("pattern", pattern)])
270            .send()
271            .await?;
272
273        if response.status().is_success() {
274            // The API returns Match objects, convert to FindResult
275            let matches: Vec<crate::models::Match> = self.client.handle_response(response).await?;
276
277            Ok(matches
278                .into_iter()
279                .map(|m| FindResult {
280                    file: m.file,
281                    line_number: m.line_number as usize,
282                    content: m.content,
283                })
284                .collect())
285        } else {
286            let error_text = response.text().await?;
287            Err(DaytonaError::FileError(format!(
288                "Failed to find in files: {}",
289                error_text
290            )))
291        }
292    }
293
294    /// Replace text in files
295    pub async fn replace_in_files(
296        &self,
297        sandbox_id: &Uuid,
298        find: &str,
299        replace: &str,
300        files: &[String],
301    ) -> Result<Vec<String>> {
302        info!(
303            "Replacing '{}' with '{}' in {} files in sandbox {}",
304            find,
305            replace,
306            files.len(),
307            sandbox_id
308        );
309
310        // Build the request body
311        let request_body = serde_json::json!({
312            "files": files,
313            "pattern": find,
314            "newValue": replace
315        });
316
317        let response = self
318            .client
319            .build_request(
320                reqwest::Method::POST,
321                &format!("/toolbox/{}/toolbox/files/replace", sandbox_id),
322            )
323            .json(&request_body)
324            .send()
325            .await?;
326
327        if response.status().is_success() {
328            // The API returns ReplaceResult objects
329            let results: Vec<crate::models::ReplaceResult> =
330                self.client.handle_response(response).await?;
331
332            Ok(results
333                .into_iter()
334                .filter_map(|r| {
335                    if r.success.unwrap_or(false) {
336                        r.file
337                    } else {
338                        None
339                    }
340                })
341                .collect())
342        } else {
343            let error_text = response.text().await?;
344            Err(DaytonaError::FileError(format!(
345                "Failed to replace in files: {}",
346                error_text
347            )))
348        }
349    }
350
351    /// Search for files by name pattern
352    pub async fn search_files(
353        &self,
354        sandbox_id: &Uuid,
355        pattern: &str,
356        path: &str,
357    ) -> Result<Vec<String>> {
358        info!(
359            "Searching for files matching '{}' in {} in sandbox {}",
360            pattern, path, sandbox_id
361        );
362
363        let response = self
364            .client
365            .build_request(
366                reqwest::Method::GET,
367                &format!("/toolbox/{}/toolbox/files/search", sandbox_id),
368            )
369            .query(&[("path", path), ("pattern", pattern)])
370            .send()
371            .await?;
372
373        if response.status().is_success() {
374            let result: crate::models::SearchFilesResponse =
375                self.client.handle_response(response).await?;
376            Ok(result.files)
377        } else {
378            let error_text = response.text().await?;
379            Err(DaytonaError::FileError(format!(
380                "Failed to search files: {}",
381                error_text
382            )))
383        }
384    }
385
386    /// Set file permissions
387    pub async fn set_file_permissions(
388        &self,
389        sandbox_id: &Uuid,
390        path: &str,
391        mode: &str,
392        owner: Option<&str>,
393        group: Option<&str>,
394    ) -> Result<()> {
395        info!(
396            "Setting permissions {} for {} in sandbox {}",
397            mode, path, sandbox_id
398        );
399
400        let mut query_params = vec![("path", path), ("mode", mode)];
401
402        if let Some(owner_val) = owner {
403            query_params.push(("owner", owner_val));
404        }
405        if let Some(group_val) = group {
406            query_params.push(("group", group_val));
407        }
408
409        let response = self
410            .client
411            .build_request(
412                reqwest::Method::POST,
413                &format!("/toolbox/{}/toolbox/files/permissions", sandbox_id),
414            )
415            .query(&query_params)
416            .send()
417            .await?;
418
419        if response.status().is_success() {
420            debug!("File permissions set successfully for: {}", path);
421            Ok(())
422        } else {
423            let error_text = response.text().await?;
424            Err(DaytonaError::FileError(format!(
425                "Failed to set file permissions: {}",
426                error_text
427            )))
428        }
429    }
430}
431
432impl DaytonaClient {
433    /// Get file manager
434    pub fn files(&self) -> FileManager<'_> {
435        FileManager::new(self)
436    }
437}