1use axum::{
8 Json,
9 extract::{Path as AxumPath, Query, State},
10 http::{HeaderMap, StatusCode},
11 response::{IntoResponse, Response},
12};
13use base64::Engine;
14use serde::{Deserialize, Serialize};
15use zeroclaw_runtime::browse::{
16 BrowseEntry, BrowseError, delete_agent_workspace_path, list_agent_workspace, list_directory,
17 make_agent_workspace_directory, make_directory, move_agent_workspace_path,
18 read_agent_workspace_file, remove_directory,
19};
20
21use super::AppState;
22use super::api::require_auth;
23
24#[derive(Debug, Deserialize, Default)]
25pub struct BrowseQuery {
26 #[serde(default)]
28 pub path: Option<String>,
29}
30
31#[derive(Debug, Serialize)]
32pub struct BrowseResponse {
33 pub path: String,
34 pub entries: Vec<BrowseEntry>,
35}
36
37pub async fn handle_browse(
39 State(state): State<AppState>,
40 headers: HeaderMap,
41 Query(q): Query<BrowseQuery>,
42) -> Response {
43 if let Err(e) = require_auth(&state, &headers) {
44 return e.into_response();
45 }
46 let config = state.config.read().clone();
47 let raw = q.path.unwrap_or_default();
48 match list_directory(&config, &raw) {
49 Ok(result) => Json(BrowseResponse {
50 path: result.path,
51 entries: result.entries,
52 })
53 .into_response(),
54 Err(err) => browse_error_response(err),
55 }
56}
57
58fn browse_error_response(err: BrowseError) -> Response {
59 let status = match &err {
60 BrowseError::Escape(_) => StatusCode::BAD_REQUEST,
61 BrowseError::NotFound(_) => StatusCode::NOT_FOUND,
62 BrowseError::NotADirectory(_) => StatusCode::BAD_REQUEST,
63 BrowseError::Protected(_) => StatusCode::FORBIDDEN,
64 BrowseError::ProtectedFile(_) => StatusCode::FORBIDDEN,
65 BrowseError::TooLarge(_, _) => StatusCode::PAYLOAD_TOO_LARGE,
66 BrowseError::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
67 };
68 (
69 status,
70 Json(serde_json::json!({ "error": format!("{}", err) })),
71 )
72 .into_response()
73}
74
75#[derive(Debug, Deserialize)]
76pub struct BrowsePathBody {
77 pub path: String,
78}
79
80pub async fn handle_browse_mkdir(
82 State(state): State<AppState>,
83 headers: HeaderMap,
84 Json(body): Json<BrowsePathBody>,
85) -> Response {
86 if let Err(e) = require_auth(&state, &headers) {
87 return e.into_response();
88 }
89 let config = state.config.read().clone();
90 match make_directory(&config, &body.path) {
91 Ok(()) => Json(serde_json::json!({ "created": body.path })).into_response(),
92 Err(err) => browse_error_response(err),
93 }
94}
95
96pub async fn handle_browse_rmdir(
99 State(state): State<AppState>,
100 headers: HeaderMap,
101 Json(body): Json<BrowsePathBody>,
102) -> Response {
103 if let Err(e) = require_auth(&state, &headers) {
104 return e.into_response();
105 }
106 let config = state.config.read().clone();
107 match remove_directory(&config, &body.path) {
108 Ok(()) => Json(serde_json::json!({ "removed": body.path })).into_response(),
109 Err(err) => browse_error_response(err),
110 }
111}
112
113pub async fn handle_agent_workspace_list(
118 State(state): State<AppState>,
119 headers: HeaderMap,
120 AxumPath(alias): AxumPath<String>,
121 Query(q): Query<BrowseQuery>,
122) -> Response {
123 if let Err(e) = require_auth(&state, &headers) {
124 return e.into_response();
125 }
126 let config = state.config.read().clone();
127 let raw = q.path.unwrap_or_default();
128 match list_agent_workspace(&config, &alias, &raw) {
129 Ok(result) => Json(BrowseResponse {
130 path: result.path,
131 entries: result.entries,
132 })
133 .into_response(),
134 Err(err) => browse_error_response(err),
135 }
136}
137
138#[derive(Debug, Serialize)]
139pub struct FileReadResponse {
140 pub path: String,
141 pub size: u64,
142 pub is_text: bool,
143 pub content: String,
147 pub encoding: &'static str,
148}
149
150pub async fn handle_agent_workspace_read(
153 State(state): State<AppState>,
154 headers: HeaderMap,
155 AxumPath(alias): AxumPath<String>,
156 Query(q): Query<BrowseQuery>,
157) -> Response {
158 if let Err(e) = require_auth(&state, &headers) {
159 return e.into_response();
160 }
161 let config = state.config.read().clone();
162 let raw = q.path.unwrap_or_default();
163 match read_agent_workspace_file(&config, &alias, &raw) {
164 Ok(result) => {
165 let (content, encoding) = if result.is_text {
166 (String::from_utf8(result.bytes).unwrap_or_default(), "utf8")
167 } else {
168 (
169 base64::engine::general_purpose::STANDARD.encode(&result.bytes),
170 "base64",
171 )
172 };
173 Json(FileReadResponse {
174 path: result.path,
175 size: result.size,
176 is_text: result.is_text,
177 content,
178 encoding,
179 })
180 .into_response()
181 }
182 Err(err) => browse_error_response(err),
183 }
184}
185
186pub async fn handle_agent_workspace_delete(
188 State(state): State<AppState>,
189 headers: HeaderMap,
190 AxumPath(alias): AxumPath<String>,
191 Json(body): Json<BrowsePathBody>,
192) -> Response {
193 if let Err(e) = require_auth(&state, &headers) {
194 return e.into_response();
195 }
196 let config = state.config.read().clone();
197 match delete_agent_workspace_path(&config, &alias, &body.path) {
198 Ok(()) => Json(serde_json::json!({ "removed": body.path })).into_response(),
199 Err(err) => browse_error_response(err),
200 }
201}
202
203#[derive(Debug, Deserialize)]
204pub struct MoveBody {
205 pub from: String,
206 pub to: String,
207}
208
209pub async fn handle_agent_workspace_move(
211 State(state): State<AppState>,
212 headers: HeaderMap,
213 AxumPath(alias): AxumPath<String>,
214 Json(body): Json<MoveBody>,
215) -> Response {
216 if let Err(e) = require_auth(&state, &headers) {
217 return e.into_response();
218 }
219 let config = state.config.read().clone();
220 match move_agent_workspace_path(&config, &alias, &body.from, &body.to) {
221 Ok(()) => Json(serde_json::json!({ "from": body.from, "to": body.to })).into_response(),
222 Err(err) => browse_error_response(err),
223 }
224}
225
226pub async fn handle_agent_workspace_mkdir(
228 State(state): State<AppState>,
229 headers: HeaderMap,
230 AxumPath(alias): AxumPath<String>,
231 Json(body): Json<BrowsePathBody>,
232) -> Response {
233 if let Err(e) = require_auth(&state, &headers) {
234 return e.into_response();
235 }
236 let config = state.config.read().clone();
237 match make_agent_workspace_directory(&config, &alias, &body.path) {
238 Ok(()) => Json(serde_json::json!({ "created": body.path })).into_response(),
239 Err(err) => browse_error_response(err),
240 }
241}