feat(card_db): Only fetch database when upstream has updated

This commit is contained in:
Tristan Daniël Maat 2025-03-26 02:27:39 +08:00
parent a3ce7f8008
commit 3d3d56440a
Signed by: tlater
GPG key ID: 49670FD774E43268
8 changed files with 249 additions and 23 deletions

View file

@ -9,6 +9,7 @@ fn main() {
if let Err(e) = a {
log::error!("{:?}", e);
panic!("{}", e);
}
})
}

View file

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

View file

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

View file

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