Skip to main content

heyo_sdk/
files.rs

1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
2use reqwest::Method;
3use serde::{Deserialize, Serialize};
4
5use crate::client::{HeyoClient, RequestOptions};
6use crate::commands::encode_path;
7use crate::errors::HeyoError;
8
9const DEFAULT_MOUNT: &str = "/workspace";
10
11/// File-system surface, obtained via [`crate::Sandbox::files`].
12#[derive(Clone)]
13pub struct Files {
14    client: HeyoClient,
15    sandbox_id: String,
16}
17
18/// Options for `Files::read` / `Files::write`.
19#[derive(Debug, Clone, Default)]
20pub struct FileOptions {
21    /// Mount on which the path is rooted. Defaults to `/workspace`.
22    pub mount_path: Option<String>,
23}
24
25/// Discriminated content for `Files::write`. `String` is encoded as UTF-8;
26/// `Bytes` is written verbatim.
27#[derive(Debug, Clone)]
28pub enum FileContent {
29    Text(String),
30    Bytes(Vec<u8>),
31}
32
33impl From<&str> for FileContent {
34    fn from(s: &str) -> Self {
35        FileContent::Text(s.to_string())
36    }
37}
38impl From<String> for FileContent {
39    fn from(s: String) -> Self {
40        FileContent::Text(s)
41    }
42}
43impl From<Vec<u8>> for FileContent {
44    fn from(b: Vec<u8>) -> Self {
45        FileContent::Bytes(b)
46    }
47}
48
49#[derive(Serialize)]
50struct ReadRequest<'a> {
51    file_path: &'a str,
52    mount_path: &'a str,
53}
54
55#[derive(Deserialize)]
56struct ReadResponse {
57    content: String,
58}
59
60#[derive(Serialize)]
61struct WriteRequest<'a> {
62    file_path: &'a str,
63    mount_path: &'a str,
64    content: String,
65}
66
67impl Files {
68    pub(crate) fn new(client: HeyoClient, sandbox_id: String) -> Self {
69        Self { client, sandbox_id }
70    }
71
72    /// Read the file at `file_path` (relative to the mount). Returns the
73    /// raw bytes — use `read_text` for UTF-8 convenience.
74    pub async fn read(
75        &self,
76        file_path: &str,
77        options: FileOptions,
78    ) -> Result<Vec<u8>, HeyoError> {
79        let mount = options.mount_path.as_deref().unwrap_or(DEFAULT_MOUNT);
80        let body = ReadRequest {
81            file_path,
82            mount_path: mount,
83        };
84        let path = format!("/sandbox/{}/read-file", encode_path(&self.sandbox_id));
85        let resp: ReadResponse = self
86            .client
87            .request(Method::POST, &path, Some(&body), RequestOptions::default())
88            .await?;
89        BASE64
90            .decode(resp.content.as_bytes())
91            .map_err(|e| HeyoError::api(0, format!("invalid base64 in read-file response: {}", e)))
92    }
93
94    /// Convenience wrapper around `read` that decodes the bytes as UTF-8.
95    pub async fn read_text(
96        &self,
97        file_path: &str,
98        options: FileOptions,
99    ) -> Result<String, HeyoError> {
100        let bytes = self.read(file_path, options).await?;
101        String::from_utf8(bytes).map_err(|e| HeyoError::api(0, format!("non-UTF-8 file: {}", e)))
102    }
103
104    /// Write `content` to `file_path`.
105    pub async fn write(
106        &self,
107        file_path: &str,
108        content: impl Into<FileContent>,
109        options: FileOptions,
110    ) -> Result<(), HeyoError> {
111        let mount = options.mount_path.as_deref().unwrap_or(DEFAULT_MOUNT);
112        let bytes = match content.into() {
113            FileContent::Text(s) => s.into_bytes(),
114            FileContent::Bytes(b) => b,
115        };
116        let body = WriteRequest {
117            file_path,
118            mount_path: mount,
119            content: BASE64.encode(&bytes),
120        };
121        let path = format!("/sandbox/{}/write-file", encode_path(&self.sandbox_id));
122        self.client
123            .request::<serde_json::Value>(Method::POST, &path, Some(&body), RequestOptions::default())
124            .await?;
125        Ok(())
126    }
127}