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#[derive(Clone)]
13pub struct Files {
14 client: HeyoClient,
15 sandbox_id: String,
16}
17
18#[derive(Debug, Clone, Default)]
20pub struct FileOptions {
21 pub mount_path: Option<String>,
23}
24
25#[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 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 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 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}