Skip to main content

bsdkrun_sdk/
filesystem.rs

1//! Files in a running sandbox — [`Sandbox::fs`](crate::Sandbox::fs).
2//!
3//! Every call goes through the guest's exec agent, so the sandbox has to be
4//! running; there is no offline write.
5
6use std::path::Path;
7
8use crate::error::{Error, Result};
9use crate::process::{run, run_binary};
10
11/// Read and write files inside a running microVM.
12///
13/// ```no_run
14/// # use bsdkrun_sdk::Sandbox;
15/// # fn main() -> bsdkrun_sdk::Result<()> {
16/// let sbx = Sandbox::get("web")?;
17/// sbx.fs().write_file("/app/main.py", b"print('hi')")?;
18/// let out = sbx.fs().read_to_string("/app/out.json")?;
19/// sbx.fs().upload("./src", "/app/src")?;
20/// sbx.fs().download("/app/dist", "./dist", true)?;
21/// # Ok(())
22/// # }
23/// ```
24pub struct FileSystem {
25    id: String,
26}
27
28impl FileSystem {
29    pub(crate) fn new(id: impl Into<String>) -> Self {
30        FileSystem { id: id.into() }
31    }
32
33    /// Write `data` to `path` in the guest, creating parent directories.
34    pub fn write_file(&self, path: &str, data: &[u8]) -> Result<()> {
35        let args = vec![
36            "cp".to_string(),
37            "-".to_string(),
38            format!("{}:{}", self.id, path),
39        ];
40        let res = run_binary(args, Some(data))?;
41        if res.exit_code != 0 {
42            return Err(transfer_error(&res.stderr, path));
43        }
44        Ok(())
45    }
46
47    /// Read `path` from the guest as bytes.
48    pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
49        let args = vec![
50            "cp".to_string(),
51            format!("{}:{}", self.id, path),
52            "-".to_string(),
53        ];
54        let res = run_binary(args, None)?;
55        if res.exit_code != 0 {
56            return Err(transfer_error(&res.stderr, path));
57        }
58        Ok(res.stdout)
59    }
60
61    /// Read `path` from the guest and decode it as UTF-8 (lossily).
62    pub fn read_to_string(&self, path: &str) -> Result<String> {
63        Ok(String::from_utf8_lossy(&self.read_file(path)?).into_owned())
64    }
65
66    /// Copy a host file or directory into the guest. A directory's *contents*
67    /// land in `remote_path`, so `upload("./src", "/app/src")` leaves the
68    /// guest's `/app/src` holding what `./src` holds.
69    ///
70    /// Whether it recurses is decided by looking at `local_path`, so callers do
71    /// not have to say which kind of thing they are copying.
72    pub fn upload(&self, local_path: impl AsRef<Path>, remote_path: &str) -> Result<()> {
73        let local = local_path.as_ref();
74        let meta = std::fs::metadata(local).map_err(|e| Error::FileTransfer {
75            path: local.display().to_string(),
76            message: format!("cannot upload {}: {e}", local.display()),
77        })?;
78        let mut args = vec!["cp".to_string()];
79        if meta.is_dir() {
80            args.push("-r".to_string());
81        }
82        args.push(local.display().to_string());
83        args.push(format!("{}:{}", self.id, remote_path));
84        let res = run(args)?;
85        if res.exit_code != 0 {
86            return Err(transfer_error(&res.stderr, &local.display().to_string()));
87        }
88        Ok(())
89    }
90
91    /// Copy a file or directory out of the guest onto the host. `recursive`
92    /// selects a directory; unlike [`upload`](Self::upload) it cannot be
93    /// detected here, because the path lives in the guest and answering would
94    /// cost an extra round trip.
95    pub fn download(
96        &self,
97        remote_path: &str,
98        local_path: impl AsRef<Path>,
99        recursive: bool,
100    ) -> Result<()> {
101        let mut args = vec!["cp".to_string()];
102        if recursive {
103            args.push("-r".to_string());
104        }
105        args.push(format!("{}:{}", self.id, remote_path));
106        args.push(local_path.as_ref().display().to_string());
107        let res = run(args)?;
108        if res.exit_code != 0 {
109            return Err(transfer_error(&res.stderr, remote_path));
110        }
111        Ok(())
112    }
113}
114
115/// The CLI already explains these well; strip its `Error: ` prefix.
116fn transfer_error(stderr: &str, path: &str) -> Error {
117    let text = stderr.trim().trim_start_matches("Error: ").trim();
118    Error::FileTransfer {
119        path: path.to_string(),
120        message: if text.is_empty() {
121            format!("file transfer failed for {path}")
122        } else {
123            text.to_string()
124        },
125    }
126}