#[cfg(target_os = "macos")]mod mac_watcher; #[cfg(any(target_os = "linux", target_os = "freebsd"))]pub mod linux_watcher; use anyhow::{anyhow, Result};use git::GitHostingProviderRegistry; #[cfg(any(target_os = "linux", target_os = "freebsd"))]use ashpd::desktop::trash;#[cfg(any(target_os = "linux", target_os = "freebsd"))]use std::fs::File;#[cfg(unix)]use std::os::fd::AsFd;#[cfg(unix)]use std::os::fd::AsRawFd; #[cfg(unix)]use std::os::unix::fs::MetadataExt; #[cfg(unix)]use std::os::unix::fs::FileTypeExt; use async_tar::Archive;use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};use git::repository::{GitRepository, RealGitRepository};use gpui::{AppContext, Global, ReadGlobal};use rope::Rope;use serde::{Deserialize, Serialize};use smol::io::AsyncWriteExt;use std::{ io::{self, Write}, path::{Component, Path, PathBuf}, pin::Pin, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH},};use tempfile::{NamedTempFile, TempDir};use text::LineEnding;use util::ResultExt; #[cfg(any(test, feature = "test-support"))]use collections::{btree_map, BTreeMap};#[cfg(any(test, feature = "test-support"))]use git::repository::{FakeGitRepositoryState, GitFileStatus};#[cfg(any(test, feature = "test-support"))]use parking_lot::Mutex;#[cfg(any(test, feature = "test-support"))]use smol::io::AsyncReadExt;#[cfg(any(test, feature = "test-support"))]use std::ffi::OsStr; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>;} #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]pub enum PathEventKind { Removed, Created, Changed,} #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]pub struct PathEvent { pub path: PathBuf, pub kind: Option<PathEventKind>,} impl From<PathEvent> for PathBuf { fn from(event: PathEvent) -> Self { event.path }} #[async_trait::async_trait]pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()>; async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()>; async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_dir(path, options).await } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_file(path, options).await } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>; async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>; async fn load(&self, path: &Path) -> Result<String> { Ok(String::from_utf8(self.load_bytes(path).await?)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn is_file(&self, path: &Path) -> bool; async fn is_dir(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>; async fn read_link(&self, path: &Path) -> Result<PathBuf>; async fn read_dir( &self, path: &Path, ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>; async fn watch( &self, path: &Path, latency: Duration, ) -> ( Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>, Arc<dyn Watcher>, ); fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result<bool>; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> Arc<FakeFs> { panic!("called as_fake on a real fs"); }} struct GlobalFs(Arc<dyn Fs>); impl Global for GlobalFs {} impl dyn Fs { /// Returns the global [`Fs`]. pub fn global(cx: &AppContext) -> Arc<Self> { GlobalFs::global(cx).0.clone() } /// Sets the global [`Fs`]. pub fn set_global(fs: Arc<Self>, cx: &mut AppContext) { cx.set_global(GlobalFs(fs)); }} #[derive(Copy, Clone, Default)]pub struct CreateOptions { pub overwrite: bool, pub ignore_if_exists: bool,} #[derive(Copy, Clone, Default)]pub struct CopyOptions { pub overwrite: bool, pub ignore_if_exists: bool,} #[derive(Copy, Clone, Default)]pub struct RenameOptions { pub overwrite: bool, pub ignore_if_exists: bool,} #[derive(Copy, Clone, Default)]pub struct RemoveOptions { pub recursive: bool, pub ignore_if_not_exists: bool,} #[derive(Copy, Clone, Debug)]pub struct Metadata { pub inode: u64, pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool,} /// Filesystem modification time. The purpose of this newtype is to discourage use of operations/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113).////// Do not derive Ord, PartialOrd, or arithmetic operation traits.#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]#[serde(transparent)]pub struct MTime(SystemTime); impl MTime { /// Conversion intended for persistence and testing. pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { MTime(UNIX_EPOCH + Duration::new(secs, nanos)) } /// Conversion intended for persistence. pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { self.0 .duration_since(UNIX_EPOCH) .ok() .map(|duration| (duration.as_secs(), duration.subsec_nanos())) } /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including /// "_for_user" is to discourage misuse - this method should not be used when making decisions /// about file dirtiness. pub fn timestamp_for_user(self) -> SystemTime { self.0 } /// Temporary method to split out the behavior changes from introduction of this newtype. pub fn bad_is_greater_than(self, other: MTime) -> bool { self.0 > other.0 }} impl From<proto::Timestamp> for MTime { fn from(timestamp: proto::Timestamp) -> Self { MTime(timestamp.into()) }} impl From<MTime> for proto::Timestamp { fn from(mtime: MTime) -> Self { mtime.0.into() }} #[derive(Default)]pub struct RealFs { git_hosting_provider_registry: Arc<GitHostingProviderRegistry>, git_binary_path: Option<PathBuf>,} pub trait FileHandle: Send + Sync + std::fmt::Debug { fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>;} impl FileHandle for std::fs::File { #[cfg(target_os = "macos")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize]; let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) }; if result == -1 { anyhow::bail!("fcntl returned -1".to_string()); } let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) }; let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { let fd = self.as_fd(); let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd()); let new_path = std::fs::read_link(fd_path)?; if new_path .file_name() .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)")) { anyhow::bail!("file was deleted") }; Ok(new_path) } #[cfg(target_os = "windows")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { anyhow::bail!("unimplemented") }} pub struct RealWatcher {} impl RealFs { pub fn new( git_hosting_provider_registry: Arc<GitHostingProviderRegistry>, git_binary_path: Option<PathBuf>, ) -> Self { Self { git_hosting_provider_registry, git_binary_path, } }} #[async_trait::async_trait]impl Fs for RealFs { async fn create_dir(&self, path: &Path) -> Result<()> { Ok(smol::fs::create_dir_all(path).await?) } async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> { #[cfg(unix)] smol::fs::unix::symlink(target, path).await?; #[cfg(windows)] if smol::fs::metadata(&target).await?.is_dir() { smol::fs::windows::symlink_dir(target, path).await? } else { smol::fs::windows::symlink_file(target, path).await? } Ok(()) } async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { let mut open_options = smol::fs::OpenOptions::new(); open_options.write(true).create(true); if options.overwrite { open_options.truncate(true); } else if !options.ignore_if_exists { open_options.create_new(true); } open_options.open(path).await?; Ok(()) } async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()> { let mut file = smol::fs::File::create(&path).await?; futures::io::copy(content, &mut file).await?; Ok(()) } async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()> { content.unpack(path).await?; Ok(()) } async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } } smol::fs::copy(source, target).await?; Ok(()) } async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } } smol::fs::rename(source, target).await?; Ok(()) } async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { let result = if options.recursive { smol::fs::remove_dir_all(path).await } else { smol::fs::remove_dir(path).await }; match result { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { Ok(()) } Err(err) => Err(err)?, } } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { #[cfg(windows)] if let Ok(Some(metadata)) = self.metadata(path).await { if metadata.is_symlink && metadata.is_dir { self.remove_dir( path, RemoveOptions { recursive: false, ignore_if_not_exists: true, }, ) .await?; return Ok(()); } } match smol::fs::remove_file(path).await { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { Ok(()) } Err(err) => Err(err)?, } } #[cfg(target_os = "macos")] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use cocoa::{ base::{id, nil}, foundation::{NSAutoreleasePool, NSString}, }; use objc::{class, msg_send, sel, sel_impl}; unsafe { unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())]; let array: id = msg_send![class!(NSArray), arrayWithObject: url]; let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil]; } Ok(()) } #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { let file = File::open(path)?; match trash::trash_file(&file.as_fd()).await { Ok(_) => Ok(()), Err(err) => Err(anyhow::Error::new(err)), } } #[cfg(target_os = "windows")] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFile}, }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` let path = SanitizedPath::from(path.canonicalize()?); let path_string = path.to_string(); let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?; file.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } #[cfg(target_os = "macos")] async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.trash_file(path, options).await } #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.trash_file(path, options).await } #[cfg(target_os = "windows")] async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFolder}, }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` let path = SanitizedPath::from(path.canonicalize()?); let path_string = path.to_string(); let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?; folder.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> { Ok(Box::new(std::fs::File::open(path)?)) } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {Lines 1–500 of 2168. Load more
#[cfg(target_os = "macos")] mod mac_watcher; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub mod linux_watcher; use anyhow::{anyhow, Result}; use git::GitHostingProviderRegistry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::fs::File; #[cfg(unix)] use std::os::fd::AsFd; #[cfg(unix)] use std::os::fd::AsRawFd; #[cfg(unix)] use std::os::unix::fs::MetadataExt; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use async_tar::Archive; use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt}; use git::repository::{GitRepository, RealGitRepository}; use gpui::{AppContext, Global, ReadGlobal}; use rope::Rope; use serde::{Deserialize, Serialize}; use smol::io::AsyncWriteExt; use std::{ io::{self, Write}, path::{Component, Path, PathBuf}, pin::Pin, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] use git::repository::{FakeGitRepositoryState, GitFileStatus}; #[cfg(any(test, feature = "test-support"))] use parking_lot::Mutex; #[cfg(any(test, feature = "test-support"))] use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>; } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum PathEventKind { Removed, Created, Changed, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct PathEvent { pub path: PathBuf, pub kind: Option<PathEventKind>, } impl From<PathEvent> for PathBuf { fn from(event: PathEvent) -> Self { event.path } } #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()>; async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()>; async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()>; async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()>; async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>; async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_dir(path, options).await } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.remove_file(path, options).await } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>; async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>; async fn load(&self, path: &Path) -> Result<String> { Ok(String::from_utf8(self.load_bytes(path).await?)?) } async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>; async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result<PathBuf>; async fn is_file(&self, path: &Path) -> bool; async fn is_dir(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>; async fn read_link(&self, path: &Path) -> Result<PathBuf>; async fn read_dir( &self, path: &Path, ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>; async fn watch( &self, path: &Path, latency: Duration, ) -> ( Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>, Arc<dyn Watcher>, ); fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result<bool>; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> Arc<FakeFs> { panic!("called as_fake on a real fs"); } } struct GlobalFs(Arc<dyn Fs>); impl Global for GlobalFs {} impl dyn Fs { /// Returns the global [`Fs`]. pub fn global(cx: &AppContext) -> Arc<Self> { GlobalFs::global(cx).0.clone() } /// Sets the global [`Fs`]. pub fn set_global(fs: Arc<Self>, cx: &mut AppContext) { cx.set_global(GlobalFs(fs)); } } #[derive(Copy, Clone, Default)] pub struct CreateOptions { pub overwrite: bool, pub ignore_if_exists: bool, } #[derive(Copy, Clone, Default)] pub struct CopyOptions { pub overwrite: bool, pub ignore_if_exists: bool, } #[derive(Copy, Clone, Default)] pub struct RenameOptions { pub overwrite: bool, pub ignore_if_exists: bool, } #[derive(Copy, Clone, Default)] pub struct RemoveOptions { pub recursive: bool, pub ignore_if_not_exists: bool, } #[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool, } /// Filesystem modification time. The purpose of this newtype is to discourage use of operations /// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using /// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it /// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113). /// /// Do not derive Ord, PartialOrd, or arithmetic operation traits. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] pub struct MTime(SystemTime); impl MTime { /// Conversion intended for persistence and testing. pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { MTime(UNIX_EPOCH + Duration::new(secs, nanos)) } /// Conversion intended for persistence. pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { self.0 .duration_since(UNIX_EPOCH) .ok() .map(|duration| (duration.as_secs(), duration.subsec_nanos())) } /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including /// "_for_user" is to discourage misuse - this method should not be used when making decisions /// about file dirtiness. pub fn timestamp_for_user(self) -> SystemTime { self.0 } /// Temporary method to split out the behavior changes from introduction of this newtype. pub fn bad_is_greater_than(self, other: MTime) -> bool { self.0 > other.0 } } impl From<proto::Timestamp> for MTime { fn from(timestamp: proto::Timestamp) -> Self { MTime(timestamp.into()) } } impl From<MTime> for proto::Timestamp { fn from(mtime: MTime) -> Self { mtime.0.into() } } #[derive(Default)] pub struct RealFs { git_hosting_provider_registry: Arc<GitHostingProviderRegistry>, git_binary_path: Option<PathBuf>, } pub trait FileHandle: Send + Sync + std::fmt::Debug { fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>; } impl FileHandle for std::fs::File { #[cfg(target_os = "macos")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { use std::{ ffi::{CStr, OsStr}, os::unix::ffi::OsStrExt, }; let fd = self.as_fd(); let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize]; let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) }; if result == -1 { anyhow::bail!("fcntl returned -1".to_string()); } let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) }; let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes())); Ok(path) } #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { let fd = self.as_fd(); let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd()); let new_path = std::fs::read_link(fd_path)?; if new_path .file_name() .is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)")) { anyhow::bail!("file was deleted") }; Ok(new_path) } #[cfg(target_os = "windows")] fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> { anyhow::bail!("unimplemented") } } pub struct RealWatcher {} impl RealFs { pub fn new( git_hosting_provider_registry: Arc<GitHostingProviderRegistry>, git_binary_path: Option<PathBuf>, ) -> Self { Self { git_hosting_provider_registry, git_binary_path, } } } #[async_trait::async_trait] impl Fs for RealFs { async fn create_dir(&self, path: &Path) -> Result<()> { Ok(smol::fs::create_dir_all(path).await?) } async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> { #[cfg(unix)] smol::fs::unix::symlink(target, path).await?; #[cfg(windows)] if smol::fs::metadata(&target).await?.is_dir() { smol::fs::windows::symlink_dir(target, path).await? } else { smol::fs::windows::symlink_file(target, path).await? } Ok(()) } async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { let mut open_options = smol::fs::OpenOptions::new(); open_options.write(true).create(true); if options.overwrite { open_options.truncate(true); } else if !options.ignore_if_exists { open_options.create_new(true); } open_options.open(path).await?; Ok(()) } async fn create_file_with( &self, path: &Path, content: Pin<&mut (dyn AsyncRead + Send)>, ) -> Result<()> { let mut file = smol::fs::File::create(&path).await?; futures::io::copy(content, &mut file).await?; Ok(()) } async fn extract_tar_file( &self, path: &Path, content: Archive<Pin<&mut (dyn AsyncRead + Send)>>, ) -> Result<()> { content.unpack(path).await?; Ok(()) } async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } } smol::fs::copy(source, target).await?; Ok(()) } async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()> { if !options.overwrite && smol::fs::metadata(target).await.is_ok() { if options.ignore_if_exists { return Ok(()); } else { return Err(anyhow!("{target:?} already exists")); } } smol::fs::rename(source, target).await?; Ok(()) } async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { let result = if options.recursive { smol::fs::remove_dir_all(path).await } else { smol::fs::remove_dir(path).await }; match result { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { Ok(()) } Err(err) => Err(err)?, } } async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> { #[cfg(windows)] if let Ok(Some(metadata)) = self.metadata(path).await { if metadata.is_symlink && metadata.is_dir { self.remove_dir( path, RemoveOptions { recursive: false, ignore_if_not_exists: true, }, ) .await?; return Ok(()); } } match smol::fs::remove_file(path).await { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound && options.ignore_if_not_exists => { Ok(()) } Err(err) => Err(err)?, } } #[cfg(target_os = "macos")] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use cocoa::{ base::{id, nil}, foundation::{NSAutoreleasePool, NSString}, }; use objc::{class, msg_send, sel, sel_impl}; unsafe { unsafe fn ns_string(string: &str) -> id { NSString::alloc(nil).init_str(string).autorelease() } let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())]; let array: id = msg_send![class!(NSArray), arrayWithObject: url]; let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil]; } Ok(()) } #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { let file = File::open(path)?; match trash::trash_file(&file.as_fd()).await { Ok(_) => Ok(()), Err(err) => Err(anyhow::Error::new(err)), } } #[cfg(target_os = "windows")] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFile}, }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` let path = SanitizedPath::from(path.canonicalize()?); let path_string = path.to_string(); let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?; file.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } #[cfg(target_os = "macos")] async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.trash_file(path, options).await } #[cfg(any(target_os = "linux", target_os = "freebsd"))] async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> { self.trash_file(path, options).await } #[cfg(target_os = "windows")] async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> { use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFolder}, }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` let path = SanitizedPath::from(path.canonicalize()?); let path_string = path.to_string(); let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?; folder.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> { Ok(Box::new(std::fs::File::open(path)?)) } async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {