use actix_web::{web, HttpResponse};
use tokio::fs;
use crate::{app_state::AppState, error::AppError};
use super::types::{SaveWorkflowRequest, WorkflowGetResponse, WorkflowListItem};
use super::validation::is_safe_workflow_name;
pub async fn list_workflows(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
let dir = app_state.app_data_dir.join("workflows");
fs::create_dir_all(&dir).await?;
let mut entries = fs::read_dir(&dir).await?;
let mut workflows: Vec<WorkflowListItem> = Vec::new();
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if !file_type.is_file() {
continue;
}
let path = entry.path();
if path.extension().and_then(|value| value.to_str()) != Some("md") {
continue;
}
let Some(stem) = path.file_stem().and_then(|value| value.to_str()) else {
continue;
};
let filename = path
.file_name()
.and_then(|value| value.to_str())
.unwrap_or_default()
.to_string();
let metadata = entry.metadata().await?;
workflows.push(WorkflowListItem {
name: stem.to_string(),
filename,
size: metadata.len(),
modified_at: None,
});
}
workflows.sort_by(|left, right| left.name.cmp(&right.name));
Ok(HttpResponse::Ok().json(workflows))
}
pub async fn get_workflow(
app_state: web::Data<AppState>,
workflow_name: web::Path<String>,
) -> Result<HttpResponse, AppError> {
let name = workflow_name.into_inner();
if !is_safe_workflow_name(&name) {
return Err(AppError::NotFound("Workflow".to_string()));
}
let dir = app_state.app_data_dir.join("workflows");
fs::create_dir_all(&dir).await?;
let filename = format!("{name}.md");
let file_path = dir.join(&filename);
let metadata = match fs::metadata(&file_path).await {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(AppError::NotFound(format!("Workflow '{name}'")));
}
Err(error) => return Err(AppError::StorageError(error)),
};
let content = fs::read_to_string(&file_path).await?;
Ok(HttpResponse::Ok().json(WorkflowGetResponse {
name,
filename,
content,
size: metadata.len(),
modified_at: None,
}))
}
pub async fn save_workflow(
app_state: web::Data<AppState>,
payload: web::Json<SaveWorkflowRequest>,
) -> Result<HttpResponse, AppError> {
let name = payload.name.trim();
if !is_safe_workflow_name(name) {
return Err(AppError::BadRequest("Invalid workflow name".to_string()));
}
let dir = app_state.app_data_dir.join("workflows");
fs::create_dir_all(&dir).await?;
let file_path = dir.join(format!("{}.md", name));
fs::write(&file_path, &payload.content).await?;
Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true,
"path": file_path.to_string_lossy()
})))
}
pub async fn delete_workflow(
app_state: web::Data<AppState>,
workflow_name: web::Path<String>,
) -> Result<HttpResponse, AppError> {
let name = workflow_name.into_inner();
if !is_safe_workflow_name(&name) {
return Err(AppError::BadRequest("Invalid workflow name".to_string()));
}
let dir = app_state.app_data_dir.join("workflows");
let file_path = dir.join(format!("{}.md", name));
if !file_path.exists() {
return Err(AppError::NotFound(format!("Workflow '{}'", name)));
}
fs::remove_file(&file_path).await?;
Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
}