opendev_web/routes/sessions/
filesystem.rs1use std::path::Path;
4
5use axum::Json;
6use axum::extract::{Query, State};
7use serde::Deserialize;
8
9use crate::error::WebError;
10use crate::state::AppState;
11
12#[derive(Debug, Deserialize)]
14pub struct ListFilesQuery {
15 #[serde(default)]
16 pub query: String,
17}
18
19#[derive(Debug, Deserialize)]
21pub struct VerifyPathRequest {
22 #[serde(default)]
23 pub path: Option<String>,
24}
25
26#[derive(Debug, Deserialize)]
28pub struct BrowseDirectoryRequest {
29 #[serde(default)]
30 pub path: String,
31 #[serde(default)]
32 pub show_hidden: bool,
33}
34
35pub(super) async fn list_files(
37 State(state): State<AppState>,
38 Query(params): Query<ListFilesQuery>,
39) -> Result<Json<serde_json::Value>, WebError> {
40 let mgr = state.session_manager().await;
41 let session = mgr.current_session();
42
43 let working_dir = match session.and_then(|s| s.working_directory.as_deref()) {
44 Some(wd) => wd.to_string(),
45 None => {
46 return Ok(Json(serde_json::json!({"files": []})));
47 }
48 };
49
50 let wd_path = Path::new(&working_dir);
51 if !wd_path.exists() || !wd_path.is_dir() {
52 return Ok(Json(serde_json::json!({"files": []})));
53 }
54
55 let always_exclude: &[&str] = &[
57 ".git",
58 ".hg",
59 ".svn",
60 "node_modules",
61 "__pycache__",
62 ".pytest_cache",
63 ".mypy_cache",
64 ".venv",
65 "venv",
66 ".DS_Store",
67 ".idea",
68 ".vscode",
69 "target",
70 "dist",
71 "build",
72 "out",
73 ".next",
74 ".nuxt",
75 ".cache",
76 ".tox",
77 ".nox",
78 ".gradle",
79 "coverage",
80 "htmlcov",
81 ];
82
83 let query = params.query.to_lowercase();
84 let mut files: Vec<serde_json::Value> = Vec::new();
85 let max_files = 100;
86
87 let mut stack = vec![wd_path.to_path_buf()];
89 'outer: while let Some(dir) = stack.pop() {
90 let entries = match std::fs::read_dir(&dir) {
91 Ok(e) => e,
92 Err(_) => continue,
93 };
94
95 for entry in entries.flatten() {
96 let file_name = entry.file_name();
97 let name = file_name.to_string_lossy();
98
99 let file_type = match entry.file_type() {
100 Ok(ft) => ft,
101 Err(_) => continue,
102 };
103
104 if file_type.is_dir() {
105 if !always_exclude.contains(&name.as_ref()) {
106 stack.push(entry.path());
107 }
108 continue;
109 }
110
111 if file_type.is_file() {
112 let rel_path = match entry.path().strip_prefix(wd_path) {
113 Ok(p) => p.to_string_lossy().to_string(),
114 Err(_) => continue,
115 };
116
117 if !query.is_empty() && !rel_path.to_lowercase().contains(&query) {
119 continue;
120 }
121
122 files.push(serde_json::json!({
123 "path": rel_path,
124 "name": name,
125 "is_file": true,
126 }));
127
128 if files.len() >= max_files {
129 break 'outer;
130 }
131 }
132 }
133 }
134
135 files.sort_by(|a, b| {
137 let pa = a["path"].as_str().unwrap_or("");
138 let pb = b["path"].as_str().unwrap_or("");
139 pa.cmp(pb)
140 });
141
142 Ok(Json(serde_json::json!({"files": files})))
143}
144
145pub(super) async fn verify_path(
147 State(_state): State<AppState>,
148 Json(payload): Json<VerifyPathRequest>,
149) -> Json<serde_json::Value> {
150 let path_str = payload.path.as_deref().unwrap_or("").trim().to_string();
151
152 if path_str.is_empty() {
153 return Json(serde_json::json!({
154 "exists": false,
155 "is_directory": false,
156 "error": "Path cannot be empty",
157 }));
158 }
159
160 let expanded = if path_str.starts_with('~') {
162 if let Some(home) = dirs_path_home() {
163 path_str.replacen('~', &home, 1)
164 } else {
165 path_str.clone()
166 }
167 } else {
168 path_str.clone()
169 };
170
171 let path = Path::new(&expanded);
172
173 if !path.exists() {
174 return Json(serde_json::json!({
175 "exists": false,
176 "is_directory": false,
177 "error": "Path does not exist",
178 }));
179 }
180
181 if !path.is_dir() {
182 return Json(serde_json::json!({
183 "exists": true,
184 "is_directory": false,
185 "error": "Path is not a directory",
186 }));
187 }
188
189 if std::fs::read_dir(path).is_err() {
191 return Json(serde_json::json!({
192 "exists": true,
193 "is_directory": true,
194 "error": "No read access to directory",
195 }));
196 }
197
198 let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
199
200 Json(serde_json::json!({
201 "exists": true,
202 "is_directory": true,
203 "path": canonical.to_string_lossy(),
204 "error": null,
205 }))
206}
207
208pub(super) async fn browse_directory(
210 State(_state): State<AppState>,
211 Json(payload): Json<BrowseDirectoryRequest>,
212) -> Json<serde_json::Value> {
213 let raw = payload.path.trim().to_string();
214
215 let target = if raw.is_empty() {
216 match dirs_path_home() {
218 Some(home) => std::path::PathBuf::from(home),
219 None => std::path::PathBuf::from("/"),
220 }
221 } else {
222 let expanded = if raw.starts_with('~') {
223 if let Some(home) = dirs_path_home() {
224 raw.replacen('~', &home, 1)
225 } else {
226 raw.clone()
227 }
228 } else {
229 raw.clone()
230 };
231 std::path::PathBuf::from(expanded)
232 };
233
234 let target = target.canonicalize().unwrap_or_else(|_| target.clone());
235
236 if !target.exists() {
237 return Json(serde_json::json!({
238 "current_path": target.to_string_lossy(),
239 "parent_path": target.parent().map(|p| p.to_string_lossy().to_string()),
240 "directories": [],
241 "error": "Path does not exist",
242 }));
243 }
244
245 if !target.is_dir() {
246 return Json(serde_json::json!({
247 "current_path": target.to_string_lossy(),
248 "parent_path": target.parent().map(|p| p.to_string_lossy().to_string()),
249 "directories": [],
250 "error": "Path is not a directory",
251 }));
252 }
253
254 let parent_path = if target.parent() != Some(&target) {
255 target.parent().map(|p| p.to_string_lossy().to_string())
256 } else {
257 None
258 };
259
260 let entries = match std::fs::read_dir(&target) {
261 Ok(e) => e,
262 Err(_) => {
263 return Json(serde_json::json!({
264 "current_path": target.to_string_lossy(),
265 "parent_path": parent_path,
266 "directories": [],
267 "error": "Permission denied reading directory contents",
268 }));
269 }
270 };
271
272 let mut dirs: Vec<serde_json::Value> = Vec::new();
273 for entry in entries.flatten() {
274 let ft = match entry.file_type() {
275 Ok(ft) => ft,
276 Err(_) => continue,
277 };
278 if !ft.is_dir() {
279 continue;
280 }
281 let name = entry.file_name().to_string_lossy().to_string();
282 if name.starts_with('.') && !payload.show_hidden {
283 continue;
284 }
285 if std::fs::read_dir(entry.path()).is_err() {
287 continue;
288 }
289 dirs.push(serde_json::json!({
290 "name": name,
291 "path": entry.path().to_string_lossy(),
292 }));
293 }
294
295 dirs.sort_by(|a, b| {
296 let na = a["name"].as_str().unwrap_or("").to_lowercase();
297 let nb = b["name"].as_str().unwrap_or("").to_lowercase();
298 na.cmp(&nb)
299 });
300
301 Json(serde_json::json!({
302 "current_path": target.to_string_lossy(),
303 "parent_path": parent_path,
304 "directories": dirs,
305 "error": null,
306 }))
307}
308
309fn dirs_path_home() -> Option<String> {
311 std::env::var("HOME")
312 .ok()
313 .or_else(|| std::env::var("USERPROFILE").ok())
314}