fs.rs

  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}