From 4aa23c55964d9fc38001e34f2aa4d2f4e9c708ad Mon Sep 17 00:00:00 2001 From: Spxg Date: Sun, 18 May 2025 14:30:24 +0800 Subject: [PATCH] Add import and load db support --- src/app/header.rs | 116 +++++++++++++++++++++++++++++++++++++- src/app/output/section.rs | 2 +- src/app/output/status.rs | 31 +++++++++- src/app/playground.rs | 9 +++ src/app/state.rs | 9 +++ src/bin/worker.rs | 5 +- src/lib.rs | 34 +++++++++-- src/worker/mod.rs | 74 +++++++++++++++++------- 8 files changed, 247 insertions(+), 33 deletions(-) diff --git a/src/app/header.rs b/src/app/header.rs index 027c869..c9f07e0 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -1,11 +1,13 @@ use istyles::istyles; -use leptos::{prelude::*, tachys::html}; +use leptos::{html::Input, prelude::*, tachys::html}; use reactive_stores::Store; -use web_sys::{Url, UrlSearchParams}; +use wasm_bindgen::{JsCast, prelude::Closure}; +use web_sys::{Event, FileReader, HtmlInputElement, Url, UrlSearchParams}; use crate::{ - PrepareOptions, WorkerRequest, + FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, WorkerRequest, app::{ + ImportProgress, Vfs, advanced_options_menu::AdvancedOptionsMenu, button_set::{Button, ButtonSet, IconButton, Rule}, config_menu::ConfigMenu, @@ -24,6 +26,8 @@ istyles!(styles, "assets/module.postcss/header.module.css.map"); pub fn Header() -> impl IntoView { let menu_container = NodeRef::new(); + let input_ref = NodeRef::new(); + view! { <>
+ + + + @@ -99,6 +107,108 @@ fn ExecuteButton() -> impl IntoView { } } +#[component] +fn LoadButton(input_ref: NodeRef) -> impl IntoView { + let state = expect_context::>(); + + let (file, set_file) = signal::>>(None); + + Effect::new(move || { + if let Some(file) = &*file.read() { + let filename = file.name(); + + if let Ok(reader) = FileReader::new() { + let on_progress = FragileComfirmed::new(Closure::wrap(Box::new( + move |ev: web_sys::ProgressEvent| { + if ev.length_computable() { + state.import_progress().set(Some(ImportProgress { + filename: filename.clone(), + loaded: ev.loaded(), + total: ev.total(), + })); + } + }, + ) + as Box)); + + let on_error = FragileComfirmed::new(Closure::wrap(Box::new( + move |ev: web_sys::ProgressEvent| { + if let Some(target) = ev.target() { + let reader = target.unchecked_into::(); + if let Some(dom_error) = reader.error() { + state.last_error().set(Some(FragileComfirmed::new( + SQLightError::ImportDb(dom_error.message().to_string()), + ))); + } + } + }, + ) + as Box)); + + let on_load = + FragileComfirmed::new(Closure::wrap(Box::new(move |ev: web_sys::Event| { + if let Some(target) = ev.target() { + let reader = target.unchecked_into::(); + if let Ok(result) = reader.result() { + let array_buffer = result.unchecked_into::(); + let data = js_sys::Uint8Array::new(&array_buffer); + let persist = state.vfs().get() != Vfs::Memory; + if let Some(worker) = &*state.worker().read() { + worker.send_task(WorkerRequest::LoadDb(LoadDbOptions { + // FIXME: multi db + id: String::new(), + persist, + data, + })); + } + } + } + }) + as Box)); + + reader.set_onprogress(Some(on_progress.as_ref().unchecked_ref())); + reader.set_onerror(Some(on_error.as_ref().unchecked_ref())); + reader.set_onload(Some(on_load.as_ref().unchecked_ref())); + reader.read_as_array_buffer(file).unwrap(); + + on_cleanup(move || { + drop(on_progress); + drop(on_error); + drop(on_load); + }); + } + } + }); + + let on_change = move |ev: Event| { + if let Some(target) = ev.target() { + let input = target.unchecked_into::(); + if let Some(files) = input.files() { + if files.length() > 0 { + let file = FragileComfirmed::new(files.get(0).unwrap()); + set_file.set(Some(file)); + } else { + set_file.set(None); + } + } + } + }; + + let on_click = move |_| { + if let Some(input) = &*input_ref.read() { + if input.onchange().is_none() { + let callback = Closure::wrap(Box::new(on_change) as Box); + input.set_onchange(Some(callback.as_ref().unchecked_ref::())); + callback.forget(); + } + input.set_value(""); + input.click(); + } + }; + + view! { } +} + #[component] fn VfsMenuButton(menu_container: NodeRef) -> impl IntoView { let state = expect_context::>(); diff --git a/src/app/output/section.rs b/src/app/output/section.rs index c9ef169..dd91469 100644 --- a/src/app/output/section.rs +++ b/src/app/output/section.rs @@ -13,7 +13,7 @@ pub fn Section(label: String, children: Children) -> impl IntoView { view! {
- {children()} +

{children()}

} } diff --git a/src/app/output/status.rs b/src/app/output/status.rs index b081811..ca6de6d 100644 --- a/src/app/output/status.rs +++ b/src/app/output/status.rs @@ -20,7 +20,7 @@ Please close other tabs and refresh, or switch to Memory VFS."; pub fn Status() -> impl IntoView { let state = expect_context::>(); - let show = move || match &*state.last_error().read() { + let last_error = move || match &*state.last_error().read() { Some(error) => { let summary = format!("{}", error.deref()); let details = match error.deref() { @@ -42,12 +42,15 @@ pub fn Status() -> impl IntoView { "An unsupported type was encountered, please create an issue on github." } }, - WorkerError::NotFound | WorkerError::OpfsSAHError => { + WorkerError::NotFound | WorkerError::Unexpected => { "This shouldn't happen, please create an issue on github." } WorkerError::InvaildState => { "SQLite is in an abnormal state when executing SQLite." } + WorkerError::LoadDb(_) => { + "Please check whether the imported DB is a SQLite3 file" + } WorkerError::OpfsSAHPoolOpened => OPFS_SAH_POOL_OPENED_DETAILS, }, SQLightError::AceEditor(ace_editor) => match ace_editor { @@ -59,6 +62,9 @@ pub fn Status() -> impl IntoView { "This shouldn't happen, please create an issue on github." } }, + SQLightError::ImportDb(_) => { + "Maybe the db was not found, could not be read, or was too large." + } }; view! { @@ -72,9 +78,28 @@ pub fn Status() -> impl IntoView { None => view! { "No Error" }.into_any(), }; + let import_progress = move || { + if let Some(progress) = &*state.import_progress().read() { + let filename = format!("Filename: {}", progress.filename); + let status = format!("Loading: {} of {} bytes.", progress.loaded, progress.total); + view! { +

{filename}

+

{status}

+ } + .into_any() + } else { + view! { "No files are being imported." }.into_any() + } + }; + view! { -
{show}
+
+
{last_error}
+
+
+
{import_progress}
+
} } diff --git a/src/app/playground.rs b/src/app/playground.rs index 0ebf8c9..90821aa 100644 --- a/src/app/playground.rs +++ b/src/app/playground.rs @@ -37,6 +37,7 @@ pub fn playground( handle_automic_orientation(state); handle_connect_db(state); hanlde_save_state(state); + handle_import_progress(state); spawn_local(handle_state(state, rx)); @@ -83,6 +84,14 @@ fn handle_last_error(state: Store) { }); } +fn handle_import_progress(state: Store) { + Effect::new(move || { + if state.import_progress().read().is_some() { + change_focus(state, Some(Focus::Status)); + } + }); +} + fn handle_system_theme(state: Store) { Effect::new(move || { let theme = match state.theme().read().value() { diff --git a/src/app/state.rs b/src/app/state.rs index fc0cad2..157434f 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -41,6 +41,8 @@ pub struct GlobalState { output: Vec, #[serde(skip)] last_error: Option>, + #[serde(skip)] + import_progress: Option, } impl Default for GlobalState { @@ -62,6 +64,7 @@ impl Default for GlobalState { show_something: false, output: Vec::new(), last_error: None, + import_progress: None, } } } @@ -85,6 +88,12 @@ impl GlobalState { } } +pub struct ImportProgress { + pub filename: String, + pub loaded: f64, + pub total: f64, +} + #[derive(Serialize, Deserialize)] pub struct EditorConfig { pub keyboard: String, diff --git a/src/bin/worker.rs b/src/bin/worker.rs index bb509de..5937cf0 100644 --- a/src/bin/worker.rs +++ b/src/bin/worker.rs @@ -6,7 +6,7 @@ use web_sys::{DedicatedWorkerGlobalScope, MessageEvent}; fn main() { console_error_panic_hook::set_once(); - console_log::init_with_level(log::Level::Warn).unwrap(); + console_log::init_with_level(log::Level::Debug).unwrap(); let (tx, rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -34,6 +34,9 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv WorkerRequest::StepOver(id) => WorkerResponse::StepOver(worker::step_over(&id)), WorkerRequest::StepIn(id) => WorkerResponse::StepIn(worker::step_in(&id)), WorkerRequest::StepOut(id) => WorkerResponse::StepOut(worker::step_out(&id)), + WorkerRequest::LoadDb(options) => { + WorkerResponse::LoadDb(worker::load_db(options).await) + } }; if let Err(err) = scope.post_message(&serde_wasm_bindgen::to_value(&resp).unwrap()) { log::error!("Failed to send task to window: {resp:?}, {err:?}"); diff --git a/src/lib.rs b/src/lib.rs index fd74bc5..715565b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod worker; use aceditor::EditorError; use app::{GlobalState, GlobalStateStoreFields}; use fragile::Fragile; +use js_sys::Uint8Array; use leptos::prelude::*; use reactive_stores::Store; use std::{ @@ -58,6 +59,8 @@ pub enum SQLightError { Worker(#[from] WorkerError), #[error(transparent)] AceEditor(#[from] EditorError), + #[error("Failed to import db: {0}")] + ImportDb(String), } impl SQLightError { @@ -80,8 +83,10 @@ pub enum WorkerError { InvaildState, #[error("OPFS already opened")] OpfsSAHPoolOpened, - #[error("OPFS unexpected error")] - OpfsSAHError, + #[error("Failed to import db: {0}")] + LoadDb(String), + #[error("Unexpected error")] + Unexpected, } #[derive(Debug, Serialize, Deserialize)] @@ -92,6 +97,7 @@ pub enum WorkerRequest { StepOver(String), StepIn(String), StepOut(String), + LoadDb(LoadDbOptions), } #[derive(Debug, Serialize, Deserialize)] @@ -103,6 +109,7 @@ pub enum WorkerResponse { StepOver(Result), StepIn(Result<()>), StepOut(Result), + LoadDb(Result<()>), } #[derive(Debug, Serialize, Deserialize)] @@ -111,6 +118,14 @@ pub struct OpenOptions { pub persist: bool, } +#[derive(Debug, Serialize, Deserialize)] +pub struct LoadDbOptions { + pub id: String, + pub persist: bool, + #[serde(with = "serde_wasm_bindgen::preserve")] + pub data: Uint8Array, +} + impl OpenOptions { pub fn uri(&self) -> String { format!( @@ -158,11 +173,11 @@ pub struct SQLiteStatementValues { pub enum SQLitendError { #[error("An error occurred while converting a string to a CString")] ToCStr, - #[error("An error occurred while opening the DB: {0:?}")] + #[error("An error occurred while opening the DB: {0:#?}")] OpenDb(InnerError), - #[error("An error occurred while preparing stmt: {0:?}")] + #[error("An error occurred while preparing stmt: {0:#?}")] Prepare(InnerError), - #[error("An error occurred while stepping to the next line: {0:?}")] + #[error("An error occurred while stepping to the next line: {0:#?}")] Step(InnerError), #[error("An error occurred while getting column name: {0}")] GetColumnName(String), @@ -239,6 +254,15 @@ pub async fn handle_state(state: Store, mut rx: UnboundedReceiver unimplemented!(), + WorkerResponse::LoadDb(result) => { + let keep_ctx = result.is_ok(); + if let Err(err) = result { + state.last_error().set(Some(SQLightError::new_worker(err))); + } + state + .keep_ctx() + .maybe_update(|keep| std::mem::replace(keep, keep_ctx) != keep_ctx); + } } } } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 7a99e2d..83838bf 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -1,11 +1,14 @@ mod sqlitend; -use crate::{OpenOptions, PERSIST_VFS, PrepareOptions, SQLiteStatementResult, WorkerError}; +use crate::{ + LoadDbOptions, OpenOptions, PERSIST_VFS, PrepareOptions, SQLiteStatementResult, WorkerError, +}; use once_cell::sync::Lazy; use parking_lot::Mutex; use sqlite_wasm_rs::{ export::{OpfsSAHPoolCfgBuilder, OpfsSAHPoolUtil}, mem_vfs::MemVfsUtil, + utils::copy_to_vec, }; use sqlitend::{SQLiteDb, SQLitePreparedStatement, SQLiteStatements}; use std::{collections::HashMap, sync::Arc}; @@ -50,31 +53,63 @@ where f(DB_POOL.lock().get_mut(id).ok_or(WorkerError::NotFound)?) } +async fn opfs_util() -> Result<&'static OpfsSAHPoolUtil> { + FS_UTIL + .opfs + .get_or_try_init(|| async { + sqlite_wasm_rs::sahpool_vfs::install( + Some( + &OpfsSAHPoolCfgBuilder::new() + .directory(PERSIST_VFS) + .vfs_name(PERSIST_VFS) + .build(), + ), + false, + ) + .await + .map_err(|_| WorkerError::OpfsSAHPoolOpened) + }) + .await +} + +pub async fn load_db(options: LoadDbOptions) -> Result<()> { + let db = copy_to_vec(&options.data); + with_worker(&options.id, |worker| { + worker.db.take(); + + let filename = &worker.open_options.filename; + let FSUtil { mem, opfs } = &*FS_UTIL; + if worker.open_options.persist { + if let Some(opfs) = opfs.get() { + opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?; + + if let Err(err) = opfs.import_db(filename, &db) { + return Err(WorkerError::LoadDb(format!("{err}"))); + } + } + } else { + mem.delete_db(filename); + if let Err(err) = mem.import_db(filename, &db) { + return Err(WorkerError::LoadDb(format!("{err}"))); + } + } + + worker.db = Some(SQLiteDb::open(&worker.open_options.uri())?); + worker.state = SQLiteState::Idie; + Ok(()) + }) +} + pub async fn open(options: OpenOptions) -> Result { if let Some(worker) = DB_POOL.lock().get(&options.filename) { return Ok(worker.id.clone()); } if options.persist { - let util = FS_UTIL - .opfs - .get_or_try_init(|| async { - sqlite_wasm_rs::sahpool_vfs::install( - Some( - &OpfsSAHPoolCfgBuilder::new() - .directory(PERSIST_VFS) - .vfs_name(PERSIST_VFS) - .build(), - ), - false, - ) - .await - .map_err(|_| WorkerError::OpfsSAHPoolOpened) - }) - .await?; + let util = opfs_util().await?; if util.get_capacity() - util.get_file_count() * 3 < 3 { util.add_capacity(3) .await - .map_err(|_| WorkerError::OpfsSAHError)?; + .map_err(|_| WorkerError::Unexpected)?; } } // FIXME: multi db support @@ -99,8 +134,7 @@ pub fn prepare(options: PrepareOptions) -> Result<()> { let FSUtil { mem, opfs } = &*FS_UTIL; if worker.open_options.persist { if let Some(opfs) = opfs.get() { - opfs.unlink(filename) - .map_err(|_| WorkerError::OpfsSAHError)?; + opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?; } } else { mem.delete_db(filename);