use crate::backend::conch::agent;
use crate::backend::conch::backend::{shell_quote, ConchBackendState, ConchUploadFile};
use crate::backend::conch::exec::ConchExec;
use agent_contracts::backend::{
capability::{
export::ExportFileRequest,
filesystem::{
ReadBytesRequest, TempPathKind, TempPathRequest, WriteBytesOutcome, WriteBytesRequest,
WriteMode,
},
OperationExport, OperationFileSystem,
},
BackendPath, ExportedFileHandle, ExportedFileMeta, ExportedFileReader, OperationError,
PathKind, PathStat, SharedExportedFileHandle,
};
use async_trait::async_trait;
use std::io::Cursor;
use std::sync::Arc;
pub(crate) struct ConchFileSystem {
state: Arc<ConchBackendState>,
exec: ConchExec,
}
impl ConchFileSystem {
pub(crate) fn new(state: Arc<ConchBackendState>) -> Self {
Self {
exec: ConchExec::new(Arc::clone(&state)),
state,
}
}
}
struct ConchExportedFileHandle {
state: Arc<ConchBackendState>,
path: BackendPath,
metadata: ExportedFileMeta,
}
#[async_trait]
impl ExportedFileHandle for ConchExportedFileHandle {
fn metadata(&self) -> &ExportedFileMeta {
&self.metadata
}
async fn open_read(&self) -> Result<ExportedFileReader, OperationError> {
let content = agent::get_file(&self.state, self.path.0.as_str()).await?;
Ok(Box::new(Cursor::new(content)))
}
}
#[async_trait]
impl OperationFileSystem for ConchFileSystem {
async fn stat(&self, path: &BackendPath) -> Result<PathStat, OperationError> {
self.state.ensure_active()?;
let quoted = shell_quote(path.0.as_str());
let script = format!(
"if [ -e {quoted} ]; then\n if [ -f {quoted} ]; then kind=file; elif [ -d {quoted} ]; then kind=directory; elif [ -L {quoted} ]; then kind=symlink; else kind=other; fi\n if [ -f {quoted} ]; then size=$(wc -c < {quoted}); else size=; fi\n mtime=$(stat -c %Y {quoted} 2>/dev/null || true)\n printf 'exists=true\\nkind=%s\\nsize=%s\\nmtime=%s\\n' \"$kind\" \"$size\" \"$mtime\"\nelse\n printf 'exists=false\\n'\nfi"
);
let output = self.exec.run_shell_script(script.as_str(), None).await?;
parse_stat_output(String::from_utf8_lossy(output.stdout.as_slice()).as_ref())
}
async fn read_bytes(&self, request: ReadBytesRequest) -> Result<Vec<u8>, OperationError> {
agent::get_file(&self.state, request.path.0.as_str()).await
}
async fn write_bytes(
&self,
request: WriteBytesRequest,
) -> Result<WriteBytesOutcome, OperationError> {
self.state.ensure_active()?;
if matches!(request.mode, WriteMode::Create) {
let stat = self.stat(&request.path).await?;
if stat.exists {
return Err(OperationError::AlreadyExists {
path: request.path.0,
});
}
}
if matches!(request.mode, WriteMode::AtomicOverwrite) {
return Err(OperationError::Unsupported {
message: "conch backend does not support atomic overwrite".to_string(),
});
}
let before = self.stat(&request.path).await?;
let uploaded = agent::post_files(
&self.state,
vec![ConchUploadFile {
filepath: request.path.0.clone(),
content: request.content,
}],
)
.await?;
if uploaded != 1 {
return Err(OperationError::Transport {
message: format!("unexpected uploaded_count from conch agent: {uploaded}"),
});
}
Ok(WriteBytesOutcome {
path: request.path,
created: !before.exists,
})
}
async fn create_dir_all(&self, path: &BackendPath) -> Result<(), OperationError> {
let script = format!("mkdir -p {}", shell_quote(path.0.as_str()));
let output = self.exec.run_shell_script(script.as_str(), None).await?;
if output.exit_code == Some(0) {
return Ok(());
}
Err(OperationError::ExecutionFailed {
message: String::from_utf8_lossy(output.stderr.as_slice()).to_string(),
})
}
async fn temp_path(&self, request: TempPathRequest) -> Result<BackendPath, OperationError> {
let parent = request
.preferred_parent
.unwrap_or_else(|| self.state.temp_root.clone());
let quoted_parent = shell_quote(parent.0.as_str());
let prefix = shell_quote(request.prefix.as_deref().unwrap_or("tmp-"));
let suffix = shell_quote(request.suffix.as_deref().unwrap_or(""));
let creation = match request.kind {
TempPathKind::File => "touch \"$path\"",
TempPathKind::Directory => "mkdir \"$path\"",
};
let script = format!(
"mkdir -p {quoted_parent}\nprefix={prefix}\nsuffix={suffix}\nwhile true; do\n path=\"{parent}/$prefix$RANDOM$(date +%s%N)$suffix\"\n if [ ! -e \"$path\" ]; then\n {creation}\n printf '%s' \"$path\"\n break\n fi\ndone",
parent = parent.0,
);
let output = self.exec.run_shell_script(script.as_str(), None).await?;
let text = String::from_utf8_lossy(output.stdout.as_slice())
.trim()
.to_string();
Ok(BackendPath(text))
}
}
#[async_trait]
impl OperationExport for ConchFileSystem {
async fn export_file(
&self,
request: ExportFileRequest,
) -> Result<SharedExportedFileHandle, OperationError> {
let stat = self.stat(&request.path).await?;
if !stat.exists {
return Err(OperationError::NotFound {
path: request.path.0.clone(),
});
}
if stat.kind != Some(PathKind::File) {
return Err(OperationError::NotFile {
path: request.path.0.clone(),
});
}
let file_name = request.preferred_name.unwrap_or_else(|| {
request
.path
.0
.rsplit('/')
.next()
.unwrap_or("exported-file")
.to_string()
});
Ok(Arc::new(ConchExportedFileHandle {
state: Arc::clone(&self.state),
path: request.path,
metadata: ExportedFileMeta {
file_name,
size_bytes: stat.size_bytes,
media_type: None,
},
}))
}
}
fn parse_stat_output(output: &str) -> Result<PathStat, OperationError> {
let mut exists = false;
let mut kind = None;
let mut size_bytes = None;
let mut modified_at = None;
for line in output.lines() {
if let Some(value) = line.strip_prefix("exists=") {
exists = value == "true";
} else if let Some(value) = line.strip_prefix("kind=") {
kind = match value {
"file" => Some(PathKind::File),
"directory" => Some(PathKind::Directory),
"symlink" => Some(PathKind::Symlink),
"other" => Some(PathKind::Other),
_ => None,
};
} else if let Some(value) = line.strip_prefix("size=") {
if !value.is_empty() {
size_bytes = value.parse::<u64>().ok();
}
} else if let Some(value) = line.strip_prefix("mtime=") {
if let Ok(seconds) = value.parse::<u64>() {
modified_at = Some(
std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(seconds),
);
}
}
}
Ok(PathStat {
exists,
kind,
size_bytes,
modified_at,
})
}