use std::{collections::HashMap, fs::File, io::Read, path::Path}; use napi::{bindgen_prelude::*, sys::napi_value__, tokio_stream::StreamExt}; use tokio_util::codec::{BytesCodec, FramedRead}; use crate::version::{ backends::{PathVersionBackend, ZipVersionBackend}, types::{ReadToAsyncRead, VersionBackend, VersionFile}, }; /** * Append new backends here */ pub fn create_backend_constructor<'a>( path: &Path, ) -> Option Box>> { if !path.exists() { return None; } let is_directory = path.is_dir(); if is_directory { let base_dir = path.to_path_buf(); return Some(Box::new(move || Box::new(PathVersionBackend { base_dir }))); }; if path.to_string_lossy().ends_with(".zip") { let f = File::open(path.to_path_buf()).unwrap(); return Some(Box::new(|| Box::new(ZipVersionBackend::new(f)))); } None } /** * Persistent object so we can cache things between commands */ #[napi(js_name = "DropletHandler")] pub struct DropletHandler<'a> { backend_cache: HashMap>, } #[napi] impl<'a> DropletHandler<'a> { #[napi(constructor)] pub fn new() -> Self { DropletHandler { backend_cache: HashMap::new(), } } pub fn create_backend_for_path( &mut self, path: String, ) -> Option<&mut Box> { let fs_path = Path::new(&path); let constructor = create_backend_constructor(fs_path)?; let existing_backend = self.backend_cache.entry(path).or_insert_with(|| { let backend = constructor(); backend }); Some(existing_backend) } #[napi] pub fn has_backend_for_path(&self, path: String) -> bool { let path = Path::new(&path); let has_backend = create_backend_constructor(path).is_some(); has_backend } #[napi] pub fn list_files(&mut self, path: String) -> Result> { let backend = self .create_backend_for_path(path) .ok_or(napi::Error::from_reason("No backend for path"))?; let files = backend.list_files(); Ok(files.into_iter().map(|e| e.relative_filename).collect()) } #[napi] pub fn peek_file(&mut self, path: String, sub_path: String) -> Result { let backend = self .create_backend_for_path(path) .ok_or(napi::Error::from_reason("No backend for path"))?; let file = backend .peek_file(sub_path) .ok_or(napi::Error::from_reason("Can't find file to peek"))?; return Ok(file.size.try_into().unwrap()); } #[napi] pub fn read_file( &mut self, reference: Reference>, path: String, sub_path: String, env: Env, start: Option, end: Option, ) -> Result { let stream = reference.share_with(env, |handler| { let backend = handler .create_backend_for_path(path) .ok_or(napi::Error::from_reason("Failed to create backend."))?; let version_file = VersionFile { relative_filename: sub_path, permission: 0, // Shouldn't matter size: 0, // Shouldn't matter }; // Use `?` operator for cleaner error propagation from `Option` let mut reader = backend.reader(&version_file).ok_or(napi::Error::from_reason("Failed to create reader."))?; if let Some(skip) = start.clone() { reader.skip(skip.get_u64().1.into()); // io::copy(&mut reader.by_ref().take(skip.into()), &mut io::sink()).unwrap(); } let async_reader = if let Some(limit) = end { let amount = limit.get_u64().1 - start.map_or(Some(0), |v| Some(v.get_u64().1)).unwrap(); ReadToAsyncRead { inner: Box::new(reader.take(amount.into())), } } else { ReadToAsyncRead { inner: reader } }; // Create a FramedRead stream with BytesCodec for chunking let stream = FramedRead::new(async_reader, BytesCodec::new()) // Use StreamExt::map to transform each Result item .map(|result_item| { result_item // Apply Result::map to transform Ok(BytesMut) to Ok(Vec) .map(|bytes| bytes.to_vec()) // Apply Result::map_err to transform Err(std::io::Error) to Err(napi::Error) .map_err(|e| napi::Error::from(e)) // napi::Error implements From }); // Create the napi-rs ReadableStream from the tokio_stream::Stream // The unwrap() here means if stream creation fails, it will panic. // For a production system, consider returning Result> and handling this. Ok(ReadableStream::create_with_stream_bytes(&env, stream).unwrap()) })?; Ok(JsDropStreamable { inner: stream, }) } } #[napi] pub struct JsDropStreamable { inner: SharedReference, ReadableStream<'static, BufferSlice<'static>>>, } #[napi] impl JsDropStreamable { #[napi] pub fn get_stream(&self) -> *mut napi_value__ { self.inner.raw() } }