Add export and download db support

This commit is contained in:
Spxg
2025-05-18 15:45:30 +08:00
parent 4aa23c5596
commit 2ad75161b9
6 changed files with 131 additions and 26 deletions

View File

@@ -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,
}));
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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:?}");

View File

@@ -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)));
}
},
}
}
}

View File

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