Skip to main content

solari/
files.rs

1//! `files` namespace (`fs.*` control-WS RPCs): read / write / list / stat /
2//! mkdir / remove / rename. Mirrors the `files` surface of `handle.ts`.
3
4use base64::Engine;
5use serde_json::{Map, Value};
6
7use crate::client::Sandbox;
8use crate::error::SolariError;
9use crate::types::{FsEntry, FsStat};
10
11/// How much one `fs.upload` / `fs.download` call moves. A bigger frame (a 48 MiB file is a 64 MiB frame once base64'd) closes
12/// the control channel and fails every call in flight on it.
13const FILE_CHUNK_SIZE: usize = 4 << 20;
14/// The most `write` sends in one frame; `upload` has no such limit.
15const MAX_SINGLE_FRAME_BYTES: usize = 32 << 20;
16
17fn file_too_large(n: usize) -> SolariError {
18    SolariError::Other(format!(
19        "file too large for a single call ({n} bytes, max {MAX_SINGLE_FRAME_BYTES}): use files.upload()/files.download() or uploadUrl()"
20    ))
21}
22
23/// A control-channel reply saying the guest does not know the method (an older image).
24fn is_unknown_method(err: &SolariError) -> bool {
25    match err {
26        SolariError::Action { message, code, .. } => {
27            let m = format!("{message} {}", code.as_deref().unwrap_or("")).to_lowercase();
28            m.contains("unknown method") || m.contains("method not found") || m.contains("unknown_method")
29        }
30        _ => false,
31    }
32}
33
34/// The ergonomic `files` accessor returned by `Sandbox::files()`.
35pub struct Files<'a> {
36    pub(crate) sb: &'a Sandbox,
37}
38
39impl<'a> Files<'a> {
40    async fn call(&self, method: &str, params: Value) -> Result<Value, SolariError> {
41        self.sb.channel().connect().await?;
42        self.sb.channel().call(method, params).await
43    }
44
45    /// Read a file's raw bytes. It reads in 4 MiB chunks ([`download`](Self::download)), so a large file never travels as one
46    /// frame; a guest image that predates `fs.download` is read in one frame (`fs.read`).
47    pub async fn read(&self, path: &str) -> Result<Vec<u8>, SolariError> {
48        match self.download(path).await {
49            Err(e) if is_unknown_method(&e) => self.read_single_frame(path).await,
50            other => other,
51        }
52    }
53
54    async fn read_single_frame(&self, path: &str) -> Result<Vec<u8>, SolariError> {
55        let r = self.call("fs.read", json_obj(&[("path", Value::from(path))])).await?;
56        let b64 = r.get("base64").and_then(Value::as_str).unwrap_or("");
57        base64::engine::general_purpose::STANDARD
58            .decode(b64)
59            .map_err(|e| SolariError::Other(format!("fs.read bad base64: {e}")))
60    }
61
62    /// Read a whole file in 4 MiB chunks (`fs.download`) until the guest reports the end.
63    pub async fn download(&self, path: &str) -> Result<Vec<u8>, SolariError> {
64        let mut out = Vec::new();
65        let mut offset: u64 = 0;
66        loop {
67            let r = self
68                .call(
69                    "fs.download",
70                    json_obj(&[
71                        ("path", Value::from(path)),
72                        ("offset", Value::from(offset)),
73                        ("length", Value::from(FILE_CHUNK_SIZE)),
74                    ]),
75                )
76                .await?;
77            let b64 = r.get("base64").and_then(Value::as_str).unwrap_or("");
78            let chunk = base64::engine::general_purpose::STANDARD
79                .decode(b64)
80                .map_err(|e| SolariError::Other(format!("fs.download bad base64: {e}")))?;
81            out.extend_from_slice(&chunk);
82            if r.get("eof").and_then(Value::as_bool).unwrap_or(false) {
83                return Ok(out);
84            }
85            let next = r.get("nextOffset").and_then(Value::as_u64).unwrap_or(offset + chunk.len() as u64);
86            if next <= offset {
87                return Err(SolariError::Other(format!("fs.download made no progress at offset {offset}")));
88            }
89            offset = next;
90        }
91    }
92
93    /// Write data to a file in 4 MiB chunks (`fs.upload`): the first call truncates, later calls append at their offset.
94    /// An empty file is one zero-length call. Unlike [`write`](Self::write) there is no size limit.
95    pub async fn upload(&self, path: &str, data: impl AsRef<[u8]>) -> Result<(), SolariError> {
96        let data = data.as_ref();
97        let mut offset = 0usize;
98        loop {
99            let end = (offset + FILE_CHUNK_SIZE).min(data.len());
100            let mut p = Map::new();
101            p.insert("path".into(), Value::from(path));
102            p.insert("base64".into(), Value::String(base64::engine::general_purpose::STANDARD.encode(&data[offset..end])));
103            p.insert("offset".into(), Value::from(offset));
104            p.insert("truncate".into(), Value::Bool(offset == 0));
105            self.call("fs.upload", Value::Object(p)).await?;
106            if end >= data.len() {
107                return Ok(());
108            }
109            offset = end;
110        }
111    }
112
113    /// Read a file as UTF-8 text.
114    pub async fn read_text(&self, path: &str) -> Result<String, SolariError> {
115        let bytes = self.read(path).await?;
116        Ok(String::from_utf8_lossy(&bytes).into_owned())
117    }
118
119    /// Write bytes to a file (`fs.write`). `mode` is an optional unix mode. The data travels as one frame, so more than
120    /// 32 MiB is refused before anything is sent (a frame that large would close the control channel and fail every call
121    /// on it): use [`upload`](Self::upload).
122    pub async fn write(
123        &self,
124        path: &str,
125        data: impl AsRef<[u8]>,
126        mode: Option<i64>,
127    ) -> Result<(), SolariError> {
128        if data.as_ref().len() > MAX_SINGLE_FRAME_BYTES {
129            return Err(file_too_large(data.as_ref().len()));
130        }
131        let b64 = base64::engine::general_purpose::STANDARD.encode(data.as_ref());
132        let mut p = Map::new();
133        p.insert("path".into(), Value::from(path));
134        p.insert("base64".into(), Value::String(b64));
135        if let Some(m) = mode {
136            p.insert("mode".into(), Value::from(m));
137        }
138        self.call("fs.write", Value::Object(p)).await.map(|_| ())
139    }
140
141    /// List a directory (`fs.list`).
142    pub async fn list(&self, path: &str) -> Result<Vec<FsEntry>, SolariError> {
143        let r = self.call("fs.list", json_obj(&[("path", Value::from(path))])).await?;
144        let entries = r.get("entries").cloned().unwrap_or(Value::Array(vec![]));
145        serde_json::from_value(entries)
146            .map_err(|e| SolariError::Other(format!("fs.list bad entries: {e}")))
147    }
148
149    /// Stat a path (`fs.stat`).
150    pub async fn stat(&self, path: &str) -> Result<FsStat, SolariError> {
151        let r = self.call("fs.stat", json_obj(&[("path", Value::from(path))])).await?;
152        serde_json::from_value(r).map_err(|e| SolariError::Other(format!("fs.stat bad: {e}")))
153    }
154
155    /// Create a directory and parents (`fs.mkdir`).
156    pub async fn mkdir(&self, path: &str) -> Result<(), SolariError> {
157        self.call("fs.mkdir", json_obj(&[("path", Value::from(path))]))
158            .await
159            .map(|_| ())
160    }
161
162    /// Remove a path (`fs.remove`); `recursive` for directories.
163    pub async fn remove(&self, path: &str, recursive: bool) -> Result<(), SolariError> {
164        self.call(
165            "fs.remove",
166            json_obj(&[("path", Value::from(path)), ("recursive", Value::from(recursive))]),
167        )
168        .await
169        .map(|_| ())
170    }
171
172    /// Rename/move a path (`fs.rename`).
173    pub async fn rename(&self, from: &str, to: &str) -> Result<(), SolariError> {
174        self.call(
175            "fs.rename",
176            json_obj(&[("from", Value::from(from)), ("to", Value::from(to))]),
177        )
178        .await
179        .map(|_| ())
180    }
181}
182
183fn json_obj(pairs: &[(&str, Value)]) -> Value {
184    let mut m = Map::new();
185    for (k, v) in pairs {
186        m.insert((*k).to_string(), v.clone());
187    }
188    Value::Object(m)
189}