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::{Component, Path, PathBuf},
  9    pin::Pin,
 10    time::{Duration, SystemTime},
 11};
 12use text::Rope;
 13
 14#[async_trait::async_trait]
 15pub trait Fs: Send + Sync {
 16    async fn create_dir(&self, path: &Path) -> Result<()>;
 17    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>;
 18    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
 19    async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
 20    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
 21    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
 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 open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
126        Ok(Box::new(std::fs::File::open(path)?))
127    }
128
129    async fn load(&self, path: &Path) -> Result<String> {
130        let mut file = smol::fs::File::open(path).await?;
131        let mut text = String::new();
132        file.read_to_string(&mut text).await?;
133        Ok(text)
134    }
135
136    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
137        let buffer_size = text.summary().bytes.min(10 * 1024);
138        let file = smol::fs::File::create(path).await?;
139        let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
140        for chunk in text.chunks() {
141            writer.write_all(chunk.as_bytes()).await?;
142        }
143        writer.flush().await?;
144        Ok(())
145    }
146
147    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
148        Ok(smol::fs::canonicalize(path).await?)
149    }
150
151    async fn is_file(&self, path: &Path) -> bool {
152        smol::fs::metadata(path)
153            .await
154            .map_or(false, |metadata| metadata.is_file())
155    }
156
157    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
158        let symlink_metadata = match smol::fs::symlink_metadata(path).await {
159            Ok(metadata) => metadata,
160            Err(err) => {
161                return match (err.kind(), err.raw_os_error()) {
162                    (io::ErrorKind::NotFound, _) => Ok(None),
163                    (io::ErrorKind::Other, Some(libc::ENOTDIR)) => Ok(None),
164                    _ => Err(anyhow::Error::new(err)),
165                }
166            }
167        };
168
169        let is_symlink = symlink_metadata.file_type().is_symlink();
170        let metadata = if is_symlink {
171            smol::fs::metadata(path).await?
172        } else {
173            symlink_metadata
174        };
175        Ok(Some(Metadata {
176            inode: metadata.ino(),
177            mtime: metadata.modified().unwrap(),
178            is_symlink,
179            is_dir: metadata.file_type().is_dir(),
180        }))
181    }
182
183    async fn read_dir(
184        &self,
185        path: &Path,
186    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
187        let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
188            Ok(entry) => Ok(entry.path()),
189            Err(error) => Err(anyhow!("failed to read dir entry {:?}", error)),
190        });
191        Ok(Box::pin(result))
192    }
193
194    async fn watch(
195        &self,
196        path: &Path,
197        latency: Duration,
198    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
199        let (tx, rx) = smol::channel::unbounded();
200        let (stream, handle) = EventStream::new(&[path], latency);
201        std::mem::forget(handle);
202        std::thread::spawn(move || {
203            stream.run(move |events| smol::block_on(tx.send(events)).is_ok());
204        });
205        Box::pin(rx)
206    }
207
208    fn is_fake(&self) -> bool {
209        false
210    }
211    #[cfg(any(test, feature = "test-support"))]
212    fn as_fake(&self) -> &FakeFs {
213        panic!("called `RealFs::as_fake`")
214    }
215}
216
217#[cfg(any(test, feature = "test-support"))]
218#[derive(Clone, Debug)]
219struct FakeFsEntry {
220    metadata: Metadata,
221    content: Option<String>,
222}
223
224#[cfg(any(test, feature = "test-support"))]
225struct FakeFsState {
226    entries: std::collections::BTreeMap<PathBuf, FakeFsEntry>,
227    next_inode: u64,
228    event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
229}
230
231#[cfg(any(test, feature = "test-support"))]
232impl FakeFsState {
233    fn validate_path(&self, path: &Path) -> Result<()> {
234        if path.is_absolute()
235            && path
236                .parent()
237                .and_then(|path| self.entries.get(path))
238                .map_or(false, |e| e.metadata.is_dir)
239        {
240            Ok(())
241        } else {
242            Err(anyhow!("invalid path {:?}", path))
243        }
244    }
245
246    async fn emit_event<I, T>(&mut self, paths: I)
247    where
248        I: IntoIterator<Item = T>,
249        T: Into<PathBuf>,
250    {
251        let events = paths
252            .into_iter()
253            .map(|path| fsevent::Event {
254                event_id: 0,
255                flags: fsevent::StreamFlags::empty(),
256                path: path.into(),
257            })
258            .collect::<Vec<_>>();
259
260        self.event_txs.retain(|tx| {
261            let _ = tx.try_send(events.clone());
262            !tx.is_closed()
263        });
264    }
265}
266
267#[cfg(any(test, feature = "test-support"))]
268pub struct FakeFs {
269    // Use an unfair lock to ensure tests are deterministic.
270    state: futures::lock::Mutex<FakeFsState>,
271    executor: std::sync::Weak<gpui::executor::Background>,
272}
273
274#[cfg(any(test, feature = "test-support"))]
275impl FakeFs {
276    pub fn new(executor: std::sync::Arc<gpui::executor::Background>) -> std::sync::Arc<Self> {
277        let mut entries = std::collections::BTreeMap::new();
278        entries.insert(
279            Path::new("/").to_path_buf(),
280            FakeFsEntry {
281                metadata: Metadata {
282                    inode: 0,
283                    mtime: SystemTime::now(),
284                    is_dir: true,
285                    is_symlink: false,
286                },
287                content: None,
288            },
289        );
290        std::sync::Arc::new(Self {
291            executor: std::sync::Arc::downgrade(&executor),
292            state: futures::lock::Mutex::new(FakeFsState {
293                entries,
294                next_inode: 1,
295                event_txs: Default::default(),
296            }),
297        })
298    }
299
300    pub async fn insert_dir(&self, path: impl AsRef<Path>) {
301        let mut state = self.state.lock().await;
302        let path = path.as_ref();
303        state.validate_path(path).unwrap();
304
305        let inode = state.next_inode;
306        state.next_inode += 1;
307        state.entries.insert(
308            path.to_path_buf(),
309            FakeFsEntry {
310                metadata: Metadata {
311                    inode,
312                    mtime: SystemTime::now(),
313                    is_dir: true,
314                    is_symlink: false,
315                },
316                content: None,
317            },
318        );
319        state.emit_event(&[path]).await;
320    }
321
322    pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
323        let mut state = self.state.lock().await;
324        let path = path.as_ref();
325        state.validate_path(path).unwrap();
326
327        let inode = state.next_inode;
328        state.next_inode += 1;
329        state.entries.insert(
330            path.to_path_buf(),
331            FakeFsEntry {
332                metadata: Metadata {
333                    inode,
334                    mtime: SystemTime::now(),
335                    is_dir: false,
336                    is_symlink: false,
337                },
338                content: Some(content),
339            },
340        );
341        state.emit_event(&[path]).await;
342    }
343
344    #[must_use]
345    pub fn insert_tree<'a>(
346        &'a self,
347        path: impl 'a + AsRef<Path> + Send,
348        tree: serde_json::Value,
349    ) -> futures::future::BoxFuture<'a, ()> {
350        use futures::FutureExt as _;
351        use serde_json::Value::*;
352
353        async move {
354            let path = path.as_ref();
355
356            match tree {
357                Object(map) => {
358                    self.insert_dir(path).await;
359                    for (name, contents) in map {
360                        let mut path = PathBuf::from(path);
361                        path.push(name);
362                        self.insert_tree(&path, contents).await;
363                    }
364                }
365                Null => {
366                    self.insert_dir(&path).await;
367                }
368                String(contents) => {
369                    self.insert_file(&path, contents).await;
370                }
371                _ => {
372                    panic!("JSON object must contain only objects, strings, or null");
373                }
374            }
375        }
376        .boxed()
377    }
378
379    pub async fn files(&self) -> Vec<PathBuf> {
380        self.state
381            .lock()
382            .await
383            .entries
384            .iter()
385            .filter_map(|(path, entry)| entry.content.as_ref().map(|_| path.clone()))
386            .collect()
387    }
388
389    async fn simulate_random_delay(&self) {
390        self.executor
391            .upgrade()
392            .expect("executor has been dropped")
393            .simulate_random_delay()
394            .await;
395    }
396}
397
398#[cfg(any(test, feature = "test-support"))]
399#[async_trait::async_trait]
400impl Fs for FakeFs {
401    async fn create_dir(&self, path: &Path) -> Result<()> {
402        self.simulate_random_delay().await;
403        let state = &mut *self.state.lock().await;
404        let path = normalize_path(path);
405        let mut ancestor_path = PathBuf::new();
406        let mut created_dir_paths = Vec::new();
407        for component in path.components() {
408            ancestor_path.push(component);
409            let entry = state
410                .entries
411                .entry(ancestor_path.clone())
412                .or_insert_with(|| {
413                    let inode = state.next_inode;
414                    state.next_inode += 1;
415                    created_dir_paths.push(ancestor_path.clone());
416                    FakeFsEntry {
417                        metadata: Metadata {
418                            inode,
419                            mtime: SystemTime::now(),
420                            is_dir: true,
421                            is_symlink: false,
422                        },
423                        content: None,
424                    }
425                });
426            if !entry.metadata.is_dir {
427                return Err(anyhow!(
428                    "cannot create directory because {:?} is a file",
429                    ancestor_path
430                ));
431            }
432        }
433        state.emit_event(&created_dir_paths).await;
434
435        Ok(())
436    }
437
438    async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> {
439        self.simulate_random_delay().await;
440        let mut state = self.state.lock().await;
441        let path = normalize_path(path);
442        state.validate_path(&path)?;
443        if let Some(entry) = state.entries.get_mut(&path) {
444            if entry.metadata.is_dir || entry.metadata.is_symlink {
445                return Err(anyhow!(
446                    "cannot create file because {:?} is a dir or a symlink",
447                    path
448                ));
449            }
450
451            if options.overwrite {
452                entry.metadata.mtime = SystemTime::now();
453                entry.content = Some(Default::default());
454            } else if !options.ignore_if_exists {
455                return Err(anyhow!(
456                    "cannot create file because {:?} already exists",
457                    &path
458                ));
459            }
460        } else {
461            let inode = state.next_inode;
462            state.next_inode += 1;
463            let entry = FakeFsEntry {
464                metadata: Metadata {
465                    inode,
466                    mtime: SystemTime::now(),
467                    is_dir: false,
468                    is_symlink: false,
469                },
470                content: Some(Default::default()),
471            };
472            state.entries.insert(path.to_path_buf(), entry);
473        }
474        state.emit_event(&[path]).await;
475
476        Ok(())
477    }
478
479    async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> {
480        let source = normalize_path(source);
481        let target = normalize_path(target);
482
483        let mut state = self.state.lock().await;
484        state.validate_path(&source)?;
485        state.validate_path(&target)?;
486
487        if !options.overwrite && state.entries.contains_key(&target) {
488            if options.ignore_if_exists {
489                return Ok(());
490            } else {
491                return Err(anyhow!("{target:?} already exists"));
492            }
493        }
494
495        let mut removed = Vec::new();
496        state.entries.retain(|path, entry| {
497            if let Ok(relative_path) = path.strip_prefix(&source) {
498                removed.push((relative_path.to_path_buf(), entry.clone()));
499                false
500            } else {
501                true
502            }
503        });
504
505        for (relative_path, entry) in removed {
506            let new_path = normalize_path(&target.join(relative_path));
507            state.entries.insert(new_path, entry);
508        }
509
510        state.emit_event(&[source, target]).await;
511        Ok(())
512    }
513
514    async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
515        let dir_path = normalize_path(dir_path);
516        let mut state = self.state.lock().await;
517        state.validate_path(&dir_path)?;
518        if let Some(entry) = state.entries.get(&dir_path) {
519            if !entry.metadata.is_dir {
520                return Err(anyhow!(
521                    "cannot remove {dir_path:?} because it is not a dir"
522                ));
523            }
524
525            if !options.recursive {
526                let descendants = state
527                    .entries
528                    .keys()
529                    .filter(|path| path.starts_with(path))
530                    .count();
531                if descendants > 1 {
532                    return Err(anyhow!("{dir_path:?} is not empty"));
533                }
534            }
535
536            state.entries.retain(|path, _| !path.starts_with(&dir_path));
537            state.emit_event(&[dir_path]).await;
538        } else if !options.ignore_if_not_exists {
539            return Err(anyhow!("{dir_path:?} does not exist"));
540        }
541
542        Ok(())
543    }
544
545    async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
546        let path = normalize_path(path);
547        let mut state = self.state.lock().await;
548        state.validate_path(&path)?;
549        if let Some(entry) = state.entries.get(&path) {
550            if entry.metadata.is_dir {
551                return Err(anyhow!("cannot remove {path:?} because it is not a file"));
552            }
553
554            state.entries.remove(&path);
555            state.emit_event(&[path]).await;
556        } else if !options.ignore_if_not_exists {
557            return Err(anyhow!("{path:?} does not exist"));
558        }
559        Ok(())
560    }
561
562    async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
563        let text = self.load(path).await?;
564        Ok(Box::new(io::Cursor::new(text)))
565    }
566
567    async fn load(&self, path: &Path) -> Result<String> {
568        let path = normalize_path(path);
569        self.simulate_random_delay().await;
570        let state = self.state.lock().await;
571        let text = state
572            .entries
573            .get(&path)
574            .and_then(|e| e.content.as_ref())
575            .ok_or_else(|| anyhow!("file {:?} does not exist", path))?;
576        Ok(text.clone())
577    }
578
579    async fn save(&self, path: &Path, text: &Rope) -> Result<()> {
580        self.simulate_random_delay().await;
581        let mut state = self.state.lock().await;
582        let path = normalize_path(path);
583        state.validate_path(&path)?;
584        if let Some(entry) = state.entries.get_mut(&path) {
585            if entry.metadata.is_dir {
586                Err(anyhow!("cannot overwrite a directory with a file"))
587            } else {
588                entry.content = Some(text.chunks().collect());
589                entry.metadata.mtime = SystemTime::now();
590                state.emit_event(&[path]).await;
591                Ok(())
592            }
593        } else {
594            let inode = state.next_inode;
595            state.next_inode += 1;
596            let entry = FakeFsEntry {
597                metadata: Metadata {
598                    inode,
599                    mtime: SystemTime::now(),
600                    is_dir: false,
601                    is_symlink: false,
602                },
603                content: Some(text.chunks().collect()),
604            };
605            state.entries.insert(path.to_path_buf(), entry);
606            state.emit_event(&[path]).await;
607            Ok(())
608        }
609    }
610
611    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
612        self.simulate_random_delay().await;
613        Ok(normalize_path(path))
614    }
615
616    async fn is_file(&self, path: &Path) -> bool {
617        let path = normalize_path(path);
618        self.simulate_random_delay().await;
619        let state = self.state.lock().await;
620        state
621            .entries
622            .get(&path)
623            .map_or(false, |entry| !entry.metadata.is_dir)
624    }
625
626    async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
627        self.simulate_random_delay().await;
628        let state = self.state.lock().await;
629        let path = normalize_path(path);
630        Ok(state.entries.get(&path).map(|entry| entry.metadata.clone()))
631    }
632
633    async fn read_dir(
634        &self,
635        abs_path: &Path,
636    ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
637        use futures::{future, stream};
638        self.simulate_random_delay().await;
639        let state = self.state.lock().await;
640        let abs_path = normalize_path(abs_path);
641        Ok(Box::pin(stream::iter(state.entries.clone()).filter_map(
642            move |(child_path, _)| {
643                future::ready(if child_path.parent() == Some(&abs_path) {
644                    Some(Ok(child_path))
645                } else {
646                    None
647                })
648            },
649        )))
650    }
651
652    async fn watch(
653        &self,
654        path: &Path,
655        _: Duration,
656    ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
657        let mut state = self.state.lock().await;
658        self.simulate_random_delay().await;
659        let (tx, rx) = smol::channel::unbounded();
660        state.event_txs.push(tx);
661        let path = path.to_path_buf();
662        let executor = self.executor.clone();
663        Box::pin(futures::StreamExt::filter(rx, move |events| {
664            let result = events.iter().any(|event| event.path.starts_with(&path));
665            let executor = executor.clone();
666            async move {
667                if let Some(executor) = executor.clone().upgrade() {
668                    executor.simulate_random_delay().await;
669                }
670                result
671            }
672        }))
673    }
674
675    fn is_fake(&self) -> bool {
676        true
677    }
678
679    #[cfg(any(test, feature = "test-support"))]
680    fn as_fake(&self) -> &FakeFs {
681        self
682    }
683}
684
685pub fn normalize_path(path: &Path) -> PathBuf {
686    let mut components = path.components().peekable();
687    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
688        components.next();
689        PathBuf::from(c.as_os_str())
690    } else {
691        PathBuf::new()
692    };
693
694    for component in components {
695        match component {
696            Component::Prefix(..) => unreachable!(),
697            Component::RootDir => {
698                ret.push(component.as_os_str());
699            }
700            Component::CurDir => {}
701            Component::ParentDir => {
702                ret.pop();
703            }
704            Component::Normal(c) => {
705                ret.push(c);
706            }
707        }
708    }
709    ret
710}