use crate::grant_file_access;
use futures::Stream;
use rong::{function::Optional, *};
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::fs;
#[js_export]
pub struct DirEntry {
name: String,
file_type: bool,
is_symlink: bool,
}
#[js_class]
impl DirEntry {
#[js_method(constructor)]
fn new(_name: String, _file_type: bool, _is_symlink: bool) -> JSResult<Self> {
rong::illegal_constructor("DirEntry cannot be constructed directly. Use Rong.readDir().")
}
#[js_method(getter)]
fn name(&self) -> String {
self.name.clone()
}
#[js_method(getter, rename = "isFile")]
fn is_file(&self) -> bool {
!self.file_type
}
#[js_method(getter, rename = "isDirectory")]
fn is_dir(&self) -> bool {
self.file_type
}
#[js_method(getter, rename = "isSymlink")]
fn is_symlink(&self) -> bool {
self.is_symlink
}
#[js_method(gc_mark)]
fn gc_mark_with<F>(&self, _mark_fn: F)
where
F: FnMut(&JSValue),
{
}
}
#[derive(FromJSObj, Default)]
struct MkdirOptions {
recursive: Option<bool>,
#[cfg(unix)]
mode: Option<u32>,
}
async fn mkdir(path: String, option: Optional<MkdirOptions>) -> JSResult<()> {
let resolved = grant_file_access(&path)?;
let options = option.0.unwrap_or_default();
if let Ok(metadata) = fs::metadata(&resolved).await
&& metadata.is_dir()
{
return Ok(()); }
let result = if options.recursive.unwrap_or(false) {
fs::create_dir_all(&resolved).await
} else {
fs::create_dir(&resolved).await
};
result.map_err(|e| HostError::new("FS_IO", format!("Failed to create directory: {}", e)))?;
#[cfg(unix)]
if let Some(mode) = options.mode {
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(mode);
tokio::fs::set_permissions(&resolved, permissions)
.await
.map_err(|e| {
HostError::new(
"FS_IO",
format!("Failed to set directory permissions: {}", e),
)
})?;
}
Ok(())
}
type FileTypeFuture =
Pin<Box<dyn futures::Future<Output = Result<std::fs::FileType, std::io::Error>> + Send>>;
pub struct DirEntryStream {
entries: fs::ReadDir,
current_entry: Option<fs::DirEntry>,
current_file_type_fut: Option<FileTypeFuture>,
}
impl DirEntryStream {
pub fn new(entries: fs::ReadDir) -> Self {
Self {
entries,
current_entry: None,
current_file_type_fut: None,
}
}
}
impl Stream for DirEntryStream {
type Item = Result<DirEntry, RongJSError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(file_type_fut) = this.current_file_type_fut.as_mut() {
match file_type_fut.as_mut().poll(cx) {
Poll::Ready(Ok(file_type)) => {
this.current_file_type_fut.take();
if let Some(entry) = this.current_entry.take() {
let name = entry.file_name().to_string_lossy().to_string();
return Poll::Ready(Some(Ok(DirEntry {
name,
file_type: file_type.is_dir(),
is_symlink: file_type.is_symlink(),
})));
}
}
Poll::Ready(Err(e)) => {
this.current_file_type_fut.take();
this.current_entry.take();
return Poll::Ready(Some(Err(HostError::new(
"FS_IO",
format!("Failed to get file type: {}", e),
)
.into())));
}
Poll::Pending => {
return Poll::Pending;
}
}
}
match this.entries.poll_next_entry(cx) {
Poll::Ready(Ok(Some(entry))) => {
let path = entry.path();
let file_type_fut = Box::pin(async move {
let metadata = fs::symlink_metadata(&path).await?;
Ok(metadata.file_type())
});
this.current_entry = Some(entry);
this.current_file_type_fut = Some(file_type_fut);
cx.waker().wake_by_ref(); Poll::Pending
}
Poll::Ready(Ok(None)) => Poll::Ready(None),
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(HostError::new(
"FS_IO",
format!("Failed to read directory entry: {}", e),
)
.into()))),
Poll::Pending => Poll::Pending,
}
}
}
async fn readdir(ctx: JSContext, path: String) -> JSResult<JSObject> {
let resolved = grant_file_access(&path)?;
let entries = fs::read_dir(&resolved)
.await
.map_err(|e| HostError::new("FS_IO", format!("Failed to read directory: {}", e)))?;
let stream = DirEntryStream::new(entries);
stream.to_js_async_iter(&ctx)
}
#[derive(FromJSObj, Default)]
struct RemoveOptions {
recursive: bool,
}
async fn remove(path: String, option: Optional<RemoveOptions>) -> JSResult<()> {
let resolved = grant_file_access(&path)?;
let options = option.0.unwrap_or_default();
match fs::metadata(&resolved).await {
Ok(metadata) => {
if metadata.is_file() || metadata.is_symlink() {
fs::remove_file(&resolved).await.map_err(|e| {
HostError::new("FS_IO", format!("Failed to remove file: {}", e)).into()
})
} else if metadata.is_dir() {
if options.recursive {
fs::remove_dir_all(&resolved).await.map_err(|e| {
HostError::new(
"FS_IO",
format!("Failed to remove directory recursively: {}", e),
)
.into()
})
} else {
fs::remove_dir(&resolved).await.map_err(|e| {
HostError::new("FS_IO", format!("Failed to remove directory: {}", e)).into()
})
}
} else {
Err(HostError::new(rong::error::E_INTERNAL, "Unknown file type").into())
}
}
Err(e) => Err(HostError::new("FS_IO", format!("Failed to access path: {}", e)).into()),
}
}
async fn chdir(directory: String) -> JSResult<()> {
let resolved = grant_file_access(&directory)?;
std::env::set_current_dir(&resolved)
.map_err(|e| HostError::new("FS_IO", format!("Failed to change directory: {}", e)).into())
}
pub(crate) fn init(ctx: &JSContext) -> JSResult<()> {
let rong = ctx.host_namespace();
ctx.register_hidden_class::<DirEntry>()?;
let mkdir_fn = JSFunc::new(ctx, mkdir)?.name("mkdir")?;
rong.set("mkdir", mkdir_fn)?;
let remove_fn = JSFunc::new(ctx, remove)?.name("remove")?;
rong.set("remove", remove_fn)?;
let readdir_fn = JSFunc::new(ctx, readdir)?.name("readDir")?;
rong.set("readDir", readdir_fn)?;
let chdir_fn = JSFunc::new(ctx, chdir)?.name("chdir")?;
rong.set("chdir", chdir_fn)?;
Ok(())
}