1use anyhow::{anyhow, Result};
2use fsevent::EventStream;
3use futures::{Stream, StreamExt};
4use smol::io::{AsyncReadExt, AsyncWriteExt};
5use std::{
6 io,
7 os::unix::fs::MetadataExt,
8 path::{Path, PathBuf},
9 pin::Pin,
10 sync::Arc,
11 time::{Duration, SystemTime},
12};
13use text::Rope;
14
15#[async_trait::async_trait]
16pub trait Fs: Send + Sync {
17 async fn create_dir(&self, path: &Path) -> Result<()>;
18 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
19 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
20 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
21 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
22 async fn load(&self, path: &Path) -> Result<String>;
23 async fn save(&self, path: &Path, text: &Rope) -> Result<()>;
24 async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
25 async fn is_file(&self, path: &Path) -> bool;
26 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
27 async fn read_dir(
28 &self,
29 path: &Path,
30 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
31 async fn watch(
32 &self,
33 path: &Path,
34 latency: Duration,
35 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
36 fn is_fake(&self) -> bool;
37 #[cfg(any(test, feature = "test-support"))]
38 fn as_fake(&self) -> &FakeFs;
39}
40
41#[derive(Copy, Clone, Default)]
42pub struct CreateOptions {
43 pub overwrite: bool,
44 pub ignore_if_exists: bool,
45}
46
47#[derive(Copy, Clone, Default)]
48pub struct RenameOptions {
49 pub overwrite: bool,
50 pub ignore_if_exists: bool,
51}
52
53#[derive(Copy, Clone, Default)]
54pub struct RemoveOptions {
55 pub recursive: bool,
56 pub ignore_if_not_exists: bool,
57}
58
59#[derive(Clone, Debug)]
60pub struct Metadata {
61 pub inode: u64,
62 pub mtime: SystemTime,
63 pub is_symlink: bool,
64 pub is_dir: bool,
65}
66
67pub struct RealFs;
68
69#[async_trait::async_trait]
70impl Fs for RealFs {
71 async fn create_dir(&self, path: &Path) -> Result<()> {
72 Ok(smol::fs::create_dir_all(path).await?)
73 }
74
75 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
76 let mut open_options = smol::fs::OpenOptions::new();
77 open_options.write(true).create(true);
78 if options.overwrite {
79 open_options.truncate(true);
80 } else if !options.ignore_if_exists {
81 open_options.create_new(true);
82 }
83 open_options.open(path).await?;
84 Ok(())
85 }
86
87 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
88 if !options.overwrite && smol::fs::metadata(target).await.is_ok() {
89 if options.ignore_if_exists {
90 return Ok(());
91 } else {
92 return Err(anyhow!("{target:?} already exists"));
93 }
94 }
95
96 smol::fs::rename(source, target).await?;
97 Ok(())
98 }
99
100 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
101 let result = if options.recursive {
102 smol::fs::remove_dir_all(path).await
103 } else {
104 smol::fs::remove_dir(path).await
105 };
106 match result {
107 Ok(()) => Ok(()),
108 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
109 Ok(())
110 }
111 Err(err) => Err(err)?,
112 }
113 }
114
115 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
116 match smol::fs::remove_file(path).await {
117 Ok(()) => Ok(()),
118 Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => {
119 Ok(())
120 }
121 Err(err) => Err(err)?,
122 }
123 }
124
125 async fn load(&self, path: &Path) -> Result<String> {
126 let mut file = smol::fs::File::open(path).await?;
127 let mut text = String::new();
128 file.read_to_string(&mut text).await?;
129 Ok(text)
130 }
131
132 async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
133 let buffer_size = text.summary().bytes.min(10 * 1024);
134 let file = smol::fs::File::create(path).await?;
135 let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
136 for chunk in text.chunks() {
137 writer.write_all(chunk.as_bytes()).await?;
138 }
139 writer.flush().await?;
140 Ok(())
141 }
142
143 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
144 Ok(smol::fs::canonicalize(path).await?)
145 }
146
147 async fn is_file(&self, path: &Path) -> bool {
148 smol::fs::metadata(path)
149 .await
150 .map_or(false, |metadata| metadata.is_file())
151 }
152
153 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
154 let symlink_metadata = match smol::fs::symlink_metadata(path).await {
155 Ok(metadata) => metadata,
156 Err(err) => {
157 return match (err.kind(), err.raw_os_error()) {
158 (io::ErrorKind::NotFound, _) => Ok(None),
159 (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
160 _ => Err(anyhow::Error::new(err)),
161 }
162 }
163 };
164
165 let is_symlink = symlink_metadata.file_type().is_symlink();
166 let metadata = if is_symlink {
167 smol::fs::metadata(path).await?
168 } else {
169 symlink_metadata
170 };
171 Ok(Some(Metadata {
172 inode: metadata.ino(),
173 mtime: metadata.modified().unwrap(),
174 is_symlink,
175 is_dir: metadata.file_type().is_dir(),
176 }))
177 }
178
179 async fn read_dir(
180 &self,
181 path: &Path,
182 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
183 let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
184 Ok(entry) => Ok(entry.path()),
185 Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
186 });
187 Ok(Box::pin(result))
188 }
189
190 async fn watch(
191 &self,
192 path: &Path,
193 latency: Duration,
194 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
195 let (tx, rx) = smol::channel::unbounded();
196 let (stream, handle) = EventStream::new(&[path], latency);
197 std::mem::forget(handle);
198 std::thread::spawn(move || {
199 stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
200 });
201 Box::pin(rx)
202 }
203
204 fn is_fake(&self) -> bool {
205 false
206 }
207
208 #[cfg(any(test, feature = "test-support"))]
209 fn as_fake(&self) -> &FakeFs {
210 panic!("called `RealFs::as_fake`")
211 }
212}
213
214#[cfg(any(test, feature = "test-support"))]
215#[derive(Clone, Debug)]
216struct FakeFsEntry {
217 metadata: Metadata,
218 content: Option<String>,
219}
220
221#[cfg(any(test, feature = "test-support"))]
222struct FakeFsState {
223 entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
224 next_inode: u64,
225 events_tx: postage::broadcast::Sender<Vec<fsevent::Event>>,
226}
227
228#[cfg(any(test, feature = "test-support"))]
229impl FakeFsState {
230 fn validate_path(&self, path: &Path) -> Result<()> {
231 if path.is_absolute()
232 && path
233 .parent()
234 .and_then(|path| self.entries.get(path))
235 .map_or(false, |e| e.metadata.is_dir)
236 {
237 Ok(())
238 } else {
239 Err(anyhow!("invalid path {:?}", path))
240 }
241 }
242
243 async fn emit_event<I, T>(&mut self, paths: I)
244 where
245 I: IntoIterator<Item = T>,
246 T: Into<PathBuf>,
247 {
248 use postage::prelude::Sink as _;
249
250 let events = paths
251 .into_iter()
252 .map(|path| fsevent::Event {
253 event_id: 0,
254 flags: fsevent::StreamFlags::empty(),
255 path: path.into(),
256 })
257 .collect();
258
259 let _ = self.events_tx.send(events).await;
260 }
261}
262
263#[cfg(any(test, feature = "test-support"))]
264pub struct FakeFs {
265 // Use an unfair lock to ensure tests are deterministic.
266 state: futures::lock::Mutex<FakeFsState>,
267 executor: std::sync::Arc<gpui::executor::Background>,
268}
269
270#[cfg(any(test, feature = "test-support"))]
271impl FakeFs {
272 pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> Arc<Self> {
273 let (events_tx, _) = postage::broadcast::channel(2048);
274 let mut entries = std::collections::BTreeMap::new();
275 entries.insert(
276 Path::new("/").to_path_buf(),
277 FakeFsEntry {
278 metadata: Metadata {
279 inode: 0,
280 mtime: SystemTime::now(),
281 is_dir: true,
282 is_symlink: false,
283 },
284 content: None,
285 },
286 );
287 Arc::new(Self {
288 executor,
289 state: futures::lock::Mutex::new(FakeFsState {
290 entries,
291 next_inode: 1,
292 events_tx,
293 }),
294 })
295 }
296
297 pub async fn insert_dir(&self, path: impl AsRef<Path>) {
298 let mut state = self.state.lock().await;
299 let path = path.as_ref();
300 state.validate_path(path).unwrap();
301
302 let inode = state.next_inode;
303 state.next_inode += 1;
304 state.entries.insert(
305 path.to_path_buf(),
306 FakeFsEntry {
307 metadata: Metadata {
308 inode,
309 mtime: SystemTime::now(),
310 is_dir: true,
311 is_symlink: false,
312 },
313 content: None,
314 },
315 );
316 state.emit_event(&[path]).await;
317 }
318
319 pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
320 let mut state = self.state.lock().await;
321 let path = path.as_ref();
322 state.validate_path(path).unwrap();
323
324 let inode = state.next_inode;
325 state.next_inode += 1;
326 state.entries.insert(
327 path.to_path_buf(),
328 FakeFsEntry {
329 metadata: Metadata {
330 inode,
331 mtime: SystemTime::now(),
332 is_dir: false,
333 is_symlink: false,
334 },
335 content: Some(content),
336 },
337 );
338 state.emit_event(&[path]).await;
339 }
340
341 #[must_use]
342 pub fn insert_tree<'a>(
343 &'a self,
344 path: impl 'a + AsRef<Path> + Send,
345 tree: serde_json::Value,
346 ) -> futures::future::BoxFuture<'a, ()> {
347 use futures::FutureExt as _;
348 use serde_json::Value::*;
349
350 async move {
351 let path = path.as_ref();
352
353 match tree {
354 Object(map) => {
355 self.insert_dir(path).await;
356 for (name, contents) in map {
357 let mut path = PathBuf::from(path);
358 path.push(name);
359 self.insert_tree(&path, contents).await;
360 }
361 }
362 Null => {
363 self.insert_dir(&path).await;
364 }
365 String(contents) => {
366 self.insert_file(&path, contents).await;
367 }
368 _ => {
369 panic!("JSON object must contain only objects, strings, or null");
370 }
371 }
372 }
373 .boxed()
374 }
375}
376
377#[cfg(any(test, feature = "test-support"))]
378#[async_trait::async_trait]
379impl Fs for FakeFs {
380 async fn create_dir(&self, path: &Path) -> Result<()> {
381 self.executor.simulate_random_delay().await;
382 let state = &mut *self.state.lock().await;
383 let mut ancestor_path = PathBuf::new();
384 let mut created_dir_paths = Vec::new();
385 for component in path.components() {
386 ancestor_path.push(component);
387 let entry = state
388 .entries
389 .entry(ancestor_path.clone())
390 .or_insert_with(|| {
391 let inode = state.next_inode;
392 state.next_inode += 1;
393 created_dir_paths.push(ancestor_path.clone());
394 FakeFsEntry {
395 metadata: Metadata {
396 inode,
397 mtime: SystemTime::now(),
398 is_dir: true,
399 is_symlink: false,
400 },
401 content: None,
402 }
403 });
404 if !entry.metadata.is_dir {
405 return Err(anyhow!(
406 "cannot create directory because {:?} is a file",
407 ancestor_path
408 ));
409 }
410 }
411 state.emit_event(&created_dir_paths).await;
412
413 Ok(())
414 }
415
416 async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
417 self.executor.simulate_random_delay().await;
418 let mut state = self.state.lock().await;
419 state.validate_path(path)?;
420 if let Some(entry) = state.entries.get_mut(path) {
421 if entry.metadata.is_dir || entry.metadata.is_symlink {
422 return Err(anyhow!(
423 "cannot create file because {:?} is a dir or a symlink",
424 path
425 ));
426 }
427
428 if options.overwrite {
429 entry.metadata.mtime = SystemTime::now();
430 entry.content = Some(Default::default());
431 } else if !options.ignore_if_exists {
432 return Err(anyhow!(
433 "cannot create file because {:?} already exists",
434 path
435 ));
436 }
437 } else {
438 let inode = state.next_inode;
439 state.next_inode += 1;
440 let entry = FakeFsEntry {
441 metadata: Metadata {
442 inode,
443 mtime: SystemTime::now(),
444 is_dir: false,
445 is_symlink: false,
446 },
447 content: Some(Default::default()),
448 };
449 state.entries.insert(path.to_path_buf(), entry);
450 }
451 state.emit_event(&[path]).await;
452
453 Ok(())
454 }
455
456 async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
457 let mut state = self.state.lock().await;
458 state.validate_path(source)?;
459 state.validate_path(target)?;
460
461 if !options.overwrite && state.entries.contains_key(target) {
462 if options.ignore_if_exists {
463 return Ok(());
464 } else {
465 return Err(anyhow!("{target:?} already exists"));
466 }
467 }
468
469 let mut removed = Vec::new();
470 state.entries.retain(|path, entry| {
471 if let Ok(relative_path) = path.strip_prefix(source) {
472 removed.push((relative_path.to_path_buf(), entry.clone()));
473 false
474 } else {
475 true
476 }
477 });
478
479 for (relative_path, entry) in removed {
480 let new_path = target.join(relative_path);
481 state.entries.insert(new_path, entry);
482 }
483
484 state.emit_event(&[source, target]).await;
485 Ok(())
486 }
487
488 async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
489 let mut state = self.state.lock().await;
490 state.validate_path(path)?;
491 if let Some(entry) = state.entries.get(path) {
492 if !entry.metadata.is_dir {
493 return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
494 }
495
496 if !options.recursive {
497 let descendants = state
498 .entries
499 .keys()
500 .filter(|path| path.starts_with(path))
501 .count();
502 if descendants > 1 {
503 return Err(anyhow!("{path:?} is not empty"));
504 }
505 }
506
507 state.entries.retain(|path, _| !path.starts_with(path));
508 state.emit_event(&[path]).await;
509 } else if !options.ignore_if_not_exists {
510 return Err(anyhow!("{path:?} does not exist"));
511 }
512
513 Ok(())
514 }
515
516 async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
517 let mut state = self.state.lock().await;
518 state.validate_path(path)?;
519 if let Some(entry) = state.entries.get(path) {
520 if entry.metadata.is_dir {
521 return Err(anyhow!("cannot remove {path:?} because it is not a file"));
522 }
523
524 state.entries.remove(path);
525 state.emit_event(&[path]).await;
526 } else if !options.ignore_if_not_exists {
527 return Err(anyhow!("{path:?} does not exist"));
528 }
529 Ok(())
530 }
531
532 async fn load(&self, path: &Path) -> Result<String> {
533 self.executor.simulate_random_delay().await;
534 let state = self.state.lock().await;
535 let text = state
536 .entries
537 .get(path)
538 .and_then(|e| e.content.as_ref())
539 .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
540 Ok(text.clone())
541 }
542
543 async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
544 self.executor.simulate_random_delay().await;
545 let mut state = self.state.lock().await;
546 state.validate_path(path)?;
547 if let Some(entry) = state.entries.get_mut(path) {
548 if entry.metadata.is_dir {
549 Err(anyhow!("cannot overwrite a directory with a file"))
550 } else {
551 entry.content = Some(text.chunks().collect());
552 entry.metadata.mtime = SystemTime::now();
553 state.emit_event(&[path]).await;
554 Ok(())
555 }
556 } else {
557 let inode = state.next_inode;
558 state.next_inode += 1;
559 let entry = FakeFsEntry {
560 metadata: Metadata {
561 inode,
562 mtime: SystemTime::now(),
563 is_dir: false,
564 is_symlink: false,
565 },
566 content: Some(text.chunks().collect()),
567 };
568 state.entries.insert(path.to_path_buf(), entry);
569 state.emit_event(&[path]).await;
570 Ok(())
571 }
572 }
573
574 async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
575 self.executor.simulate_random_delay().await;
576 Ok(path.to_path_buf())
577 }
578
579 async fn is_file(&self, path: &Path) -> bool {
580 self.executor.simulate_random_delay().await;
581 let state = self.state.lock().await;
582 state
583 .entries
584 .get(path)
585 .map_or(false, |entry| !entry.metadata.is_dir)
586 }
587
588 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
589 self.executor.simulate_random_delay().await;
590 let state = self.state.lock().await;
591 Ok(state.entries.get(path).map(|entry| entry.metadata.clone()))
592 }
593
594 async fn read_dir(
595 &self,
596 abs_path: &Path,
597 ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
598 use futures::{future, stream};
599 self.executor.simulate_random_delay().await;
600 let state = self.state.lock().await;
601 let abs_path = abs_path.to_path_buf();
602 Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
603 move |(child_path, _)| {
604 future::ready(if child_path.parent() == Some(&abs_path) {
605 Some(Ok(child_path))
606 } else {
607 None
608 })
609 },
610 )))
611 }
612
613 async fn watch(
614 &self,
615 path: &Path,
616 _: Duration,
617 ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
618 let state = self.state.lock().await;
619 self.executor.simulate_random_delay().await;
620 let rx = state.events_tx.subscribe();
621 let path = path.to_path_buf();
622 Box::pin(futures::StreamExt::filter(rx, move |events| {
623 let result = events.iter().any(|event| event.path.starts_with(&path));
624 async move { result }
625 }))
626 }
627
628 fn is_fake(&self) -> bool {
629 true
630 }
631
632 #[cfg(any(test, feature = "test-support"))]
633 fn as_fake(&self) -> &FakeFs {
634 self
635 }
636}