1use crate::dir::Dir;
2use crate::error::{ClientResultExt, ZeroFsError};
3use crate::file::File;
4use crate::path::{components, display, display_path, path_bytes, path_from_bytes, split_parent};
5use crate::session::{FidGuard, Session};
6use crate::types::{
7 Capabilities, ConnectOptions, DirEntry, FileType, Metadata, NodeKind, OpenOptions, SetAttrs,
8 SetTime, StatFs,
9};
10use bytes::{Bytes, BytesMut};
11use ninep_client::{NOFID, NinePClient, Target};
12use ninep_proto::Stat;
13use std::collections::VecDeque;
14use std::future::Future;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::sync::atomic::Ordering;
18use std::time::Duration;
19
20const MULTI_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(500);
21
22const MAX_SYMLINK_HOPS: u32 = 40;
24
25pub struct Client {
34 session: Arc<Session>,
35}
36
37impl std::fmt::Debug for Client {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("Client")
40 .field("closed", &self.session.closed.load(Ordering::Relaxed))
41 .finish_non_exhaustive()
42 }
43}
44
45impl Client {
46 pub async fn connect(target: &str) -> Result<Arc<Client>, ZeroFsError> {
51 Self::connect_with(target, ConnectOptions::default()).await
52 }
53
54 pub async fn connect_with(
56 target: &str,
57 opts: ConnectOptions,
58 ) -> Result<Arc<Client>, ZeroFsError> {
59 let targets = parse_targets([target])?;
60 connect_before(
61 opts.connect_timeout_ms,
62 Self::establish(targets, target, &opts),
63 |ms| format!("connecting to {target}: timed out after {ms} ms"),
64 )
65 .await
66 }
67
68 pub async fn connect_multi(targets: &[String]) -> Result<Arc<Client>, ZeroFsError> {
70 if targets.is_empty() {
71 return Err(ZeroFsError::ConnectFailed {
72 message: "no targets given".into(),
73 });
74 }
75 let opts = ConnectOptions::default();
76 let label = targets.join(", ");
77 let targets = parse_targets(targets.iter().map(String::as_str))?;
78 let connect = async {
79 loop {
80 match Self::establish(targets.clone(), &label, &opts).await {
81 Ok(client) => return Ok(client),
82 Err(_) => crate::runtime::sleep(MULTI_CONNECT_RETRY_DELAY).await,
83 }
84 }
85 };
86 connect_before(opts.connect_timeout_ms, connect, |ms| {
87 format!("no serving leader found among [{label}] within {ms} ms")
88 })
89 .await
90 }
91
92 async fn establish(
93 targets: Vec<Target>,
94 target: &str,
95 opts: &ConnectOptions,
96 ) -> Result<Arc<Client>, ZeroFsError> {
97 let client = NinePClient::connect_multi(targets, opts.msize)
98 .await
99 .map_err(|error| connect_failed(format!("9P target set: {error}")))?;
100 #[cfg(not(target_arch = "wasm32"))]
101 let uid = opts.uid.unwrap_or_else(|| unsafe { libc::geteuid() });
102 #[cfg(target_arch = "wasm32")]
103 let uid = opts.uid.unwrap_or(0);
104 #[cfg(not(target_arch = "wasm32"))]
105 let gid = opts.gid.unwrap_or_else(|| unsafe { libc::getegid() });
106 #[cfg(target_arch = "wasm32")]
107 let gid = opts.gid.unwrap_or(0);
108 let uname = match &opts.uname {
109 Some(u) => u.clone(),
110 None => {
111 #[cfg(not(target_arch = "wasm32"))]
112 {
113 std::env::var("USER").unwrap_or_else(|_| uid.to_string())
114 }
115 #[cfg(target_arch = "wasm32")]
116 {
117 uid.to_string()
118 }
119 }
120 };
121 let root_fid = client.alloc_fid();
122 client
123 .attach(root_fid, NOFID, &uname, &opts.aname, uid)
124 .await
125 .map_err(|e| ZeroFsError::ConnectFailed {
126 message: format!("attach to {target} failed: {e}"),
127 })?;
128 let session = Session::new(client, root_fid, gid);
129 Ok(Arc::new(Client { session }))
130 }
131
132 pub fn capabilities(&self) -> Capabilities {
134 let c = &self.session.client;
135 Capabilities {
136 msize: c.msize(),
137 max_read_chunk: c.max_io(),
138 max_write_chunk: c.max_write_payload(),
139 }
140 }
141
142 pub fn is_connected(&self) -> bool {
145 self.session.client.is_connected()
146 }
147
148 pub fn traffic_stats(&self) -> ninep_client::TrafficStats {
150 self.session.client.traffic_stats()
151 }
152
153 #[doc(hidden)]
157 pub fn outstanding_fids(&self) -> usize {
158 self.session.client.outstanding_fids()
159 }
160
161 pub async fn close(&self) {
164 if self.session.closed.swap(true, Ordering::AcqRel) {
165 return;
166 }
167 self.session.enqueue_clunk(self.session.root_fid);
168 }
169
170 pub async fn read(&self, path: impl AsRef<Path>) -> Result<Bytes, ZeroFsError> {
173 let path = path.as_ref();
174 let pd = display(path);
175 let (guard, stat) = self.open_read(path, &pd).await?;
176 let max = self.session.client.max_io().max(1);
177 let first = self
179 .session
180 .client
181 .read_bytes(guard.fid(), 0, max)
182 .await
183 .ctx(&pd)?;
184 if (first.len() as u32) < max {
185 return Ok(first);
186 }
187 let cap = (stat.size as usize).min((max as usize).saturating_mul(2));
189 let mut out = BytesMut::with_capacity(cap);
190 out.extend_from_slice(&first);
191 loop {
192 let data = self
193 .session
194 .client
195 .read_bytes(guard.fid(), out.len() as u64, max)
196 .await
197 .ctx(&pd)?;
198 let got = data.len();
199 out.extend_from_slice(&data);
200 if (got as u32) < max {
201 return Ok(out.freeze());
202 }
203 }
204 }
205
206 pub async fn read_range(
208 &self,
209 path: impl AsRef<Path>,
210 offset: u64,
211 len: u32,
212 ) -> Result<Bytes, ZeroFsError> {
213 let path = path.as_ref();
214 let pd = display(path);
215 let (guard, _) = self.open_read(path, &pd).await?;
216 self.session
217 .client
218 .read_bytes(guard.fid(), offset, len)
219 .await
220 .ctx(&pd)
221 }
222
223 pub async fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<(), ZeroFsError> {
226 let path = path.as_ref();
227 let pd = display(path);
228 let opts = OpenOptions::write_only().create(true).truncate(true);
229 let guard = self.open_relative_path(path, &pd, &opts).await?;
230 self.session.write_all(guard.fid(), 0, data, &pd).await
231 }
232
233 pub async fn append(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<u64, ZeroFsError> {
237 let path = path.as_ref();
238 let pd = display(path);
239 let opts = OpenOptions::write_only().create(true);
240 let guard = self.open_relative_path(path, &pd, &opts).await?;
241 let stat = self.session.stat_fid(guard.fid(), &pd).await?;
242 self.session
243 .write_all(guard.fid(), stat.size, data, &pd)
244 .await?;
245 Ok(stat.size)
246 }
247
248 async fn parent_of<'a>(
250 &self,
251 path: &'a Path,
252 pd: &str,
253 ) -> Result<(FidGuard, &'a [u8]), ZeroFsError> {
254 self.session.check_open()?;
255 let names = components(path)?;
256 let (parents, name) = split_parent(&names, pd)?;
257 let (guard, _) = self.session.walk(parents, pd).await?;
258 Ok((guard, name))
259 }
260
261 async fn open_read(&self, path: &Path, pd: &str) -> Result<(FidGuard, Stat), ZeroFsError> {
263 self.session.check_open()?;
264 let names = components(path)?;
265 let (guard, stat) = self.session.walk(&names, pd).await?;
266 self.session
267 .lopen(guard.fid(), crate::linux::O_RDONLY, pd)
268 .await?;
269 Ok((guard, stat))
270 }
271
272 async fn open_relative_path(
274 &self,
275 path: &Path,
276 pd: &str,
277 opts: &OpenOptions,
278 ) -> Result<FidGuard, ZeroFsError> {
279 let (dir_guard, name) = self.parent_of(path, pd).await?;
280 self.session
281 .open_relative(dir_guard.fid(), name, opts, pd)
282 .await
283 }
284
285 pub async fn stat(&self, path: impl AsRef<Path>) -> Result<Metadata, ZeroFsError> {
290 let path = path.as_ref();
291 let pd = display(path);
292 self.session.check_open()?;
293 let names = components(path)?;
294 let (_guard, stat) = self.session.walk(&names, &pd).await?;
295 Ok(Metadata::from_stat(&stat))
296 }
297
298 pub async fn metadata(&self, path: impl AsRef<Path>) -> Result<Metadata, ZeroFsError> {
301 let path = path.as_ref();
302 let pd = display(path);
303 self.session.check_open()?;
304 let (_, stat) = self.resolve(path, &pd).await?;
305 Ok(Metadata::from_stat(&stat))
306 }
307
308 pub async fn canonicalize(&self, path: impl AsRef<Path>) -> Result<PathBuf, ZeroFsError> {
312 let path = path.as_ref();
313 let pd = display(path);
314 self.session.check_open()?;
315 let (stack, _) = self.resolve(path, &pd).await?;
316 let mut buf = Vec::new();
317 for comp in &stack {
318 buf.push(b'/');
319 buf.extend_from_slice(comp);
320 }
321 if buf.is_empty() {
322 buf.push(b'/');
323 }
324 path_from_bytes(buf)
325 }
326
327 pub async fn exists(&self, path: impl AsRef<Path>) -> Result<bool, ZeroFsError> {
330 match self.stat(path).await {
331 Ok(_) => Ok(true),
332 Err(ZeroFsError::NotFound { .. }) => Ok(false),
333 Err(e) => Err(e),
334 }
335 }
336
337 pub async fn set_attr(
339 &self,
340 path: impl AsRef<Path>,
341 attrs: SetAttrs,
342 ) -> Result<Metadata, ZeroFsError> {
343 let path = path.as_ref();
344 let pd = display(path);
345 self.session.check_open()?;
346 let names = components(path)?;
347 let (guard, _) = self.session.walk(&names, &pd).await?;
348 let stat = self.session.setattr_fid(guard.fid(), &attrs, &pd).await?;
349 Ok(Metadata::from_stat(&stat))
350 }
351
352 pub async fn chmod(&self, path: impl AsRef<Path>, mode: u32) -> Result<Metadata, ZeroFsError> {
354 self.set_attr(
355 path,
356 SetAttrs {
357 mode: Some(mode),
358 ..Default::default()
359 },
360 )
361 .await
362 }
363
364 pub async fn chown(
366 &self,
367 path: impl AsRef<Path>,
368 uid: Option<u32>,
369 gid: Option<u32>,
370 ) -> Result<Metadata, ZeroFsError> {
371 self.set_attr(
372 path,
373 SetAttrs {
374 uid,
375 gid,
376 ..Default::default()
377 },
378 )
379 .await
380 }
381
382 pub async fn truncate(
384 &self,
385 path: impl AsRef<Path>,
386 size: u64,
387 ) -> Result<Metadata, ZeroFsError> {
388 self.set_attr(
389 path,
390 SetAttrs {
391 size: Some(size),
392 ..Default::default()
393 },
394 )
395 .await
396 }
397
398 pub async fn set_times(
400 &self,
401 path: impl AsRef<Path>,
402 atime: Option<SetTime>,
403 mtime: Option<SetTime>,
404 ) -> Result<Metadata, ZeroFsError> {
405 self.set_attr(
406 path,
407 SetAttrs {
408 atime,
409 mtime,
410 ..Default::default()
411 },
412 )
413 .await
414 }
415
416 pub async fn statfs(&self) -> Result<StatFs, ZeroFsError> {
418 self.session.check_open()?;
419 let r = self
420 .session
421 .client
422 .statfs(self.session.root_fid)
423 .await
424 .ctx("/")?;
425 Ok(StatFs::from_wire(&r))
426 }
427
428 pub async fn sync(&self) -> Result<(), ZeroFsError> {
432 self.session.check_open()?;
433 self.session
434 .client
435 .fsync_all(self.session.root_fid, 0)
436 .await
437 .ctx("/")
438 }
439
440 pub async fn create_dir(
442 &self,
443 path: impl AsRef<Path>,
444 mode: u32,
445 ) -> Result<Metadata, ZeroFsError> {
446 let path = path.as_ref();
447 let pd = display(path);
448 let (dir_guard, name) = self.parent_of(path, &pd).await?;
449 self.session
450 .mkdir_at(dir_guard.fid(), name, mode, &pd)
451 .await
452 }
453
454 pub async fn create_dir_all(
456 &self,
457 path: impl AsRef<Path>,
458 mode: u32,
459 ) -> Result<(), ZeroFsError> {
460 let path = path.as_ref();
461 self.session.check_open()?;
462 let names = components(path)?;
463 for depth in 1..=names.len() {
464 let prefix = &names[..depth];
465 let pd = display_path(prefix);
466 let (parents, name) = split_parent(prefix, &pd)?;
467 let (dir_guard, _) = self.session.walk(parents, &pd).await?;
468 match self
469 .session
470 .mkdir_at(dir_guard.fid(), name, mode, &pd)
471 .await
472 {
473 Ok(_) | Err(ZeroFsError::AlreadyExists { .. }) => {}
474 Err(e) => return Err(e),
475 }
476 }
477 if !names.is_empty() {
481 let meta = self.metadata(path).await?;
482 if !meta.is_dir() {
483 return Err(ZeroFsError::NotADirectory {
484 path: display(path),
485 });
486 }
487 }
488 Ok(())
489 }
490
491 pub async fn remove_file(&self, path: impl AsRef<Path>) -> Result<(), ZeroFsError> {
493 let path = path.as_ref();
494 let pd = display(path);
495 let (dir_guard, name) = self.parent_of(path, &pd).await?;
496 self.session
497 .client
498 .unlinkat(dir_guard.fid(), name, 0)
499 .await
500 .ctx(&pd)
501 }
502
503 pub async fn remove_dir(&self, path: impl AsRef<Path>) -> Result<(), ZeroFsError> {
505 let path = path.as_ref();
506 let pd = display(path);
507 let (dir_guard, name) = self.parent_of(path, &pd).await?;
508 self.session
509 .client
510 .unlinkat(dir_guard.fid(), name, crate::linux::AT_REMOVEDIR)
511 .await
512 .ctx(&pd)
513 }
514
515 pub async fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<(), ZeroFsError> {
518 let path = path.as_ref();
519 self.session.check_open()?;
520 if components(path)?.is_empty() {
521 return Err(ZeroFsError::InvalidArgument {
522 message: "refusing to remove the attach root".to_string(),
523 });
524 }
525 let dir = self.open_dir(path).await?;
526 let result = remove_dir_contents(&dir).await;
527 dir.close().await;
528 result?;
529 self.remove_dir(path).await
530 }
531
532 pub async fn rename(
535 &self,
536 from: impl AsRef<Path>,
537 to: impl AsRef<Path>,
538 ) -> Result<(), ZeroFsError> {
539 let (from, to) = (from.as_ref(), to.as_ref());
540 let (fd, td) = (display(from), display(to));
541 let (from_guard, from_name) = self.parent_of(from, &fd).await?;
542 let (to_guard, to_name) = self.parent_of(to, &td).await?;
543 self.session
544 .client
545 .renameat(from_guard.fid(), from_name, to_guard.fid(), to_name)
546 .await
547 .ctx(&fd)
548 }
549
550 pub async fn hard_link(
552 &self,
553 original: impl AsRef<Path>,
554 link: impl AsRef<Path>,
555 ) -> Result<Metadata, ZeroFsError> {
556 let (original, link) = (original.as_ref(), link.as_ref());
557 let (od, ld) = (display(original), display(link));
558 let (dir_guard, link_name) = self.parent_of(link, &ld).await?;
559 let orig_names = components(original)?;
560 let (orig_guard, _) = self.session.walk(&orig_names, &od).await?;
561 self.session
562 .link_at(dir_guard.fid(), orig_guard.fid(), link_name, &ld)
563 .await
564 }
565
566 pub async fn symlink(
569 &self,
570 target: impl AsRef<Path>,
571 link_path: impl AsRef<Path>,
572 ) -> Result<Metadata, ZeroFsError> {
573 let link_path = link_path.as_ref();
574 let ld = display(link_path);
575 let (dir_guard, name) = self.parent_of(link_path, &ld).await?;
576 let target = path_bytes(target.as_ref())?;
577 self.session
578 .symlink_at(dir_guard.fid(), name, target, &ld)
579 .await
580 }
581
582 pub async fn read_link(&self, path: impl AsRef<Path>) -> Result<PathBuf, ZeroFsError> {
584 let path = path.as_ref();
585 let pd = display(path);
586 self.session.check_open()?;
587 let names = components(path)?;
588 let (guard, _) = self.session.walk(&names, &pd).await?;
589 let target = self.session.client.readlink(guard.fid()).await.ctx(&pd)?;
590 path_from_bytes(target)
591 }
592
593 pub async fn mknod(
596 &self,
597 path: impl AsRef<Path>,
598 kind: NodeKind,
599 mode: u32,
600 ) -> Result<Metadata, ZeroFsError> {
601 let path = path.as_ref();
602 let pd = display(path);
603 let (dir_guard, name) = self.parent_of(path, &pd).await?;
604 self.session
605 .mknod_at(dir_guard.fid(), name, kind, mode, &pd)
606 .await
607 }
608
609 pub async fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<DirEntry>, ZeroFsError> {
612 let dir = self.open_dir(path).await?;
613 let mut out = Vec::new();
614 let result = loop {
615 match dir.next_batch(None).await {
616 Ok(batch) if batch.is_empty() => break Ok(out),
617 Ok(batch) => out.extend(batch),
618 Err(e) => break Err(e),
619 }
620 };
621 dir.close().await;
622 result
623 }
624
625 pub async fn open_dir(&self, path: impl AsRef<Path>) -> Result<Arc<Dir>, ZeroFsError> {
628 let path = path.as_ref();
629 let pd = display(path);
630 self.session.check_open()?;
631 let names = components(path)?;
632 let (guard, stat) = self.session.walk(&names, &pd).await?;
633 if FileType::from_mode(stat.mode) != FileType::Dir {
634 return Err(ZeroFsError::NotADirectory { path: pd });
635 }
636 Ok(Dir::new(
637 Arc::clone(&self.session),
638 guard,
639 display_path(&names),
640 ))
641 }
642
643 pub async fn open(
645 &self,
646 path: impl AsRef<Path>,
647 opts: OpenOptions,
648 ) -> Result<Arc<File>, ZeroFsError> {
649 let path = path.as_ref();
650 let pd = display(path);
651 self.session.check_open()?;
652 let guard = self.open_relative_path(path, &pd, &opts).await?;
653 Ok(File::new(Arc::clone(&self.session), guard, pd))
654 }
655
656 pub async fn create(&self, path: impl AsRef<Path>) -> Result<Arc<File>, ZeroFsError> {
658 let path = path.as_ref();
659 let pd = display(path);
660 self.session.check_open()?;
661 let opts = OpenOptions::read_write().create(true).truncate(true);
662 let guard = self.open_relative_path(path, &pd, &opts).await?;
663 Ok(File::new(Arc::clone(&self.session), guard, pd))
664 }
665
666 async fn resolve(&self, path: &Path, pd: &str) -> Result<(Vec<Vec<u8>>, Stat), ZeroFsError> {
671 let session = &self.session;
672
673 let literal: Vec<&[u8]> = components(path)?;
677 if let Ok((_guard, stat)) = session.walk(&literal, pd).await
678 && FileType::from_mode(stat.mode) != FileType::Symlink
679 {
680 return Ok((literal.iter().map(|c| c.to_vec()).collect(), stat));
681 }
682
683 let mut todo: VecDeque<Vec<u8>> = literal.iter().map(|c| c.to_vec()).collect();
684 let mut stack: Vec<Vec<u8>> = Vec::new();
685 let mut hops = 0u32;
686 let (mut cur, mut cur_stat) = session.walk(&[], pd).await?;
689
690 while let Some(name) = todo.pop_front() {
691 if name == b".." {
692 stack.pop();
695 let refs: Vec<&[u8]> = stack.iter().map(|c| c.as_slice()).collect();
696 let (guard, stat) = session.walk(&refs, pd).await?;
697 cur = guard;
698 cur_stat = stat;
699 continue;
700 }
701
702 let (guard, stat) = session.walk_from(cur.fid(), &[name.as_slice()], pd).await?;
703 if FileType::from_mode(stat.mode) == FileType::Symlink {
704 hops += 1;
705 if hops > MAX_SYMLINK_HOPS {
706 return Err(ZeroFsError::TooManySymlinks {
707 path: pd.to_string(),
708 });
709 }
710 let target = session.client.readlink(guard.fid()).await.ctx(pd)?;
711 if target.first() == Some(&b'/') {
712 stack.clear();
713 let (root_clone, root_stat) = session.walk(&[], pd).await?;
714 cur = root_clone;
715 cur_stat = root_stat;
716 }
717 for comp in target
719 .split(|&b| b == b'/')
720 .filter(|c| !c.is_empty() && *c != b".")
721 .rev()
722 {
723 todo.push_front(comp.to_vec());
724 }
725 } else {
726 stack.push(name);
727 cur = guard;
728 cur_stat = stat;
729 }
730 }
731
732 Ok((stack, cur_stat))
733 }
734}
735
736#[cfg(not(target_arch = "wasm32"))]
738type RemoveDirFuture<'a> =
739 std::pin::Pin<Box<dyn Future<Output = Result<(), ZeroFsError>> + Send + 'a>>;
740#[cfg(target_arch = "wasm32")]
741type RemoveDirFuture<'a> = std::pin::Pin<Box<dyn Future<Output = Result<(), ZeroFsError>> + 'a>>;
742
743fn remove_dir_contents<'a>(dir: &'a Dir) -> RemoveDirFuture<'a> {
744 Box::pin(async move {
745 loop {
746 dir.rewind().await?;
747 let batch = dir.next_batch(None).await?;
748 if batch.is_empty() {
749 return Ok(());
750 }
751 for entry in batch {
752 if entry.file_type == FileType::Dir {
753 let child = dir.open_dir_at(&entry.name_bytes).await?;
754 let result = remove_dir_contents(&child).await;
755 child.close().await;
756 result?;
757 dir.remove_dir_at(&entry.name_bytes).await?;
758 } else {
759 dir.remove_file_at(&entry.name_bytes).await?;
760 }
761 }
762 }
763 })
764}
765
766fn parse_targets<'a>(specs: impl IntoIterator<Item = &'a str>) -> Result<Vec<Target>, ZeroFsError> {
767 specs.into_iter().try_fold(Vec::new(), |mut targets, spec| {
768 targets.extend(Target::parse_list(spec).map_err(connect_failed)?);
769 Ok(targets)
770 })
771}
772
773fn connect_failed(message: String) -> ZeroFsError {
774 ZeroFsError::ConnectFailed { message }
775}
776
777async fn connect_before<T>(
778 timeout_ms: Option<u32>,
779 future: impl Future<Output = Result<T, ZeroFsError>>,
780 timeout_message: impl FnOnce(u32) -> String,
781) -> Result<T, ZeroFsError> {
782 match timeout_ms {
783 Some(ms) => crate::runtime::timeout(Duration::from_millis(ms.into()), future)
784 .await
785 .unwrap_or_else(|_| Err(connect_failed(timeout_message(ms)))),
786 None => future.await,
787 }
788}