diff --git a/src/app/header.rs b/src/app/header.rs index c9f07e0..d353201 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -2,12 +2,13 @@ use istyles::istyles; use leptos::{html::Input, prelude::*, tachys::html}; use reactive_stores::Store; use wasm_bindgen::{JsCast, prelude::Closure}; -use web_sys::{Event, FileReader, HtmlInputElement, Url, UrlSearchParams}; +use web_sys::{Blob, Event, FileReader, HtmlInputElement, Url, UrlSearchParams}; use crate::{ - FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, WorkerRequest, + DownloadDbOptions, FragileComfirmed, LoadDbOptions, PrepareOptions, SQLightError, + WorkerRequest, app::{ - ImportProgress, Vfs, + ImportProgress, advanced_options_menu::AdvancedOptionsMenu, button_set::{Button, ButtonSet, IconButton, Rule}, config_menu::ConfigMenu, @@ -48,6 +49,8 @@ pub fn Header() -> impl IntoView { + + @@ -107,6 +110,47 @@ fn ExecuteButton() -> impl IntoView { } } +#[component] +fn DownloadButton() -> impl IntoView { + let state = expect_context::>(); + + Effect::new(move || { + state.exported().track(); + + let Some(downloaded) = state.exported().write_untracked().take() else { + return; + }; + let filename = downloaded.filename; + let buffer = downloaded.data; + let blob = Blob::new_with_u8_array_sequence(&js_sys::Array::from(&buffer)).unwrap(); + let url = Url::create_object_url_with_blob(&blob).unwrap(); + + let document = document(); + let a = document + .create_element("a") + .unwrap() + .dyn_into::() + .unwrap(); + + a.set_href(&url); + a.set_download(&filename); + a.click(); + + Url::revoke_object_url(&url).unwrap(); + }); + + let on_click = move |_| { + if let Some(worker) = &*state.worker().read() { + worker.send_task(WorkerRequest::DownloadDb(DownloadDbOptions { + // FIXME: multi db + id: String::new(), + })); + } + }; + + view! { } +} + #[component] fn LoadButton(input_ref: NodeRef) -> impl IntoView { let state = expect_context::>(); @@ -152,12 +196,10 @@ fn LoadButton(input_ref: NodeRef) -> impl IntoView { 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, })); } diff --git a/src/app/output/status.rs b/src/app/output/status.rs index ca6de6d..2c5fc36 100644 --- a/src/app/output/status.rs +++ b/src/app/output/status.rs @@ -51,6 +51,7 @@ pub fn Status() -> impl IntoView { WorkerError::LoadDb(_) => { "Please check whether the imported DB is a SQLite3 file" } + WorkerError::DownloadDb(_) => "It may be caused by OOM", WorkerError::OpfsSAHPoolOpened => OPFS_SAH_POOL_OPENED_DETAILS, }, SQLightError::AceEditor(ace_editor) => match ace_editor { diff --git a/src/app/state.rs b/src/app/state.rs index 157434f..4849cf5 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use aceditor::Editor; +use js_sys::Uint8Array; use leptos::tachys::dom::window; use reactive_stores::Store; use serde::{Deserialize, Serialize}; @@ -43,6 +44,8 @@ pub struct GlobalState { last_error: Option>, #[serde(skip)] import_progress: Option, + #[serde(skip)] + exported: Option, } impl Default for GlobalState { @@ -65,6 +68,7 @@ impl Default for GlobalState { output: Vec::new(), last_error: None, import_progress: None, + exported: None, } } } @@ -94,6 +98,11 @@ pub struct ImportProgress { pub total: f64, } +pub struct Exported { + pub filename: String, + pub data: FragileComfirmed, +} + #[derive(Serialize, Deserialize)] pub struct EditorConfig { pub keyboard: String, diff --git a/src/bin/worker.rs b/src/bin/worker.rs index 5937cf0..36e4f3b 100644 --- a/src/bin/worker.rs +++ b/src/bin/worker.rs @@ -29,7 +29,9 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv let request = serde_wasm_bindgen::from_value::(request).unwrap(); let resp = match request { WorkerRequest::Open(options) => WorkerResponse::Open(worker::open(options).await), - WorkerRequest::Prepare(options) => WorkerResponse::Prepare(worker::prepare(options)), + WorkerRequest::Prepare(options) => { + WorkerResponse::Prepare(worker::prepare(options).await) + } WorkerRequest::Continue(id) => WorkerResponse::Continue(worker::r#continue(&id)), WorkerRequest::StepOver(id) => WorkerResponse::StepOver(worker::step_over(&id)), WorkerRequest::StepIn(id) => WorkerResponse::StepIn(worker::step_in(&id)), @@ -37,6 +39,9 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv WorkerRequest::LoadDb(options) => { WorkerResponse::LoadDb(worker::load_db(options).await) } + WorkerRequest::DownloadDb(options) => { + WorkerResponse::DownloadDb(worker::download_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 715565b..db8b585 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod app; pub mod worker; use aceditor::EditorError; -use app::{GlobalState, GlobalStateStoreFields}; +use app::{Exported, GlobalState, GlobalStateStoreFields}; use fragile::Fragile; use js_sys::Uint8Array; use leptos::prelude::*; @@ -83,8 +83,10 @@ pub enum WorkerError { InvaildState, #[error("OPFS already opened")] OpfsSAHPoolOpened, - #[error("Failed to import db: {0}")] + #[error("Failed to load db: {0}")] LoadDb(String), + #[error("Failed to download db: {0}")] + DownloadDb(String), #[error("Unexpected error")] Unexpected, } @@ -98,6 +100,7 @@ pub enum WorkerRequest { StepIn(String), StepOut(String), LoadDb(LoadDbOptions), + DownloadDb(DownloadDbOptions), } #[derive(Debug, Serialize, Deserialize)] @@ -110,6 +113,14 @@ pub enum WorkerResponse { StepIn(Result<()>), StepOut(Result), LoadDb(Result<()>), + DownloadDb(Result), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DownloadDbResponse { + filename: String, + #[serde(with = "serde_wasm_bindgen::preserve")] + data: Uint8Array, } #[derive(Debug, Serialize, Deserialize)] @@ -121,11 +132,15 @@ pub struct OpenOptions { #[derive(Debug, Serialize, Deserialize)] pub struct LoadDbOptions { pub id: String, - pub persist: bool, #[serde(with = "serde_wasm_bindgen::preserve")] pub data: Uint8Array, } +#[derive(Debug, Serialize, Deserialize)] +pub struct DownloadDbOptions { + pub id: String, +} + impl OpenOptions { pub fn uri(&self) -> String { format!( @@ -263,6 +278,17 @@ pub async fn handle_state(state: Store, mut rx: UnboundedReceiver match result { + Ok(resp) => { + state.exported().set(Some(Exported { + filename: resp.filename, + data: FragileComfirmed::new(resp.data), + })); + } + Err(err) => { + state.last_error().set(Some(SQLightError::new_worker(err))); + } + }, } } } diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 83838bf..680efb7 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -1,14 +1,15 @@ mod sqlitend; use crate::{ - LoadDbOptions, OpenOptions, PERSIST_VFS, PrepareOptions, SQLiteStatementResult, WorkerError, + DownloadDbOptions, DownloadDbResponse, 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, + utils::{copy_to_uint8_array, copy_to_vec}, }; use sqlitend::{SQLiteDb, SQLitePreparedStatement, SQLiteStatements}; use std::{collections::HashMap, sync::Arc}; @@ -72,24 +73,45 @@ async fn opfs_util() -> Result<&'static OpfsSAHPoolUtil> { .await } +pub async fn download_db(options: DownloadDbOptions) -> Result { + let opfs = opfs_util().await?; + let mem_vfs = &FS_UTIL.mem; + + with_worker(&options.id, |worker| { + let filename = &worker.open_options.filename; + let db = if worker.open_options.persist { + opfs.export_file(filename) + .map_err(|err| WorkerError::DownloadDb(format!("{err}")))? + } else { + mem_vfs + .export_db(filename) + .map_err(|err| WorkerError::DownloadDb(format!("{err}")))? + }; + Ok(DownloadDbResponse { + filename: worker.open_options.filename.clone(), + data: copy_to_uint8_array(&db), + }) + }) +} + pub async fn load_db(options: LoadDbOptions) -> Result<()> { let db = copy_to_vec(&options.data); + + let opfs = opfs_util().await?; + let mem_vfs = &FS_UTIL.mem; + 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}"))); - } + 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) { + mem_vfs.delete_db(filename); + if let Err(err) = mem_vfs.import_db(filename, &db) { return Err(WorkerError::LoadDb(format!("{err}"))); } } @@ -125,19 +147,19 @@ pub async fn open(options: OpenOptions) -> Result { Ok(id) } -pub fn prepare(options: PrepareOptions) -> Result<()> { +pub async fn prepare(options: PrepareOptions) -> Result<()> { + let opfs = opfs_util().await?; + let mem_vfs = &FS_UTIL.mem; + with_worker(&options.id, |worker| { if options.clear_on_prepare { 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)?; - } + opfs.unlink(filename).map_err(|_| WorkerError::Unexpected)?; } else { - mem.delete_db(filename); + mem_vfs.delete_db(filename); } worker.db = Some(SQLiteDb::open(&worker.open_options.uri())?);