Add export and download db support
This commit is contained in:
@@ -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 {
|
||||
<input type="file" node_ref=input_ref style="display: none" />
|
||||
<ButtonSet>
|
||||
<LoadButton input_ref=input_ref />
|
||||
<Rule />
|
||||
<DownloadButton />
|
||||
</ButtonSet>
|
||||
<ButtonSet>
|
||||
<ShareButton />
|
||||
@@ -107,6 +110,47 @@ fn ExecuteButton() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DownloadButton() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
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::<web_sys::HtmlAnchorElement>()
|
||||
.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! { <Button on_click=on_click>"Download DB"</Button> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoadButton(input_ref: NodeRef<Input>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
@@ -152,12 +196,10 @@ fn LoadButton(input_ref: NodeRef<Input>) -> impl IntoView {
|
||||
if let Ok(result) = reader.result() {
|
||||
let array_buffer = result.unchecked_into::<js_sys::ArrayBuffer>();
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<FragileComfirmed<SQLightError>>,
|
||||
#[serde(skip)]
|
||||
import_progress: Option<ImportProgress>,
|
||||
#[serde(skip)]
|
||||
exported: Option<Exported>,
|
||||
}
|
||||
|
||||
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<Uint8Array>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditorConfig {
|
||||
pub keyboard: String,
|
||||
|
||||
@@ -29,7 +29,9 @@ async fn execute_task(scope: DedicatedWorkerGlobalScope, mut rx: UnboundedReceiv
|
||||
let request = serde_wasm_bindgen::from_value::<WorkerRequest>(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:?}");
|
||||
|
||||
32
src/lib.rs
32
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<SQLiteStatementResult>),
|
||||
LoadDb(Result<()>),
|
||||
DownloadDb(Result<DownloadDbResponse>),
|
||||
}
|
||||
|
||||
#[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<GlobalState>, mut rx: UnboundedReceiver<W
|
||||
.keep_ctx()
|
||||
.maybe_update(|keep| std::mem::replace(keep, keep_ctx) != keep_ctx);
|
||||
}
|
||||
WorkerResponse::DownloadDb(result) => 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)));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DownloadDbResponse> {
|
||||
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<String> {
|
||||
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())?);
|
||||
|
||||
Reference in New Issue
Block a user