1use tracing::{debug, info};
4use uuid::Uuid;
5
6use crate::{
7 client::DaytonaClient,
8 error::{DaytonaError, Result},
9 models::{FileInfo, FindResult},
10};
11
12pub struct FileManager<'a> {
14 client: &'a DaytonaClient,
15}
16
17impl<'a> FileManager<'a> {
18 pub fn new(client: &'a DaytonaClient) -> Self {
20 Self { client }
21 }
22
23 pub async fn upload(&self, sandbox_id: &Uuid, path: &str, content: &[u8]) -> Result<()> {
25 info!("Uploading file to sandbox {}: {}", sandbox_id, path);
26
27 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn files(&self) -> FileManager<'_> {
435 FileManager::new(self)
436 }
437}