Skip to main content

Mountain/IPC/WindServiceHandler/
FileSystem.rs

1#![allow(non_snake_case)]
2
3//! File System domain handlers for Wind IPC.
4//!
5//! Contains both legacy runtime-based handlers and native URI-aware handlers
6//! used by VS Code's DiskFileSystemProviderClient.
7
8use std::{path::PathBuf, sync::Arc};
9
10use serde_json::{Value, json};
11use CommonLibrary::{
12	Environment::Requires::Requires,
13	Error::CommonError::CommonError,
14	FileSystem::{FileSystemReader::FileSystemReader, FileSystemWriter::FileSystemWriter},
15};
16
17use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
18use super::{extract_path_from_arg, metadata_to_istat};
19
20// ============================================================================
21// Legacy runtime-based handlers
22// ============================================================================
23
24/// Handler for file read requests
25pub async fn handle_file_read(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
26	let Path = Args
27		.get(0)
28		.ok_or("Missing file path".to_string())?
29		.as_str()
30		.ok_or("File path must be a string".to_string())?;
31
32	let Provider:Arc<dyn FileSystemReader> = Runtime.Environment.Require();
33
34	let Content = Provider
35		.ReadFile(&PathBuf::from(Path))
36		.await
37		.map_err(|E| format!("Failed to read file: {}", E))?;
38
39	dev_log!("vfs", "read: {} ({} bytes)", Path, Content.len());
40	Ok(json!(Content))
41}
42
43/// Handler for file write requests
44pub async fn handle_file_write(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
45	let Path = Args
46		.get(0)
47		.ok_or("Missing file path".to_string())?
48		.as_str()
49		.ok_or("File path must be a string".to_string())?;
50
51	let Content = Args
52		.get(1)
53		.ok_or("Missing file content".to_string())?
54		.as_str()
55		.ok_or("File content must be a string".to_string())?;
56
57	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
58
59	Provider
60		.WriteFile(&PathBuf::from(Path), Content.as_bytes().to_vec(), true, true)
61		.await
62		.map_err(|E:CommonError| format!("Failed to write file: {}", E))?;
63
64	dev_log!("vfs", "written: {} ({} bytes)", Path, Content.len());
65	Ok(Value::Null)
66}
67
68/// Handler for file stat requests
69pub async fn handle_file_stat(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
70	let Path = Args
71		.get(0)
72		.ok_or("Missing file path".to_string())?
73		.as_str()
74		.ok_or("File path must be a string".to_string())?;
75
76	let Provider:Arc<dyn FileSystemReader> = Runtime.Environment.Require();
77
78	let Stats = Provider
79		.StatFile(&PathBuf::from(Path))
80		.await
81		.map_err(|E| format!("Failed to stat file: {}", E))?;
82
83	dev_log!("vfs", "legacy_stat: {}", Path);
84	Ok(json!(Stats))
85}
86
87/// Handler for file exists requests
88pub async fn handle_file_exists(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
89	let Path = Args
90		.get(0)
91		.ok_or("Missing file path".to_string())?
92		.as_str()
93		.ok_or("File path must be a string".to_string())?;
94
95	let Provider:Arc<dyn FileSystemReader> = Runtime.Environment.Require();
96
97	let Exists = Provider.StatFile(&PathBuf::from(Path)).await.is_ok();
98
99	dev_log!("vfs", "exists: {} = {}", Path, Exists);
100	Ok(json!(Exists))
101}
102
103/// Handler for file delete requests
104pub async fn handle_file_delete(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
105	let Path = Args
106		.get(0)
107		.ok_or("Missing file path".to_string())?
108		.as_str()
109		.ok_or("File path must be a string".to_string())?;
110
111	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
112
113	Provider
114		.Delete(&PathBuf::from(Path), false, false)
115		.await
116		.map_err(|E:CommonError| format!("Failed to delete file: {}", E))?;
117
118	dev_log!("vfs", "deleted: {}", Path);
119	Ok(Value::Null)
120}
121
122/// Handler for file copy requests
123pub async fn handle_file_copy(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
124	let Source = Args
125		.get(0)
126		.ok_or("Missing source path".to_string())?
127		.as_str()
128		.ok_or("Source path must be a string".to_string())?;
129
130	let Destination = Args
131		.get(1)
132		.ok_or("Missing destination path".to_string())?
133		.as_str()
134		.ok_or("Destination path must be a string".to_string())?;
135
136	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
137
138	Provider
139		.Copy(&PathBuf::from(Source), &PathBuf::from(Destination), false)
140		.await
141		.map_err(|_E:CommonError| format!("Failed to copy file: {} -> {}", Source, Destination))?;
142
143	dev_log!("vfs", "copied: {} -> {}", Source, Destination);
144	Ok(Value::Null)
145}
146
147/// Handler for file move requests
148pub async fn handle_file_move(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
149	let Source = Args
150		.get(0)
151		.ok_or("Missing source path".to_string())?
152		.as_str()
153		.ok_or("Source path must be a string".to_string())?;
154
155	let Destination = Args
156		.get(1)
157		.ok_or("Missing destination path".to_string())?
158		.as_str()
159		.ok_or("Destination path must be a string".to_string())?;
160
161	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
162
163	Provider
164		.Rename(&PathBuf::from(Source), &PathBuf::from(Destination), false)
165		.await
166		.map_err(|_E:CommonError| format!("Failed to move file: {} -> {}", Source, Destination))?;
167
168	dev_log!("vfs", "moved: {} -> {}", Source, Destination);
169	Ok(Value::Null)
170}
171
172/// Handler for directory creation requests
173pub async fn handle_file_mkdir(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
174	let Path = Args
175		.get(0)
176		.ok_or("Missing directory path".to_string())?
177		.as_str()
178		.ok_or("Directory path must be a string".to_string())?;
179
180	let Recursive = Args.get(1).and_then(|V| V.as_bool()).unwrap_or(true);
181
182	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
183
184	Provider
185		.CreateDirectory(&PathBuf::from(Path), Recursive)
186		.await
187		.map_err(|E:CommonError| format!("Failed to create directory: {}", E))?;
188
189	dev_log!("vfs", "mkdir: {} (recursive: {})", Path, Recursive);
190	Ok(Value::Null)
191}
192
193/// Handler for directory reading requests
194pub async fn handle_file_readdir(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
195	let Path = Args
196		.get(0)
197		.ok_or("Missing directory path".to_string())?
198		.as_str()
199		.ok_or("Directory path must be a string".to_string())?;
200
201	let Provider:Arc<dyn FileSystemReader> = Runtime.Environment.Require();
202
203	let Entries = Provider
204		.ReadDirectory(&PathBuf::from(Path))
205		.await
206		.map_err(|E| format!("Failed to read directory: {}", E))?;
207
208	dev_log!("vfs", "readdir_legacy: {} ({} entries)", Path, Entries.len());
209	Ok(json!(Entries))
210}
211
212/// Handler for binary file read requests
213pub async fn handle_file_read_binary(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
214	let Path = Args
215		.get(0)
216		.ok_or("Missing file path".to_string())?
217		.as_str()
218		.ok_or("File path must be a string".to_string())?;
219
220	let Provider:Arc<dyn FileSystemReader> = Runtime.Environment.Require();
221
222	let Content = Provider
223		.ReadFile(&PathBuf::from(Path))
224		.await
225		.map_err(|E| format!("Failed to read binary file: {}", E))?;
226
227	dev_log!("vfs", "readBinary: {} ({} bytes)", Path, Content.len());
228	Ok(json!(Content))
229}
230
231/// Handler for binary file write requests
232pub async fn handle_file_write_binary(Runtime:Arc<ApplicationRunTime>, Args:Vec<Value>) -> Result<Value, String> {
233	let Path = Args
234		.get(0)
235		.ok_or("Missing file path".to_string())?
236		.as_str()
237		.ok_or("File path must be a string".to_string())?;
238
239	let Content = Args
240		.get(1)
241		.ok_or("Missing file content".to_string())?
242		.as_str()
243		.ok_or("File content must be a string".to_string())?;
244
245	let ContentBytes = Content.as_bytes().to_vec();
246	let ContentLength = ContentBytes.len();
247
248	let Provider:Arc<dyn FileSystemWriter> = Runtime.Environment.Require();
249
250	Provider
251		.WriteFile(&PathBuf::from(Path), ContentBytes.clone(), true, true)
252		.await
253		.map_err(|E:CommonError| format!("Failed to write binary file: {}", E))?;
254
255	dev_log!("vfs", "writeBinary: {} ({} bytes)", Path, ContentLength);
256	Ok(Value::Null)
257}
258
259// ============================================================================
260// Native URI-aware handlers (used by VS Code DiskFileSystemProviderClient)
261// ============================================================================
262
263/// Read file with URI arg support (VS Code sends { scheme, path } objects)
264/// Returns { buffer: number[] } where buffer is the raw byte content.
265pub async fn handle_file_read_native(Args:Vec<Value>) -> Result<Value, String> {
266	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing file path")?)?;
267
268	dev_log!("vfs", "readFile: {}", Path);
269
270	let Bytes = tokio::fs::read(&Path)
271		.await
272		.map_err(|E| format!("Failed to read file: {} (path: {})", E, Path))?;
273
274	dev_log!("vfs", "readFile OK: {} ({} bytes)", Path, Bytes.len());
275
276	let ByteArray:Vec<Value> = Bytes.iter().map(|B| json!(*B)).collect();
277	Ok(json!({ "buffer": ByteArray }))
278}
279
280/// Write file with URI arg support
281pub async fn handle_file_write_native(Args:Vec<Value>) -> Result<Value, String> {
282	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing file path")?)?;
283
284	let Content = Args.get(1).ok_or("Missing file content")?;
285
286	let Bytes = if let Some(S) = Content.as_str() {
287		S.as_bytes().to_vec()
288	} else if let Some(Obj) = Content.as_object() {
289		if let Some(Buf) = Obj.get("buffer") {
290			if let Some(Arr) = Buf.as_array() {
291				Arr.iter().filter_map(|V| V.as_u64().map(|N| N as u8)).collect()
292			} else if let Some(S) = Buf.as_str() {
293				S.as_bytes().to_vec()
294			} else {
295				return Err("Unsupported buffer format".to_string());
296			}
297		} else {
298			serde_json::to_string(Content).unwrap_or_default().into_bytes()
299		}
300	} else {
301		return Err("File content must be a string or VSBuffer".to_string());
302	};
303
304	if let Some(Parent) = std::path::Path::new(&Path).parent() {
305		tokio::fs::create_dir_all(Parent).await.ok();
306	}
307
308	tokio::fs::write(&Path, &Bytes)
309		.await
310		.map_err(|E| format!("Failed to write file: {} (path: {})", E, Path))?;
311
312	Ok(Value::Null)
313}
314
315/// Stat file - pure stat, no side effects. Returns IStat shape.
316pub async fn handle_file_stat_native(Args:Vec<Value>) -> Result<Value, String> {
317	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing file path")?)?;
318
319	dev_log!("vfs", "stat: {}", Path);
320
321	let Metadata = tokio::fs::symlink_metadata(&Path).await.map_err(|E| {
322		dev_log!("vfs", "stat ENOENT: {}", Path);
323		format!("Failed to stat file: {} (path: {})", E, Path)
324	})?;
325
326	dev_log!("vfs", "stat OK: {} (dir={})", Path, Metadata.is_dir());
327	Ok(metadata_to_istat(&Metadata))
328}
329
330/// Check file existence with URI arg support
331pub async fn handle_file_exists_native(Args:Vec<Value>) -> Result<Value, String> {
332	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing file path")?)?;
333
334	Ok(json!(tokio::fs::try_exists(&Path).await.unwrap_or(false)))
335}
336
337/// Delete file or directory with URI arg support
338pub async fn handle_file_delete_native(Args:Vec<Value>) -> Result<Value, String> {
339	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing file path")?)?;
340
341	let Recursive = Args
342		.get(1)
343		.and_then(|V| V.as_object())
344		.and_then(|O| O.get("recursive"))
345		.and_then(|V| V.as_bool())
346		.unwrap_or(false);
347
348	let PathBuf = std::path::Path::new(&Path);
349
350	if PathBuf.is_dir() {
351		if Recursive {
352			tokio::fs::remove_dir_all(&Path).await
353		} else {
354			tokio::fs::remove_dir(&Path).await
355		}
356	} else {
357		tokio::fs::remove_file(&Path).await
358	}
359	.map_err(|E| format!("Failed to delete: {} ({})", Path, E))?;
360
361	Ok(Value::Null)
362}
363
364/// Create directory with URI arg support
365pub async fn handle_file_mkdir_native(Args:Vec<Value>) -> Result<Value, String> {
366	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing directory path")?)?;
367
368	tokio::fs::create_dir_all(&Path)
369		.await
370		.map_err(|E| format!("Failed to mkdir: {} ({})", Path, E))?;
371
372	Ok(Value::Null)
373}
374
375/// Read directory contents with URI arg support
376/// Returns array of [name, fileType] tuples matching VS Code's ReadDirResult
377pub async fn handle_file_readdir_native(Args:Vec<Value>) -> Result<Value, String> {
378	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing directory path")?)?;
379
380	dev_log!("vfs", "readdir: {}", Path);
381
382	let mut Entries = tokio::fs::read_dir(&Path)
383		.await
384		.map_err(|E| format!("Failed to readdir: {} ({})", Path, E))?;
385
386	let mut Result = Vec::new();
387
388	while let Some(Entry) = Entries.next_entry().await.map_err(|E| E.to_string())? {
389		let Name = Entry.file_name().to_string_lossy().to_string();
390		let FileType = Entry.file_type().await.map_err(|E| E.to_string())?;
391
392		let TypeValue = if FileType.is_symlink() {
393			64 // SymbolicLink
394		} else if FileType.is_dir() {
395			2 // Directory
396		} else {
397			1 // File
398		};
399
400		Result.push(json!([Name, TypeValue]));
401	}
402
403	Ok(json!(Result))
404}
405
406/// Rename/move file with URI arg support
407pub async fn handle_file_rename_native(Args:Vec<Value>) -> Result<Value, String> {
408	let Source = extract_path_from_arg(Args.get(0).ok_or("Missing source path")?)?;
409	let Target = extract_path_from_arg(Args.get(1).ok_or("Missing target path")?)?;
410
411	tokio::fs::rename(&Source, &Target)
412		.await
413		.map_err(|E| format!("Failed to rename: {} -> {} ({})", Source, Target, E))?;
414
415	Ok(Value::Null)
416}
417
418/// Resolve real path (follow symlinks)
419pub async fn handle_file_realpath(Args:Vec<Value>) -> Result<Value, String> {
420	let Path = extract_path_from_arg(Args.get(0).ok_or("Missing path")?)?;
421
422	let Canonical = tokio::fs::canonicalize(&Path)
423		.await
424		.map_err(|E| format!("Failed to realpath: {} ({})", Path, E))?;
425
426	Ok(json!({
427		"scheme": "file",
428		"path": Canonical.to_string_lossy(),
429		"authority": ""
430	}))
431}
432
433/// Clone file (copy with metadata)
434pub async fn handle_file_clone_native(Args:Vec<Value>) -> Result<Value, String> {
435	let Source = extract_path_from_arg(Args.get(0).ok_or("Missing source path")?)?;
436	let Target = extract_path_from_arg(Args.get(1).ok_or("Missing target path")?)?;
437
438	tokio::fs::copy(&Source, &Target)
439		.await
440		.map_err(|E| format!("Failed to clone: {} -> {} ({})", Source, Target, E))?;
441
442	Ok(Value::Null)
443}