feat(card_db): Only fetch database when upstream has updated
This commit is contained in:
parent
a3ce7f8008
commit
3d3d56440a
8 changed files with 249 additions and 23 deletions
src
|
@ -9,6 +9,7 @@ fn main() {
|
|||
|
||||
if let Err(e) = a {
|
||||
log::error!("{:?}", e);
|
||||
panic!("{}", e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,11 +4,7 @@ pub mod models;
|
|||
pub mod schema;
|
||||
mod worker;
|
||||
|
||||
use sqlite_wasm_rs::export::{install_opfs_sahpool, OpfsSAHError};
|
||||
use thiserror_ext::AsReport;
|
||||
use worker::{DatabaseWorker, Query};
|
||||
|
||||
use crate::utils::github_fetch_file;
|
||||
use worker::{BrowserDatabaseError, DatabaseWorker, Query};
|
||||
|
||||
pub struct BrowserDatabase {
|
||||
worker: WorkerBridge<DatabaseWorker>,
|
||||
|
@ -34,19 +30,9 @@ impl BrowserDatabase {
|
|||
/// Initialize the database worker.
|
||||
///
|
||||
/// Must be executed in a worker script, not on the main thread.
|
||||
pub async fn init_worker() -> Result<(), OpfsSAHError> {
|
||||
pub async fn init_worker() -> Result<(), BrowserDatabaseError> {
|
||||
log::debug!("Initializing database worker...");
|
||||
let sah_pool_util = install_opfs_sahpool(None, false).await?;
|
||||
|
||||
let babel_cdb = match github_fetch_file("ProjectIgnis", "BabelCDB", "cards.cdb").await {
|
||||
Ok(cdb) => cdb,
|
||||
Err(err) => {
|
||||
log::error!("{}", err.to_report_string());
|
||||
panic!("{:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
sah_pool_util.import_db("/cards.db", &babel_cdb)?;
|
||||
DatabaseWorker::initialize_db().await?;
|
||||
DatabaseWorker::registrar().register();
|
||||
log::debug!("Database worker set up!");
|
||||
Ok(())
|
||||
|
|
|
@ -2,16 +2,22 @@ use diesel::{prelude::*, Connection};
|
|||
use futures::TryFutureExt;
|
||||
use gloo::worker::{HandlerId, Worker, WorkerScope};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlite_wasm_rs::export::{install_opfs_sahpool, OpfsSAHPoolUtil};
|
||||
use thiserror::Error;
|
||||
use thiserror_ext::AsReport;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
use crate::utils::{
|
||||
self, github_fetch_file, github_fetch_last_updated, read_opfs_file, write_opfs_file,
|
||||
};
|
||||
|
||||
use super::{
|
||||
models::CardDetails,
|
||||
schema::{datas, texts},
|
||||
};
|
||||
|
||||
const DB_URL: &str = "file:/cards.db?vfs=opfs-sahpool&mode=ro";
|
||||
const DB_URL: &str = "file:/cards.cdb?vfs=opfs-sahpool&mode=ro";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Query {
|
||||
|
@ -20,6 +26,7 @@ pub enum Query {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Response {
|
||||
Update,
|
||||
ByPassword(Option<CardDetails>),
|
||||
Error(String),
|
||||
}
|
||||
|
@ -27,6 +34,13 @@ pub enum Response {
|
|||
pub struct DatabaseWorker {}
|
||||
|
||||
impl DatabaseWorker {
|
||||
pub async fn initialize_db() -> Result<()> {
|
||||
let sah_pool_util = install_opfs_sahpool(None, false).await?;
|
||||
update_card_db(&sah_pool_util).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_query(query: &Query) -> Result<Response> {
|
||||
let res = match query {
|
||||
Query::ByPassword(pw) => Response::ByPassword(Self::get_card(*pw).await?),
|
||||
|
@ -126,15 +140,58 @@ impl Worker for DatabaseWorker {
|
|||
}
|
||||
}
|
||||
|
||||
async fn should_update_card_db() -> Result<bool> {
|
||||
let last_update = {
|
||||
let ts = read_opfs_file("last_update.txt").await?;
|
||||
if !ts.is_empty() {
|
||||
OffsetDateTime::parse(&ts, &Rfc3339).map_err(BrowserDatabaseError::from)
|
||||
} else {
|
||||
Ok(OffsetDateTime::UNIX_EPOCH)
|
||||
}
|
||||
}?;
|
||||
|
||||
log::debug!("Last database update: {}", last_update);
|
||||
|
||||
let latest_version = github_fetch_last_updated("ProjectIgnis", "BabelCDB", "cards.cdb").await?;
|
||||
|
||||
log::debug!("Latest database version: {}", latest_version);
|
||||
|
||||
Ok(last_update < latest_version)
|
||||
}
|
||||
|
||||
async fn update_card_db(sah_pool_util: &OpfsSAHPoolUtil) -> Result<()> {
|
||||
if should_update_card_db().await? {
|
||||
log::info!("Updating card database...");
|
||||
let db = github_fetch_file("ProjectIgnis", "BabelCDB", "cards.cdb").await?;
|
||||
sah_pool_util.import_db("/cards.cdb", &db)?;
|
||||
write_opfs_file(
|
||||
"last_update.txt",
|
||||
&OffsetDateTime::now_utc().format(&Rfc3339)?,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
log::debug!("Skipping card database updte");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BrowserDatabaseError {
|
||||
// #[error("failed to set up browser database storage")]
|
||||
// VFSError(#[from] sqlite_wasm_rs::export::OpfsSAHError),
|
||||
// #[error("could not fetch updated card database")]
|
||||
// CardDbFetchError(#[from] GithubError),
|
||||
#[error("failed to connect to database")]
|
||||
DieselConnectionError(#[from] diesel::ConnectionError),
|
||||
#[error("failed to query database")]
|
||||
DieselQueryError(#[from] diesel::result::Error),
|
||||
|
||||
#[error("failed to write database to local storage")]
|
||||
FileStorage(#[from] sqlite_wasm_rs::export::OpfsSAHError),
|
||||
#[error("failed to fetch Babel DB")]
|
||||
GitHubAPI(#[from] crate::utils::GithubError),
|
||||
#[error("failed to retrieve last Babel DB update date")]
|
||||
LocalStorage(#[from] utils::OpfsError),
|
||||
#[error("failed to retrieve last Babel DB update date")]
|
||||
TimeParseError(#[from] time::error::Parse),
|
||||
#[error("failed to write last Babel DB update date")]
|
||||
TimeFormatError(#[from] time::error::Format),
|
||||
}
|
||||
type Result<T> = std::result::Result<T, BrowserDatabaseError>;
|
||||
|
|
122
src/utils.rs
122
src/utils.rs
|
@ -2,6 +2,13 @@ use gloo::net::http;
|
|||
use thiserror::Error;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::{js_sys, JsFuture};
|
||||
use web_sys::{
|
||||
js_sys::Object, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions,
|
||||
FileSystemWritableFileStream, WorkerGlobalScope,
|
||||
};
|
||||
|
||||
const GITHUB_API: &str = "https://api.github.com";
|
||||
|
||||
pub async fn github_fetch_last_updated(
|
||||
|
@ -58,3 +65,118 @@ pub enum GithubError {
|
|||
#[error("date/time format reported for GitHub commit is invalid")]
|
||||
InvalidDateTimeError(#[from] time::error::Parse),
|
||||
}
|
||||
|
||||
/// Read a file from the in-browser OPFS filesystem, creating the file
|
||||
/// if it does not exist.
|
||||
pub async fn read_opfs_file(path: &str) -> Result<String, OpfsError> {
|
||||
let dir: FileSystemDirectoryHandle = JsFuture::from(
|
||||
js_sys::global()
|
||||
.dyn_into::<WorkerGlobalScope>()
|
||||
.map_err(OpfsError::NotSupported)?
|
||||
.navigator()
|
||||
.storage()
|
||||
.get_directory(),
|
||||
)
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
let opt = FileSystemGetFileOptions::new();
|
||||
opt.set_create(true);
|
||||
let file_handle: FileSystemFileHandle =
|
||||
JsFuture::from(dir.get_file_handle_with_options(path, &opt))
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
let file: web_sys::File = JsFuture::from(file_handle.get_file())
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
Ok(gloo::file::futures::read_as_text(&gloo::file::Blob::from(file)).await?)
|
||||
}
|
||||
|
||||
/// Write contents to a file in the in-browser OPFS, creating the file
|
||||
/// if it does not exist.
|
||||
pub async fn write_opfs_file(path: &str, contents: &str) -> Result<(), OpfsError> {
|
||||
let dir: FileSystemDirectoryHandle = JsFuture::from(
|
||||
js_sys::global()
|
||||
.dyn_into::<WorkerGlobalScope>()
|
||||
.map_err(OpfsError::NotSupported)?
|
||||
.navigator()
|
||||
.storage()
|
||||
.get_directory(),
|
||||
)
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
let opt = FileSystemGetFileOptions::new();
|
||||
opt.set_create(true);
|
||||
let file_handle: FileSystemFileHandle =
|
||||
JsFuture::from(dir.get_file_handle_with_options(path, &opt))
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
let write_handle: FileSystemWritableFileStream = JsFuture::from(file_handle.create_writable())
|
||||
.await
|
||||
.map_err(OpfsError::HandleAccess)?
|
||||
.into();
|
||||
|
||||
JsFuture::from(
|
||||
write_handle
|
||||
.write_with_str(contents)
|
||||
.map_err(OpfsError::WriteFile)?,
|
||||
)
|
||||
.await
|
||||
.map_err(OpfsError::WriteFile)?;
|
||||
|
||||
JsFuture::from(write_handle.close())
|
||||
.await
|
||||
.map_err(OpfsError::WriteFile)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OpfsError {
|
||||
#[error("OPFS is not supported by this browser: {0:?}")]
|
||||
NotSupported(Object),
|
||||
#[error("could not get OPFS directory path: {0:?}")]
|
||||
HandleAccess(JsValue),
|
||||
#[error("could not read OPFS file")]
|
||||
ReadFile(#[from] gloo::file::FileReadError),
|
||||
#[error("could not write OPFS file: {0:?}")]
|
||||
WriteFile(JsValue),
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(target_arch = "wasm32", target_arch = "wasm64"),
|
||||
target_os = "unknown",
|
||||
test
|
||||
))]
|
||||
mod worker_tests {
|
||||
use wasm_bindgen_test::*;
|
||||
wasm_bindgen_test_configure!(run_in_dedicated_worker);
|
||||
|
||||
use super::{read_opfs_file, write_opfs_file};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_read_write_opfs() {
|
||||
let empty = read_opfs_file("test.txt")
|
||||
.await
|
||||
.expect("file read must succeed");
|
||||
assert_eq!(empty, "");
|
||||
|
||||
write_opfs_file("test.txt", "contents")
|
||||
.await
|
||||
.expect("file write must succeed");
|
||||
|
||||
let nonempty = read_opfs_file("test.txt")
|
||||
.await
|
||||
.expect("file read must succeed");
|
||||
assert_eq!(nonempty, "contents");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue