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