1use crate::model::filesystem::{
6 DirEntry, EntryType, FileMetadata, FilePermissions, FileReader, FileSystem, FileWriter, WriteOp,
7};
8use crate::services::remote::channel::{AgentChannel, ChannelError};
9use crate::services::remote::protocol::{
10 append_params, count_lf_params, decode_base64, ls_params, patch_params, read_params,
11 stat_params, sudo_write_params, truncate_params, write_params, PatchOp, RemoteDirEntry,
12 RemoteMetadata,
13};
14use std::io::{self, Cursor, Read, Seek, Write};
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::{Duration, UNIX_EPOCH};
18
19pub struct RemoteFileSystem {
21 channel: Arc<AgentChannel>,
22 connection_string: String,
24}
25
26impl RemoteFileSystem {
27 pub fn new(channel: Arc<AgentChannel>, connection_string: String) -> Self {
29 Self {
30 channel,
31 connection_string,
32 }
33 }
34
35 pub fn connection_string(&self) -> &str {
37 &self.connection_string
38 }
39
40 pub fn is_connected(&self) -> bool {
42 self.channel.is_connected()
43 }
44
45 fn to_io_error(e: ChannelError) -> io::Error {
47 match e {
48 ChannelError::Io(e) => e,
49 ChannelError::Remote(msg) => {
50 let kind = if msg.contains("not found") || msg.contains("No such file") {
51 io::ErrorKind::NotFound
52 } else if msg.contains("permission denied") {
53 io::ErrorKind::PermissionDenied
54 } else if msg.contains("is a directory") {
55 io::ErrorKind::IsADirectory
56 } else if msg.contains("not a directory") {
57 io::ErrorKind::NotADirectory
58 } else {
59 io::ErrorKind::Other
60 };
61 io::Error::new(kind, msg)
62 }
63 e => io::Error::other(e.to_string()),
64 }
65 }
66
67 fn convert_metadata(rm: &RemoteMetadata, name: &str) -> FileMetadata {
69 let modified = if rm.mtime > 0 {
70 Some(UNIX_EPOCH + Duration::from_secs(rm.mtime as u64))
71 } else {
72 None
73 };
74
75 let is_hidden = name.starts_with('.');
76 let permissions = FilePermissions::from_mode(rm.mode);
77 let is_readonly = permissions.is_readonly();
78
79 let mut meta = FileMetadata::new(rm.size)
80 .with_hidden(is_hidden)
81 .with_readonly(is_readonly)
82 .with_permissions(permissions);
83
84 if let Some(m) = modified {
85 meta = meta.with_modified(m);
86 }
87
88 #[cfg(unix)]
89 {
90 meta.uid = Some(rm.uid);
91 meta.gid = Some(rm.gid);
92 }
93
94 meta
95 }
96
97 fn convert_dir_entry(re: &RemoteDirEntry) -> DirEntry {
99 let entry_type = if re.link {
100 EntryType::Symlink
101 } else if re.dir {
102 EntryType::Directory
103 } else {
104 EntryType::File
105 };
106
107 let modified = if re.mtime > 0 {
108 Some(UNIX_EPOCH + Duration::from_secs(re.mtime as u64))
109 } else {
110 None
111 };
112
113 let is_hidden = re.name.starts_with('.');
114 let permissions = FilePermissions::from_mode(re.mode);
115 let is_readonly = permissions.is_readonly();
116
117 let mut metadata = FileMetadata::new(re.size)
118 .with_hidden(is_hidden)
119 .with_readonly(is_readonly)
120 .with_permissions(permissions);
121
122 if let Some(m) = modified {
123 metadata = metadata.with_modified(m);
124 }
125
126 let mut entry = DirEntry::new(PathBuf::from(&re.path), re.name.clone(), entry_type);
127 entry.metadata = Some(metadata);
128 entry.symlink_target_is_dir = re.link_dir;
129
130 entry
131 }
132}
133
134impl FileSystem for RemoteFileSystem {
135 fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
136 let path_str = path.to_string_lossy();
137 let (data_chunks, _result) = self
138 .channel
139 .request_with_data_blocking("read", read_params(&path_str, None, None))
140 .map_err(Self::to_io_error)?;
141
142 let mut content = Vec::new();
144 for chunk in data_chunks {
145 if let Some(b64) = chunk.get("data").and_then(|v| v.as_str()) {
146 let decoded = decode_base64(b64)
147 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
148 content.extend(decoded);
149 }
150 }
151
152 Ok(content)
153 }
154
155 fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
156 let path_str = path.to_string_lossy();
157 let (data_chunks, result) = self
158 .channel
159 .request_with_data_blocking("read", read_params(&path_str, Some(offset), Some(len)))
160 .map_err(Self::to_io_error)?;
161
162 let mut content = Vec::new();
164 for chunk in data_chunks {
165 if let Some(b64) = chunk.get("data").and_then(|v| v.as_str()) {
166 let decoded = decode_base64(b64)
167 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
168 content.extend(decoded);
169 }
170 }
171
172 let agent_reported_size = result
174 .get("size")
175 .and_then(|v| v.as_u64())
176 .map(|s| s as usize);
177
178 if content.len() != len {
182 return Err(io::Error::new(
183 io::ErrorKind::UnexpectedEof,
184 format!(
185 "read_range: expected {} bytes at offset {}, got {} (agent reported: {:?}, path: {})",
186 len,
187 offset,
188 content.len(),
189 agent_reported_size,
190 path_str
191 ),
192 ));
193 }
194
195 Ok(content)
196 }
197
198 fn count_line_feeds_in_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<usize> {
199 let path_str = path.to_string_lossy();
200 let result = self
201 .channel
202 .request_blocking("count_lf", count_lf_params(&path_str, offset, len))
203 .map_err(Self::to_io_error)?;
204
205 result
206 .get("count")
207 .and_then(|v| v.as_u64())
208 .map(|c| c as usize)
209 .ok_or_else(|| {
210 io::Error::new(
211 io::ErrorKind::InvalidData,
212 "missing count in count_lf response",
213 )
214 })
215 }
216
217 fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
218 let path_str = path.to_string_lossy();
219 self.channel
220 .request_blocking("write", write_params(&path_str, data))
221 .map_err(Self::to_io_error)?;
222 Ok(())
223 }
224
225 fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
226 self.write_file(path, &[])?;
228 Ok(Box::new(RemoteFileWriter::new(
229 self.channel.clone(),
230 path.to_path_buf(),
231 )))
232 }
233
234 fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
235 let data = self.read_file(path)?;
237 Ok(Box::new(RemoteFileReader::new(data)))
238 }
239
240 fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
241 Ok(Box::new(RemoteFileWriter::new(
242 self.channel.clone(),
243 path.to_path_buf(),
244 )))
245 }
246
247 fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
248 Ok(Box::new(AppendingRemoteFileWriter::new(
250 self.channel.clone(),
251 path.to_path_buf(),
252 )))
253 }
254
255 fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
256 let path_str = path.to_string_lossy();
257 self.channel
258 .request_blocking("truncate", truncate_params(&path_str, len))
259 .map_err(Self::to_io_error)?;
260 Ok(())
261 }
262
263 fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
264 let patch_ops: Vec<PatchOp> = ops
266 .iter()
267 .map(|op| match op {
268 WriteOp::Copy { offset, len } => PatchOp::copy(*offset, *len),
269 WriteOp::Insert { data } => PatchOp::insert(data),
270 })
271 .collect();
272
273 let src_str = src_path.to_string_lossy();
274 let dst_str = dst_path.to_string_lossy();
275 let dst_param = if src_path == dst_path {
276 None
277 } else {
278 Some(dst_str.as_ref())
279 };
280
281 self.channel
282 .request_blocking("patch", patch_params(&src_str, dst_param, &patch_ops))
283 .map_err(Self::to_io_error)?;
284 Ok(())
285 }
286
287 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
288 let params = serde_json::json!({
289 "from": from.to_string_lossy(),
290 "to": to.to_string_lossy()
291 });
292 self.channel
293 .request_blocking("mv", params)
294 .map_err(Self::to_io_error)?;
295 Ok(())
296 }
297
298 fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
299 let params = serde_json::json!({
300 "from": from.to_string_lossy(),
301 "to": to.to_string_lossy()
302 });
303 let result = self
304 .channel
305 .request_blocking("cp", params)
306 .map_err(Self::to_io_error)?;
307
308 Ok(result.get("size").and_then(|v| v.as_u64()).unwrap_or(0))
309 }
310
311 fn remove_file(&self, path: &Path) -> io::Result<()> {
312 let params = serde_json::json!({"path": path.to_string_lossy()});
313 self.channel
314 .request_blocking("rm", params)
315 .map_err(Self::to_io_error)?;
316 Ok(())
317 }
318
319 fn remove_dir(&self, path: &Path) -> io::Result<()> {
320 let params = serde_json::json!({"path": path.to_string_lossy()});
321 self.channel
322 .request_blocking("rmdir", params)
323 .map_err(Self::to_io_error)?;
324 Ok(())
325 }
326
327 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
328 let path_str = path.to_string_lossy();
329 let result = self
330 .channel
331 .request_blocking("stat", stat_params(&path_str, true))
332 .map_err(Self::to_io_error)?;
333
334 let rm: RemoteMetadata = serde_json::from_value(result)
335 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
336
337 let name = path
338 .file_name()
339 .map(|n| n.to_string_lossy().to_string())
340 .unwrap_or_default();
341 Ok(Self::convert_metadata(&rm, &name))
342 }
343
344 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
345 let path_str = path.to_string_lossy();
346 let result = self
347 .channel
348 .request_blocking("stat", stat_params(&path_str, false))
349 .map_err(Self::to_io_error)?;
350
351 let rm: RemoteMetadata = serde_json::from_value(result)
352 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
353
354 let name = path
355 .file_name()
356 .map(|n| n.to_string_lossy().to_string())
357 .unwrap_or_default();
358 Ok(Self::convert_metadata(&rm, &name))
359 }
360
361 fn is_dir(&self, path: &Path) -> io::Result<bool> {
362 let path_str = path.to_string_lossy();
363 let result = self
364 .channel
365 .request_blocking("stat", stat_params(&path_str, true))
366 .map_err(Self::to_io_error)?;
367
368 Ok(result.get("dir").and_then(|v| v.as_bool()).unwrap_or(false))
369 }
370
371 fn is_file(&self, path: &Path) -> io::Result<bool> {
372 let path_str = path.to_string_lossy();
373 let result = self
374 .channel
375 .request_blocking("stat", stat_params(&path_str, true))
376 .map_err(Self::to_io_error)?;
377
378 Ok(result
379 .get("file")
380 .and_then(|v| v.as_bool())
381 .unwrap_or(false))
382 }
383
384 fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
385 #[cfg(unix)]
386 {
387 let params = serde_json::json!({
388 "path": path.to_string_lossy(),
389 "mode": permissions.mode()
390 });
391 self.channel
392 .request_blocking("chmod", params)
393 .map_err(Self::to_io_error)?;
394 }
395 #[cfg(not(unix))]
396 {
397 let _ = (path, permissions);
398 }
399 Ok(())
400 }
401
402 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
403 let path_str = path.to_string_lossy();
404 let result = self
405 .channel
406 .request_blocking("ls", ls_params(&path_str))
407 .map_err(Self::to_io_error)?;
408
409 let entries: Vec<RemoteDirEntry> = result
410 .get("entries")
411 .and_then(|v| serde_json::from_value(v.clone()).ok())
412 .unwrap_or_default();
413
414 Ok(entries.iter().map(Self::convert_dir_entry).collect())
415 }
416
417 fn create_dir(&self, path: &Path) -> io::Result<()> {
418 let params = serde_json::json!({"path": path.to_string_lossy()});
419 self.channel
420 .request_blocking("mkdir", params)
421 .map_err(Self::to_io_error)?;
422 Ok(())
423 }
424
425 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
426 let params = serde_json::json!({
427 "path": path.to_string_lossy(),
428 "parents": true
429 });
430 self.channel
431 .request_blocking("mkdir", params)
432 .map_err(Self::to_io_error)?;
433 Ok(())
434 }
435
436 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
437 let params = serde_json::json!({"path": path.to_string_lossy()});
438 let result = self
439 .channel
440 .request_blocking("realpath", params)
441 .map_err(Self::to_io_error)?;
442
443 let canonical = result.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
444 io::Error::new(io::ErrorKind::InvalidData, "missing path in response")
445 })?;
446
447 Ok(PathBuf::from(canonical))
448 }
449
450 fn current_uid(&self) -> u32 {
451 0
454 }
455
456 fn remote_connection_info(&self) -> Option<&str> {
457 Some(&self.connection_string)
458 }
459
460 fn home_dir(&self) -> io::Result<PathBuf> {
461 let result = self
462 .channel
463 .request_blocking("info", serde_json::json!({}))
464 .map_err(Self::to_io_error)?;
465
466 let home = result.get("home").and_then(|v| v.as_str()).ok_or_else(|| {
467 io::Error::new(io::ErrorKind::InvalidData, "missing home in response")
468 })?;
469
470 Ok(PathBuf::from(home))
471 }
472
473 fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
474 let temp_dir = PathBuf::from("/tmp");
476 let file_name = dest_path
477 .file_name()
478 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
479 let timestamp = std::time::SystemTime::now()
480 .duration_since(std::time::UNIX_EPOCH)
481 .map(|d| d.as_nanos())
482 .unwrap_or(0);
483 temp_dir.join(format!(
484 "{}-{}-{}.tmp",
485 file_name.to_string_lossy(),
486 std::process::id(),
487 timestamp
488 ))
489 }
490
491 fn sudo_write(
492 &self,
493 path: &Path,
494 data: &[u8],
495 mode: u32,
496 uid: u32,
497 gid: u32,
498 ) -> io::Result<()> {
499 let path_str = path.to_string_lossy();
500 self.channel
501 .request_blocking(
502 "sudo_write",
503 sudo_write_params(&path_str, data, mode, uid, gid),
504 )
505 .map_err(Self::to_io_error)?;
506 Ok(())
507 }
508}
509
510struct RemoteFileReader {
512 cursor: Cursor<Vec<u8>>,
513}
514
515impl RemoteFileReader {
516 fn new(data: Vec<u8>) -> Self {
517 Self {
518 cursor: Cursor::new(data),
519 }
520 }
521}
522
523impl Read for RemoteFileReader {
524 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
525 self.cursor.read(buf)
526 }
527}
528
529impl Seek for RemoteFileReader {
530 fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
531 self.cursor.seek(pos)
532 }
533}
534
535impl FileReader for RemoteFileReader {}
536
537struct RemoteFileWriter {
539 channel: Arc<AgentChannel>,
540 path: PathBuf,
541 buffer: Vec<u8>,
542}
543
544impl RemoteFileWriter {
545 fn new(channel: Arc<AgentChannel>, path: PathBuf) -> Self {
546 Self {
547 channel,
548 path,
549 buffer: Vec::new(),
550 }
551 }
552}
553
554impl Write for RemoteFileWriter {
555 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
556 self.buffer.extend_from_slice(buf);
557 Ok(buf.len())
558 }
559
560 fn flush(&mut self) -> io::Result<()> {
561 Ok(())
563 }
564}
565
566impl FileWriter for RemoteFileWriter {
567 fn sync_all(&self) -> io::Result<()> {
568 let path_str = self.path.to_string_lossy();
569 self.channel
570 .request_blocking("write", write_params(&path_str, &self.buffer))
571 .map_err(RemoteFileSystem::to_io_error)?;
572 Ok(())
573 }
574}
575
576struct AppendingRemoteFileWriter {
578 channel: Arc<AgentChannel>,
579 path: PathBuf,
580 buffer: Vec<u8>,
581}
582
583impl AppendingRemoteFileWriter {
584 fn new(channel: Arc<AgentChannel>, path: PathBuf) -> Self {
585 Self {
586 channel,
587 path,
588 buffer: Vec::new(),
589 }
590 }
591}
592
593impl Write for AppendingRemoteFileWriter {
594 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
595 self.buffer.extend_from_slice(buf);
596 Ok(buf.len())
597 }
598
599 fn flush(&mut self) -> io::Result<()> {
600 Ok(())
601 }
602}
603
604impl FileWriter for AppendingRemoteFileWriter {
605 fn sync_all(&self) -> io::Result<()> {
606 if self.buffer.is_empty() {
607 return Ok(());
608 }
609 let path_str = self.path.to_string_lossy();
610 self.channel
611 .request_blocking("append", append_params(&path_str, &self.buffer))
612 .map_err(RemoteFileSystem::to_io_error)?;
613 Ok(())
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
622 fn test_convert_metadata() {
623 let rm = RemoteMetadata {
624 size: 1234,
625 mtime: 1700000000,
626 mode: 0o644,
627 uid: 1000,
628 gid: 1000,
629 dir: false,
630 file: true,
631 link: false,
632 };
633
634 let meta = RemoteFileSystem::convert_metadata(&rm, "test.txt");
635 assert_eq!(meta.size, 1234);
636 assert!(!meta.is_hidden);
637 assert!(!meta.is_readonly);
638
639 let meta = RemoteFileSystem::convert_metadata(&rm, ".hidden");
640 assert!(meta.is_hidden);
641 }
642
643 #[test]
644 fn test_convert_dir_entry() {
645 let re = RemoteDirEntry {
646 name: "file.rs".to_string(),
647 path: "/home/user/file.rs".to_string(),
648 dir: false,
649 file: true,
650 link: false,
651 link_dir: false,
652 size: 100,
653 mtime: 1700000000,
654 mode: 0o644,
655 };
656
657 let entry = RemoteFileSystem::convert_dir_entry(&re);
658 assert_eq!(entry.name, "file.rs");
659 assert_eq!(entry.entry_type, EntryType::File);
660 assert!(!entry.is_symlink());
661 }
662}