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/// The ergonomic `files` accessor returned by `Sandbox::files()`.
12pub struct Files<'a> {
13    pub(crate) sb: &'a Sandbox,
14}
15
16impl<'a> Files<'a> {
17    async fn call(&self, method: &str, params: Value) -> Result<Value, SolariError> {
18        self.sb.channel().connect().await?;
19        self.sb.channel().call(method, params).await
20    }
21
22    /// Read a file's raw bytes (`fs.read`).
23    pub async fn read(&self, path: &str) -> Result<Vec<u8>, SolariError> {
24        let r = self.call("fs.read", json_obj(&[("path", Value::from(path))])).await?;
25        let b64 = r.get("base64").and_then(Value::as_str).unwrap_or("");
26        base64::engine::general_purpose::STANDARD
27            .decode(b64)
28            .map_err(|e| SolariError::Other(format!("fs.read bad base64: {e}")))
29    }
30
31    /// Read a file as UTF-8 text.
32    pub async fn read_text(&self, path: &str) -> Result<String, SolariError> {
33        let bytes = self.read(path).await?;
34        Ok(String::from_utf8_lossy(&bytes).into_owned())
35    }
36
37    /// Write bytes to a file (`fs.write`). `mode` is an optional unix mode.
38    pub async fn write(
39        &self,
40        path: &str,
41        data: impl AsRef<[u8]>,
42        mode: Option<i64>,
43    ) -> Result<(), SolariError> {
44        let b64 = base64::engine::general_purpose::STANDARD.encode(data.as_ref());
45        let mut p = Map::new();
46        p.insert("path".into(), Value::from(path));
47        p.insert("base64".into(), Value::String(b64));
48        if let Some(m) = mode {
49            p.insert("mode".into(), Value::from(m));
50        }
51        self.call("fs.write", Value::Object(p)).await.map(|_| ())
52    }
53
54    /// List a directory (`fs.list`).
55    pub async fn list(&self, path: &str) -> Result<Vec<FsEntry>, SolariError> {
56        let r = self.call("fs.list", json_obj(&[("path", Value::from(path))])).await?;
57        let entries = r.get("entries").cloned().unwrap_or(Value::Array(vec![]));
58        serde_json::from_value(entries)
59            .map_err(|e| SolariError::Other(format!("fs.list bad entries: {e}")))
60    }
61
62    /// Stat a path (`fs.stat`).
63    pub async fn stat(&self, path: &str) -> Result<FsStat, SolariError> {
64        let r = self.call("fs.stat", json_obj(&[("path", Value::from(path))])).await?;
65        serde_json::from_value(r).map_err(|e| SolariError::Other(format!("fs.stat bad: {e}")))
66    }
67
68    /// Create a directory and parents (`fs.mkdir`).
69    pub async fn mkdir(&self, path: &str) -> Result<(), SolariError> {
70        self.call("fs.mkdir", json_obj(&[("path", Value::from(path))]))
71            .await
72            .map(|_| ())
73    }
74
75    /// Remove a path (`fs.remove`); `recursive` for directories.
76    pub async fn remove(&self, path: &str, recursive: bool) -> Result<(), SolariError> {
77        self.call(
78            "fs.remove",
79            json_obj(&[("path", Value::from(path)), ("recursive", Value::from(recursive))]),
80        )
81        .await
82        .map(|_| ())
83    }
84
85    /// Rename/move a path (`fs.rename`).
86    pub async fn rename(&self, from: &str, to: &str) -> Result<(), SolariError> {
87        self.call(
88            "fs.rename",
89            json_obj(&[("from", Value::from(from)), ("to", Value::from(to))]),
90        )
91        .await
92        .map(|_| ())
93    }
94}
95
96fn json_obj(pairs: &[(&str, Value)]) -> Value {
97    let mut m = Map::new();
98    for (k, v) in pairs {
99        m.insert((*k).to_string(), v.clone());
100    }
101    Value::Object(m)
102}