rong_fs 0.3.1

Filesystem module for RongJS
use crate::file::{FileHandle, FileOpenOption, open_file_internal};
use crate::grant_file_access;
use crate::sink::{FileSink, FileSinkOptions};
use crate::stat::FileInfo;
use bytes::{Bytes, BytesMut};
use rong::{function::Optional, *};
use rong_stream::JSReadableStream;
use std::path::PathBuf;
use tokio::sync::mpsc;

#[js_export]
pub(crate) struct RongFile {
    path: String,
    resolved: PathBuf,
}

impl RongFile {
    pub(crate) fn resolved(&self) -> &PathBuf {
        &self.resolved
    }
}

#[js_class]
impl RongFile {
    #[js_method(constructor)]
    fn new() -> JSResult<Self> {
        rong::illegal_constructor("Not Allowed 'new RongFile()', use Rong.file(path)")
    }

    #[js_method(getter)]
    fn name(&self) -> String {
        self.path.clone()
    }

    #[js_method]
    async fn text(&self) -> JSResult<String> {
        tokio::fs::read_to_string(&self.resolved)
            .await
            .map_err(|e| HostError::new("FS_IO", e.to_string()).into())
    }

    #[js_method]
    async fn json(&self, ctx: JSContext) -> JSResult<JSValue> {
        let text = tokio::fs::read_to_string(&self.resolved)
            .await
            .map_err(|e| HostError::new("FS_IO", e.to_string()))?;

        text.as_str().json_to_js_value(&ctx)
    }

    #[js_method]
    async fn bytes(&self, ctx: JSContext) -> JSResult<JSTypedArray> {
        let data = tokio::fs::read(&self.resolved)
            .await
            .map_err(|e| HostError::new("FS_IO", format!("Failed to read file: {}", e)))?;

        let len = data.len();
        let ab = JSArrayBuffer::from_bytes_owned(&ctx, data)?;
        JSTypedArray::from_array_buffer(&ctx, ab, 0, Some(len))
    }

    #[js_method(rename = "arrayBuffer")]
    async fn array_buffer(&self, ctx: JSContext) -> JSResult<JSArrayBuffer> {
        let data = tokio::fs::read(&self.resolved)
            .await
            .map_err(|e| HostError::new("FS_IO", format!("Failed to read file: {}", e)))?;

        JSArrayBuffer::from_bytes_owned(&ctx, data)
    }

    #[js_method]
    fn stream(&self, ctx: JSContext) -> Option<JSObject> {
        let resolved = self.resolved.clone();
        let (tx, rx) = mpsc::channel::<Result<Bytes, String>>(16);
        let chunk_size = 64 * 1024;

        tokio::task::spawn(async move {
            let file = match tokio::fs::File::open(&resolved).await {
                Ok(f) => f,
                Err(e) => {
                    let _ = tx.send(Err(e.to_string())).await;
                    return;
                }
            };
            let mut reader = tokio::io::BufReader::new(file);
            let mut buf = BytesMut::with_capacity(chunk_size);
            loop {
                buf.clear();
                match tokio::io::AsyncReadExt::read_buf(&mut reader, &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]
    async fn exists(&self) -> bool {
        tokio::fs::metadata(&self.resolved).await.is_ok()
    }

    #[js_method]
    async fn delete(&self) -> JSResult<()> {
        tokio::fs::remove_file(&self.resolved)
            .await
            .map_err(|e| HostError::new("FS_IO", format!("Failed to delete file: {}", e)).into())
    }

    #[js_method]
    async fn stat(&self) -> JSResult<FileInfo> {
        tokio::fs::metadata(&self.resolved)
            .await
            .map(FileInfo::from_metadata)
            .map_err(|e| HostError::new("FS_IO", format!("Failed to get file info: {}", e)).into())
    }

    #[js_method]
    async fn lstat(&self) -> JSResult<FileInfo> {
        tokio::fs::symlink_metadata(&self.resolved)
            .await
            .map(FileInfo::from_metadata)
            .map_err(|e| HostError::new("FS_IO", format!("Failed to get file info: {}", e)).into())
    }

    #[js_method]
    async fn open(&self, option: Optional<FileOpenOption>) -> JSResult<FileHandle> {
        open_file_internal(&self.resolved, &self.path, option.0).await
    }

    #[js_method]
    async fn writer(&self, option: Optional<FileSinkOptions>) -> JSResult<FileSink> {
        FileSink::create(&self.resolved, &self.path, option.0).await
    }

    #[js_method(gc_mark)]
    fn gc_mark_with<F>(&self, _mark_fn: F)
    where
        F: FnMut(&JSValue),
    {
    }
}

fn file(path: String) -> JSResult<RongFile> {
    let resolved = grant_file_access(&path)?;
    Ok(RongFile { path, resolved })
}

pub(crate) fn init(ctx: &JSContext) -> JSResult<()> {
    let rong = ctx.host_namespace();

    ctx.register_hidden_class::<RongFile>()?;

    let file_fn = JSFunc::new(ctx, file)?.name("file")?;
    rong.set("file", file_fn)?;

    Ok(())
}