1use base64::Engine;
5use serde_json::{Map, Value};
6
7use crate::client::Sandbox;
8use crate::error::SolariError;
9use crate::types::{FsEntry, FsStat};
10
11const FILE_CHUNK_SIZE: usize = 4 << 20;
14const 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
23fn 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
34pub 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 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 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 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 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 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 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 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 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 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 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}