Skip to main content

a3s_box_sdk/sandbox/
filesystem.rs

1use std::sync::Arc;
2
3use a3s_box_core::{
4    FileOp, FileRequest, FileResponse, FilesystemEntry, FilesystemOp, FilesystemRequest,
5    FilesystemResponse,
6};
7use base64::engine::general_purpose::STANDARD;
8use base64::Engine;
9
10use super::SandboxInner;
11use crate::{ClientError, Result};
12
13/// Metadata returned after a successful file write.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct WriteInfo {
16    pub path: String,
17    pub size: u64,
18}
19
20/// Optional guest identity for a filesystem operation.
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct FilesystemOptions {
23    pub user: Option<String>,
24}
25
26impl FilesystemOptions {
27    pub fn user(mut self, user: impl Into<String>) -> Self {
28        self.user = Some(user.into());
29        self
30    }
31}
32
33/// E2B-style file namespace attached to a local [`super::Sandbox`].
34#[derive(Clone)]
35pub struct Filesystem {
36    pub(crate) inner: Arc<SandboxInner>,
37}
38
39impl std::fmt::Debug for Filesystem {
40    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        formatter
42            .debug_struct("Filesystem")
43            .field("sandbox_id", &self.inner.execution_id)
44            .finish()
45    }
46}
47
48impl Filesystem {
49    pub async fn write(
50        &self,
51        path: impl Into<String>,
52        data: impl AsRef<[u8]>,
53    ) -> Result<WriteInfo> {
54        self.write_with_options(path, data, FilesystemOptions::default())
55            .await
56    }
57
58    pub async fn write_with_options(
59        &self,
60        path: impl Into<String>,
61        data: impl AsRef<[u8]>,
62        options: FilesystemOptions,
63    ) -> Result<WriteInfo> {
64        let path = path.into();
65        let response = self
66            .transfer(FileRequest {
67                op: FileOp::Upload,
68                guest_path: path.clone(),
69                data: Some(STANDARD.encode(data.as_ref())),
70                user: options.user,
71            })
72            .await?;
73        require_file_success(response).map(|response| WriteInfo {
74            path,
75            size: response.size,
76        })
77    }
78
79    pub async fn read(&self, path: impl Into<String>) -> Result<Vec<u8>> {
80        self.read_with_options(path, FilesystemOptions::default())
81            .await
82    }
83
84    pub async fn read_with_options(
85        &self,
86        path: impl Into<String>,
87        options: FilesystemOptions,
88    ) -> Result<Vec<u8>> {
89        let response = self
90            .transfer(FileRequest {
91                op: FileOp::Download,
92                guest_path: path.into(),
93                data: None,
94                user: options.user,
95            })
96            .await?;
97        let response = require_file_success(response)?;
98        STANDARD
99            .decode(response.data.unwrap_or_default())
100            .map_err(|error| {
101                ClientError::Guest(format!("guest returned invalid file data: {error}"))
102            })
103    }
104
105    pub async fn read_text(&self, path: impl Into<String>) -> Result<String> {
106        String::from_utf8(self.read(path).await?)
107            .map_err(|error| ClientError::Guest(format!("guest file is not valid UTF-8: {error}")))
108    }
109
110    pub async fn stat(&self, path: impl Into<String>) -> Result<FilesystemEntry> {
111        self.stat_with_options(path, FilesystemOptions::default())
112            .await
113    }
114
115    pub async fn stat_with_options(
116        &self,
117        path: impl Into<String>,
118        options: FilesystemOptions,
119    ) -> Result<FilesystemEntry> {
120        let response = self
121            .filesystem(FilesystemRequest {
122                op: FilesystemOp::Stat,
123                path: path.into(),
124                destination: None,
125                depth: 0,
126                user: options.user,
127            })
128            .await?;
129        let mut response = require_filesystem_success(response)?;
130        response.entry.take().ok_or_else(|| {
131            ClientError::Guest("guest stat response did not include an entry".to_string())
132        })
133    }
134
135    pub async fn exists(&self, path: impl Into<String>) -> Result<bool> {
136        match self.stat(path).await {
137            Ok(_) => Ok(true),
138            Err(ClientError::Guest(message))
139                if message.to_ascii_lowercase().contains("not found") =>
140            {
141                Ok(false)
142            }
143            Err(error) => Err(error),
144        }
145    }
146
147    pub async fn list(&self, path: impl Into<String>, depth: u32) -> Result<Vec<FilesystemEntry>> {
148        self.list_with_options(path, depth, FilesystemOptions::default())
149            .await
150    }
151
152    pub async fn list_with_options(
153        &self,
154        path: impl Into<String>,
155        depth: u32,
156        options: FilesystemOptions,
157    ) -> Result<Vec<FilesystemEntry>> {
158        let response = self
159            .filesystem(FilesystemRequest {
160                op: FilesystemOp::ListDir,
161                path: path.into(),
162                destination: None,
163                depth,
164                user: options.user,
165            })
166            .await?;
167        Ok(require_filesystem_success(response)?.entries)
168    }
169
170    pub async fn make_dir(&self, path: impl Into<String>) -> Result<()> {
171        self.make_dir_with_options(path, FilesystemOptions::default())
172            .await
173    }
174
175    pub async fn make_dir_with_options(
176        &self,
177        path: impl Into<String>,
178        options: FilesystemOptions,
179    ) -> Result<()> {
180        self.mutate(FilesystemRequest {
181            op: FilesystemOp::MakeDir,
182            path: path.into(),
183            destination: None,
184            depth: 0,
185            user: options.user,
186        })
187        .await
188    }
189
190    pub async fn move_path(
191        &self,
192        source: impl Into<String>,
193        destination: impl Into<String>,
194    ) -> Result<()> {
195        self.move_path_with_options(source, destination, FilesystemOptions::default())
196            .await
197    }
198
199    pub async fn move_path_with_options(
200        &self,
201        source: impl Into<String>,
202        destination: impl Into<String>,
203        options: FilesystemOptions,
204    ) -> Result<()> {
205        self.mutate(FilesystemRequest {
206            op: FilesystemOp::Move,
207            path: source.into(),
208            destination: Some(destination.into()),
209            depth: 0,
210            user: options.user,
211        })
212        .await
213    }
214
215    pub async fn remove(&self, path: impl Into<String>) -> Result<()> {
216        self.remove_with_options(path, FilesystemOptions::default())
217            .await
218    }
219
220    pub async fn remove_with_options(
221        &self,
222        path: impl Into<String>,
223        options: FilesystemOptions,
224    ) -> Result<()> {
225        self.mutate(FilesystemRequest {
226            op: FilesystemOp::Remove,
227            path: path.into(),
228            destination: None,
229            depth: 0,
230            user: options.user,
231        })
232        .await
233    }
234
235    async fn mutate(&self, request: FilesystemRequest) -> Result<()> {
236        require_filesystem_success(self.filesystem(request).await?).map(|_| ())
237    }
238
239    async fn transfer(&self, request: FileRequest) -> Result<FileResponse> {
240        let (_, generation) = self.inner.active_execution()?;
241        #[cfg(unix)]
242        {
243            self.inner
244                .client
245                .transfer_execution_file(&self.inner.execution_id, generation, request)
246                .await
247        }
248        #[cfg(not(unix))]
249        {
250            let _ = (generation, request);
251            Err(ClientError::Execution(
252                a3s_box_core::ExecutionManagerError::Unavailable(
253                    "local file sessions are not available on this host".to_string(),
254                ),
255            ))
256        }
257    }
258
259    async fn filesystem(&self, request: FilesystemRequest) -> Result<FilesystemResponse> {
260        let (_, generation) = self.inner.active_execution()?;
261        #[cfg(unix)]
262        {
263            self.inner
264                .client
265                .filesystem_execution(&self.inner.execution_id, generation, request)
266                .await
267        }
268        #[cfg(not(unix))]
269        {
270            let _ = (generation, request);
271            Err(ClientError::Execution(
272                a3s_box_core::ExecutionManagerError::Unavailable(
273                    "local filesystem sessions are not available on this host".to_string(),
274                ),
275            ))
276        }
277    }
278}
279
280fn require_file_success(response: FileResponse) -> Result<FileResponse> {
281    if response.success {
282        Ok(response)
283    } else {
284        Err(ClientError::Guest(response.error.unwrap_or_else(|| {
285            "guest file operation failed".to_string()
286        })))
287    }
288}
289
290fn require_filesystem_success(response: FilesystemResponse) -> Result<FilesystemResponse> {
291    if response.success {
292        Ok(response)
293    } else {
294        Err(ClientError::Guest(response.error.unwrap_or_else(|| {
295            "guest filesystem operation failed".to_string()
296        })))
297    }
298}