Add import and load db support
This commit is contained in:
@@ -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! {
|
||||
<>
|
||||
<div id="header" class=styles::container>
|
||||
@@ -41,6 +45,10 @@ pub fn Header() -> impl IntoView {
|
||||
</ButtonSet>
|
||||
</div>
|
||||
<div class=styles::right>
|
||||
<input type="file" node_ref=input_ref style="display: none" />
|
||||
<ButtonSet>
|
||||
<LoadButton input_ref=input_ref />
|
||||
</ButtonSet>
|
||||
<ButtonSet>
|
||||
<ShareButton />
|
||||
</ButtonSet>
|
||||
@@ -99,6 +107,108 @@ fn ExecuteButton() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoadButton(input_ref: NodeRef<Input>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
let (file, set_file) = signal::<Option<FragileComfirmed<web_sys::File>>>(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<dyn FnMut(_)>));
|
||||
|
||||
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::<FileReader>();
|
||||
if let Some(dom_error) = reader.error() {
|
||||
state.last_error().set(Some(FragileComfirmed::new(
|
||||
SQLightError::ImportDb(dom_error.message().to_string()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
as Box<dyn FnMut(_)>));
|
||||
|
||||
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::<FileReader>();
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
as Box<dyn FnMut(_)>));
|
||||
|
||||
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::<HtmlInputElement>();
|
||||
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<dyn Fn(Event)>);
|
||||
input.set_onchange(Some(callback.as_ref().unchecked_ref::<js_sys::Function>()));
|
||||
callback.forget();
|
||||
}
|
||||
input.set_value("");
|
||||
input.click();
|
||||
}
|
||||
};
|
||||
|
||||
view! { <Button on_click=on_click>"Load DB"</Button> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn VfsMenuButton(menu_container: NodeRef<html::element::Div>) -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn Section(label: String, children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<Header label=label />
|
||||
{children()}
|
||||
<p>{children()}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ Please close other tabs and refresh, or switch to Memory VFS.";
|
||||
pub fn Status() -> impl IntoView {
|
||||
let state = expect_context::<Store<GlobalState>>();
|
||||
|
||||
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! {
|
||||
<p>{filename}</p>
|
||||
<p>{status}</p>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
view! { "No files are being imported." }.into_any()
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<SimplePane>
|
||||
<Section label="Last Error".into()>{show}</Section>
|
||||
<Section label="Last Error".into()>
|
||||
<pre style="white-space: pre-wrap;">{last_error}</pre>
|
||||
</Section>
|
||||
<Section label="Import Progress".into()>
|
||||
<pre style="white-space: pre-wrap;">{import_progress}</pre>
|
||||
</Section>
|
||||
</SimplePane>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GlobalState>) {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_import_progress(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
if state.import_progress().read().is_some() {
|
||||
change_focus(state, Some(Focus::Status));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_system_theme(state: Store<GlobalState>) {
|
||||
Effect::new(move || {
|
||||
let theme = match state.theme().read().value() {
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct GlobalState {
|
||||
output: Vec<SQLiteStatementResult>,
|
||||
#[serde(skip)]
|
||||
last_error: Option<FragileComfirmed<SQLightError>>,
|
||||
#[serde(skip)]
|
||||
import_progress: Option<ImportProgress>,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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::<JsValue>();
|
||||
|
||||
@@ -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:?}");
|
||||
|
||||
34
src/lib.rs
34
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<SQLiteStatementResult>),
|
||||
StepIn(Result<()>),
|
||||
StepOut(Result<SQLiteStatementResult>),
|
||||
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<GlobalState>, mut rx: UnboundedReceiver<W
|
||||
WorkerResponse::StepOver(_)
|
||||
| WorkerResponse::StepIn(_)
|
||||
| WorkerResponse::StepOut(_) => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 +53,8 @@ where
|
||||
f(DB_POOL.lock().get_mut(id).ok_or(WorkerError::NotFound)?)
|
||||
}
|
||||
|
||||
pub async fn open(options: OpenOptions) -> Result<String> {
|
||||
if let Some(worker) = DB_POOL.lock().get(&options.filename) {
|
||||
return Ok(worker.id.clone());
|
||||
}
|
||||
if options.persist {
|
||||
let util = FS_UTIL
|
||||
async fn opfs_util() -> Result<&'static OpfsSAHPoolUtil> {
|
||||
FS_UTIL
|
||||
.opfs
|
||||
.get_or_try_init(|| async {
|
||||
sqlite_wasm_rs::sahpool_vfs::install(
|
||||
@@ -70,11 +69,47 @@ pub async fn open(options: OpenOptions) -> Result<String> {
|
||||
.await
|
||||
.map_err(|_| WorkerError::OpfsSAHPoolOpened)
|
||||
})
|
||||
.await?;
|
||||
.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<String> {
|
||||
if let Some(worker) = DB_POOL.lock().get(&options.filename) {
|
||||
return Ok(worker.id.clone());
|
||||
}
|
||||
if options.persist {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user