use crate::stat::FileInfo;
use bytes::{Bytes, BytesMut};
use rong::{function::Optional, *};
use rong_stream::{JSReadableStream, WritableStream};
use std::path::Path;
use std::sync::Arc;
use tokio::fs::{File, OpenOptions};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom};
use tokio::sync::Mutex;
use tokio::sync::mpsc;
#[derive(FromJSObj)]
pub(crate) struct FileOpenOption {
pub(crate) read: Option<bool>,
pub(crate) write: Option<bool>,
pub(crate) append: Option<bool>,
pub(crate) truncate: Option<bool>,
pub(crate) create: Option<bool>,
#[rename = "createNew"]
pub(crate) create_new: Option<bool>,
pub(crate) mode: Option<u32>,
}
#[js_export]
pub(crate) struct FileHandle {
file: Arc<Mutex<Option<File>>>,
}
#[js_class]
impl FileHandle {
#[js_method(constructor)]
fn new() -> JSResult<Self> {
rong::illegal_constructor("Not Allowed 'new FileHandle()', use Rong.file(path).open()")
}
#[js_method]
async fn stat(&self) -> JSResult<FileInfo> {
let file = self.file.lock().await;
file.as_ref()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?
.metadata()
.await
.map(FileInfo::from_metadata)
.map_err(|e| HostError::new("FS_IO", format!("Failed to get file stats: {}", e)).into())
}
#[js_method]
async fn read(&self, buffer: JSArrayBuffer) -> JSResult<Option<usize>> {
let buf_len = buffer.len();
if buf_len == 0 {
return Ok(Some(0));
}
let mut buffer = buffer;
let buffer_slice = buffer.as_mut_slice();
let mut file = self.file.lock().await;
let file = file
.as_mut()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?;
match file.read(buffer_slice).await {
Ok(0) => Ok(None),
Ok(bytes_read) => Ok(Some(bytes_read)),
Err(e) => Err(HostError::new("FS_IO", format!("Failed to read file: {}", e)).into()),
}
}
#[js_method]
async fn write(&self, buffer: JSArrayBuffer) -> JSResult<usize> {
let buf = buffer.as_slice();
let mut file = self.file.lock().await;
let file = file
.as_mut()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?;
file.write_all(buf)
.await
.map_err(|e| HostError::new("FS_IO", format!("Failed to write file: {}", e)))?;
Ok(buf.len())
}
#[js_method]
async fn sync(&self) -> JSResult<()> {
let file = self.file.lock().await;
file.as_ref()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?
.sync_all()
.await
.map_err(|e| HostError::new("FS_IO", format!("Failed to sync file: {}", e)).into())
}
#[js_method]
async fn truncate(&self, len: Optional<u64>) -> JSResult<()> {
let length = len.0.unwrap_or(0);
let file = self.file.lock().await;
file.as_ref()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?
.set_len(length)
.await
.map_err(|e| HostError::new("FS_IO", format!("Failed to truncate file: {}", e)).into())
}
#[js_method]
async fn seek(&self, offset: i64, whence: Optional<u32>) -> JSResult<u64> {
let whence_mode = whence.0.unwrap_or(0);
let seek_from = match whence_mode {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => {
return Err(HostError::new(
rong::error::E_INVALID_ARG,
format!(
"Invalid whence value: {}. Must be 0 (Start), 1 (Current), or 2 (End)",
whence_mode
),
)
.with_name("TypeError")
.into());
}
};
let mut file = self.file.lock().await;
let file = file
.as_mut()
.ok_or_else(|| HostError::new(rong::error::E_INVALID_STATE, "FileHandle is closed"))?;
let new_position = file
.seek(seek_from)
.await
.map_err(|e| HostError::new("FS_IO", format!("Failed to seek: {}", e)))?;
Ok(new_position)
}
#[js_method]
async fn close(&self) -> JSResult<()> {
let mut file = self.file.lock().await;
file.take();
Ok(())
}
#[js_method(getter)]
fn readable(&self, ctx: JSContext) -> Option<JSObject> {
let file = self.file.clone();
let (tx, rx) = mpsc::channel::<Result<Bytes, String>>(16);
let chunk_size = 64 * 1024;
tokio::task::spawn(async move {
let mut buf = BytesMut::with_capacity(chunk_size);
let mut guard = file.lock().await;
let Some(f) = guard.as_mut() else {
let _ = tx.send(Err("FileHandle is closed".to_string())).await;
return;
};
loop {
buf.clear();
match tokio::io::AsyncReadExt::read_buf(f, &mut buf).await {
Ok(0) => break,
Ok(_) => {
if tx.send(Ok(buf.split().freeze())).await.is_err() {
break;
}
}
Err(e) => {
let _ = tx.send(Err(e.to_string())).await;
break;
}
}
}
});
JSReadableStream::from_receiver(&ctx, rx)
.map(|jsrs| jsrs.into_object())
.ok()
}
#[js_method(getter)]
fn writable(&self) -> JSResult<WritableStream> {
let file = self.file.clone();
let (tx, mut rx) = mpsc::channel::<Bytes>(128);
let (done_tx, done_rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
tokio::task::spawn(async move {
let mut guard = file.lock().await;
let Some(f) = guard.as_mut() else {
let _ = done_tx.send(Err("FileHandle is closed".to_string()));
return;
};
let mut error: Option<String> = None;
while let Some(chunk) = rx.recv().await {
if let Err(e) = f.write_all(&chunk).await {
error = Some(e.to_string());
break;
}
}
if let Err(e) = f.flush().await
&& error.is_none()
{
error = Some(e.to_string());
}
let _ = done_tx.send(match error {
Some(e) => Err(e),
None => Ok(()),
});
});
Ok(rong_stream::writable_stream_to_sender_with_done(
tx, done_rx,
))
}
#[js_method(gc_mark)]
fn gc_mark_with<F>(&self, _mark_fn: F)
where
F: FnMut(&JSValue),
{
}
}
pub(crate) async fn open_file_internal(
resolved: &Path,
display_path: &str,
option: Option<FileOpenOption>,
) -> JSResult<FileHandle> {
let opts = option.unwrap_or(FileOpenOption {
read: None,
write: None,
append: None,
truncate: None,
create: None,
create_new: None,
mode: None,
});
let read = opts.read.unwrap_or(true);
let write = opts.write.unwrap_or(false);
let append = opts.append.unwrap_or(false);
let truncate = opts.truncate.unwrap_or(false);
let create = opts.create.unwrap_or(false);
let create_new = opts.create_new.unwrap_or(false);
let mode = opts.mode;
let file_handle = if cfg!(unix) && mode.is_some() {
#[cfg(unix)]
{
let resolved = resolved.to_path_buf();
let display_path = display_path.to_string();
let mode = mode.unwrap_or(0);
let handle = tokio::task::spawn_blocking(move || {
let mut open_options = std::fs::OpenOptions::new();
open_options
.read(read)
.write(write)
.append(append)
.truncate(truncate)
.create(create)
.create_new(create_new);
std::os::unix::fs::OpenOptionsExt::mode(&mut open_options, mode);
open_options.open(&resolved)
})
.await
.map_err(|e| {
HostError::new(
"FS_IO",
format!("Failed to open file '{}': {}", display_path, e),
)
})?
.map_err(|e| {
HostError::new(
"FS_IO",
format!("Failed to open file '{}': {}", display_path, e),
)
})?;
File::from_std(handle)
}
#[cfg(not(unix))]
{
unreachable!("mode should be ignored on non-unix platforms");
}
} else {
let mut open_options = OpenOptions::new();
open_options
.read(read)
.write(write)
.append(append)
.truncate(truncate)
.create(create)
.create_new(create_new);
open_options.open(resolved).await.map_err(|e| {
HostError::new(
"FS_IO",
format!("Failed to open file '{}': {}", display_path, e),
)
})?
};
Ok(FileHandle {
file: Arc::new(Mutex::new(Some(file_handle))),
})
}
pub(crate) fn init(ctx: &JSContext) -> JSResult<()> {
let rong = ctx.host_namespace();
ctx.register_hidden_class::<FileHandle>()?;
let seek_mode = JSObject::new(ctx);
seek_mode.set("Start", 0u32)?;
seek_mode.set("Current", 1u32)?;
seek_mode.set("End", 2u32)?;
rong.set("SeekMode", seek_mode)?;
Ok(())
}