use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::client::{HeyoClient, RequestOptions};
use crate::commands::encode_path;
use crate::errors::HeyoError;
const DEFAULT_MOUNT: &str = "/workspace";
#[derive(Clone)]
pub struct Files {
client: HeyoClient,
sandbox_id: String,
}
#[derive(Debug, Clone, Default)]
pub struct FileOptions {
pub mount_path: Option<String>,
}
#[derive(Debug, Clone)]
pub enum FileContent {
Text(String),
Bytes(Vec<u8>),
}
impl From<&str> for FileContent {
fn from(s: &str) -> Self {
FileContent::Text(s.to_string())
}
}
impl From<String> for FileContent {
fn from(s: String) -> Self {
FileContent::Text(s)
}
}
impl From<Vec<u8>> for FileContent {
fn from(b: Vec<u8>) -> Self {
FileContent::Bytes(b)
}
}
#[derive(Serialize)]
struct ReadRequest<'a> {
file_path: &'a str,
mount_path: &'a str,
}
#[derive(Deserialize)]
struct ReadResponse {
content: String,
}
#[derive(Serialize)]
struct WriteRequest<'a> {
file_path: &'a str,
mount_path: &'a str,
content: String,
}
impl Files {
pub(crate) fn new(client: HeyoClient, sandbox_id: String) -> Self {
Self { client, sandbox_id }
}
pub async fn read(
&self,
file_path: &str,
options: FileOptions,
) -> Result<Vec<u8>, HeyoError> {
let mount = options.mount_path.as_deref().unwrap_or(DEFAULT_MOUNT);
let body = ReadRequest {
file_path,
mount_path: mount,
};
let path = format!("/sandbox/{}/read-file", encode_path(&self.sandbox_id));
let resp: ReadResponse = self
.client
.request(Method::POST, &path, Some(&body), RequestOptions::default())
.await?;
BASE64
.decode(resp.content.as_bytes())
.map_err(|e| HeyoError::api(0, format!("invalid base64 in read-file response: {}", e)))
}
pub async fn read_text(
&self,
file_path: &str,
options: FileOptions,
) -> Result<String, HeyoError> {
let bytes = self.read(file_path, options).await?;
String::from_utf8(bytes).map_err(|e| HeyoError::api(0, format!("non-UTF-8 file: {}", e)))
}
pub async fn write(
&self,
file_path: &str,
content: impl Into<FileContent>,
options: FileOptions,
) -> Result<(), HeyoError> {
let mount = options.mount_path.as_deref().unwrap_or(DEFAULT_MOUNT);
let bytes = match content.into() {
FileContent::Text(s) => s.into_bytes(),
FileContent::Bytes(b) => b,
};
let body = WriteRequest {
file_path,
mount_path: mount,
content: BASE64.encode(&bytes),
};
let path = format!("/sandbox/{}/write-file", encode_path(&self.sandbox_id));
self.client
.request::<serde_json::Value>(Method::POST, &path, Some(&body), RequestOptions::default())
.await?;
Ok(())
}
}