use std::{
fs, io,
path::{Path, PathBuf},
};
use cfg_if::cfg_if;
#[cfg(feature = "yarn_pnp")]
use pnp::fs::{LruZipCache, VPath, VPathInfo, ZipCache};
#[async_trait::async_trait]
pub trait FileSystem {
async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
async fn read_to_string(&self, path: &Path) -> io::Result<String>;
async fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
}
#[derive(Debug, Clone, Copy)]
pub struct FileMetadata {
pub is_file: bool,
pub is_dir: bool,
pub is_symlink: bool,
}
impl FileMetadata {
pub fn new(is_file: bool, is_dir: bool, is_symlink: bool) -> Self {
Self {
is_file,
is_dir,
is_symlink,
}
}
}
#[cfg(feature = "yarn_pnp")]
impl From<pnp::fs::FileType> for FileMetadata {
fn from(value: pnp::fs::FileType) -> Self {
Self::new(
value == pnp::fs::FileType::File,
value == pnp::fs::FileType::Directory,
false,
)
}
}
impl From<fs::Metadata> for FileMetadata {
fn from(metadata: fs::Metadata) -> Self {
Self::new(metadata.is_file(), metadata.is_dir(), metadata.is_symlink())
}
}
pub struct FileSystemOptions {
#[cfg(feature = "yarn_pnp")]
pub enable_pnp: bool,
}
impl Default for FileSystemOptions {
fn default() -> Self {
Self {
#[cfg(feature = "yarn_pnp")]
enable_pnp: true,
}
}
}
pub struct FileSystemOs {
options: FileSystemOptions,
#[cfg(feature = "yarn_pnp")]
pnp_lru: LruZipCache<Vec<u8>>,
}
impl Default for FileSystemOs {
fn default() -> Self {
Self {
options: FileSystemOptions::default(),
#[cfg(feature = "yarn_pnp")]
pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p),
}
}
}
impl FileSystemOs {
pub fn new(options: FileSystemOptions) -> Self {
Self {
options,
#[cfg(feature = "yarn_pnp")]
pnp_lru: LruZipCache::new(50, pnp::fs::open_zip_via_read_p),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait::async_trait]
impl FileSystem for FileSystemOs {
async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.options.enable_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => self.pnp_lru.read(info.physical_base_path(), info.zip_path),
VPath::Virtual(info) => tokio::fs::read(info.physical_base_path()).await,
VPath::Native(path) => tokio::fs::read(&path).await,
}
}
}}
tokio::fs::read(path).await
}
async fn read_to_string(&self, path: &Path) -> io::Result<String> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.options.enable_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => self.pnp_lru.read_to_string(info.physical_base_path(), info.zip_path),
VPath::Virtual(info) => tokio::fs::read_to_string(info.physical_base_path()).await,
VPath::Native(path) => tokio::fs::read_to_string(&path).await,
}
}
}
}
tokio::fs::read_to_string(path).await
}
async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.options.enable_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => self
.pnp_lru
.file_type(info.physical_base_path(), info.zip_path)
.map(FileMetadata::from),
VPath::Virtual(info) => {
tokio::fs::metadata(info.physical_base_path())
.await
.map(FileMetadata::from)
}
VPath::Native(path) => tokio::fs::metadata(path).await.map(FileMetadata::from),
}
}
}
}
tokio::fs::metadata(path).await.map(FileMetadata::from)
}
async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
tokio::fs::symlink_metadata(path)
.await
.map(FileMetadata::from)
}
async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
cfg_if! {
if #[cfg(feature = "yarn_pnp")] {
if self.options.enable_pnp {
return match VPath::from(path)? {
VPath::Zip(info) => {
dunce::canonicalize(info.physical_base_path().join(info.zip_path))
}
VPath::Virtual(info) => dunce::canonicalize(info.physical_base_path()),
VPath::Native(path) => dunce::canonicalize(path),
}
}
}
}
dunce::canonicalize(path)
}
}
#[cfg(target_arch = "wasm32")]
#[async_trait::async_trait]
impl FileSystem for FileSystemOs {
async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
std::fs::read(path)
}
async fn read_to_string(&self, path: &Path) -> io::Result<String> {
std::fs::read_to_string(path)
}
async fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
if let Ok(m) = std::fs::metadata(path).map(FileMetadata::from) {
return Ok(m);
}
self.symlink_metadata(path).await?;
let path = self.canonicalize(path).await?;
std::fs::metadata(path).map(FileMetadata::from)
}
async fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
std::fs::symlink_metadata(path).map(FileMetadata::from)
}
async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
use std::path::Component;
let mut path_buf = path.to_path_buf();
let link = fs::read_link(&path_buf)?;
path_buf.pop();
for component in link.components() {
match component {
Component::ParentDir => {
path_buf.pop();
}
Component::Normal(seg) => {
path_buf.push(seg.to_string_lossy().trim_end_matches('\0'));
}
Component::RootDir => {
path_buf = PathBuf::from("/");
}
Component::CurDir | Component::Prefix(_) => {}
}
if fs::symlink_metadata(&path_buf).is_ok_and(|m| m.is_symlink()) {
let dir = self.canonicalize(&path_buf).await?;
path_buf = dir;
}
}
Ok(path_buf)
}
}
#[tokio::test]
async fn metadata() {
let meta = FileMetadata {
is_file: true,
is_dir: true,
is_symlink: true,
};
assert_eq!(
format!("{meta:?}"),
"FileMetadata { is_file: true, is_dir: true, is_symlink: true }"
);
let _ = meta;
}