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())?);