mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
refactor(infra): directory structure (#4615)
This commit is contained in:
225
packages/frontend/native/src/hashcash.rs
Normal file
225
packages/frontend/native/src/hashcash.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Stamp {
|
||||
version: String,
|
||||
claim: u32,
|
||||
ts: String,
|
||||
resource: String,
|
||||
ext: String,
|
||||
rand: String,
|
||||
counter: String,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
fn check_expiration(&self) -> bool {
|
||||
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
|
||||
.ok()
|
||||
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
|
||||
.and_then(|utc| {
|
||||
utc
|
||||
.checked_add_signed(Duration::minutes(5))
|
||||
.map(|utc| Utc::now() <= utc)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
|
||||
if self.version == "1"
|
||||
&& bits <= self.claim
|
||||
&& self.check_expiration()
|
||||
&& self.resource == resource.as_ref()
|
||||
{
|
||||
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
|
||||
|
||||
// check challenge
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(&self.format().as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize());
|
||||
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn format(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}",
|
||||
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint a new hashcash stamp.
|
||||
pub fn mint(resource: String, bits: Option<u32>) -> Self {
|
||||
let version = "1";
|
||||
let now = Utc::now();
|
||||
let ts = now.format("%Y%m%d%H%M%S");
|
||||
let bits = bits.unwrap_or(20);
|
||||
let rand = String::from_iter(
|
||||
Alphanumeric
|
||||
.sample_iter(thread_rng())
|
||||
.take(SALT_LENGTH)
|
||||
.map(char::from),
|
||||
);
|
||||
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
|
||||
|
||||
Stamp {
|
||||
version: version.to_string(),
|
||||
claim: bits,
|
||||
ts: ts.to_string(),
|
||||
resource,
|
||||
ext: "".to_string(),
|
||||
rand,
|
||||
counter: {
|
||||
let mut hasher = Sha3_256::new();
|
||||
let mut counter = 0;
|
||||
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
|
||||
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
|
||||
loop {
|
||||
hasher.update(&format!("{}:{:x}", challenge, counter).as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize_reset());
|
||||
if result[..hex_digits] == zeros {
|
||||
break format!("{:x}", counter);
|
||||
};
|
||||
counter += 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Stamp {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let stamp_vec = value.split(':').collect::<Vec<&str>>();
|
||||
if stamp_vec.len() != 7 {
|
||||
return Err(format!(
|
||||
"Malformed stamp, expected 6 parts, got {}",
|
||||
stamp_vec.len()
|
||||
));
|
||||
}
|
||||
Ok(Stamp {
|
||||
version: stamp_vec[0].to_string(),
|
||||
claim: stamp_vec[1]
|
||||
.parse()
|
||||
.map_err(|_| "Malformed stamp".to_string())?,
|
||||
ts: stamp_vec[2].to_string(),
|
||||
resource: stamp_vec[3].to_string(),
|
||||
ext: stamp_vec[4].to_string(),
|
||||
rand: stamp_vec[5].to_string(),
|
||||
counter: stamp_vec[6].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
stamp.check(self.bits, &self.resource)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn verify_challenge_response(
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
) -> AsyncTask<AsyncVerifyChallengeResponse> {
|
||||
AsyncTask::new(AsyncVerifyChallengeResponse {
|
||||
response,
|
||||
bits,
|
||||
resource,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AsyncMintChallengeResponse {
|
||||
bits: Option<u32>,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn mint_challenge_response(
|
||||
resource: String,
|
||||
bits: Option<u32>,
|
||||
) -> AsyncTask<AsyncMintChallengeResponse> {
|
||||
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Stamp;
|
||||
|
||||
#[test]
|
||||
fn test_mint() {
|
||||
let response = Stamp::mint("test".into(), Some(22)).format();
|
||||
assert!(Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(22, "test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check() {
|
||||
assert!(Stamp::try_from("1:20:20202116:test::Z4p8WaiO:31c14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
assert!(!Stamp::try_from("1:20:20202116:test1::Z4p8WaiO:31c14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
assert!(!Stamp::try_from("1:20:20202116:test::z4p8WaiO:31c14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
assert!(!Stamp::try_from("1:20:20202116:test::Z4p8WaiO:31C14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
assert!(Stamp::try_from("0:20:20202116:test::Z4p8WaiO:31c14").is_err());
|
||||
assert!(!Stamp::try_from("1:19:20202116:test::Z4p8WaiO:31c14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
assert!(!Stamp::try_from("1:20:20202115:test::Z4p8WaiO:31c14")
|
||||
.unwrap()
|
||||
.check(20, "test"));
|
||||
}
|
||||
}
|
||||
2
packages/frontend/native/src/lib.rs
Normal file
2
packages/frontend/native/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod sqlite;
|
||||
pub mod hashcash;
|
||||
364
packages/frontend/native/src/sqlite/mod.rs
Normal file
364
packages/frontend/native/src/sqlite/mod.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use napi::bindgen_prelude::{Buffer, FromNapiValue, ToNapiValue, Uint8Array};
|
||||
use napi_derive::napi;
|
||||
use sqlx::{
|
||||
migrate::MigrateDatabase,
|
||||
sqlite::{Sqlite, SqliteConnectOptions, SqlitePoolOptions},
|
||||
Pool, Row,
|
||||
};
|
||||
|
||||
// latest version
|
||||
const LATEST_VERSION: i32 = 4;
|
||||
|
||||
#[napi(object)]
|
||||
pub struct BlobRow {
|
||||
pub key: String,
|
||||
pub data: Buffer,
|
||||
pub timestamp: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct UpdateRow {
|
||||
pub id: i64,
|
||||
pub timestamp: NaiveDateTime,
|
||||
pub data: Buffer,
|
||||
pub doc_id: Option<String>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct InsertRow {
|
||||
pub doc_id: Option<String>,
|
||||
pub data: Uint8Array,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct SqliteConnection {
|
||||
pool: Pool<Sqlite>,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub enum ValidationResult {
|
||||
MissingTables,
|
||||
MissingDocIdColumn,
|
||||
MissingVersionColumn,
|
||||
GeneralError,
|
||||
Valid,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl SqliteConnection {
|
||||
#[napi(constructor)]
|
||||
pub fn new(path: String) -> napi::Result<Self> {
|
||||
let sqlite_options = SqliteConnectOptions::new()
|
||||
.filename(&path)
|
||||
.foreign_keys(false)
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Off);
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(4)
|
||||
.connect_lazy_with(sqlite_options);
|
||||
Ok(Self { pool, path })
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn connect(&self) -> napi::Result<()> {
|
||||
if !Sqlite::database_exists(&self.path).await.unwrap_or(false) {
|
||||
Sqlite::create_database(&self.path)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
};
|
||||
let mut connection = self.pool.acquire().await.map_err(anyhow::Error::from)?;
|
||||
sqlx::query(affine_schema::SCHEMA)
|
||||
.execute(connection.as_mut())
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
self.migrate_add_doc_id().await?;
|
||||
connection.detach();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn add_blob(&self, key: String, blob: Uint8Array) -> napi::Result<()> {
|
||||
let blob = blob.as_ref();
|
||||
sqlx::query_as!(
|
||||
BlobRow,
|
||||
"INSERT INTO blobs (key, data) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET data = excluded.data",
|
||||
key,
|
||||
blob,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_blob(&self, key: String) -> Option<BlobRow> {
|
||||
sqlx::query_as!(
|
||||
BlobRow,
|
||||
"SELECT key, data, timestamp FROM blobs WHERE key = ?",
|
||||
key
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn delete_blob(&self, key: String) -> napi::Result<()> {
|
||||
sqlx::query!("DELETE FROM blobs WHERE key = ?", key)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_blob_keys(&self) -> napi::Result<Vec<String>> {
|
||||
let keys = sqlx::query!("SELECT key FROM blobs")
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map(|rows| rows.into_iter().map(|row| row.key).collect())
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_updates(&self, doc_id: Option<String>) -> napi::Result<Vec<UpdateRow>> {
|
||||
let updates = match doc_id {
|
||||
Some(doc_id) => sqlx::query_as!(
|
||||
UpdateRow,
|
||||
"SELECT id, timestamp, data, doc_id FROM updates WHERE doc_id = ?",
|
||||
doc_id
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?,
|
||||
None => sqlx::query_as!(
|
||||
UpdateRow,
|
||||
"SELECT id, timestamp, data, doc_id FROM updates WHERE doc_id is NULL",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?,
|
||||
};
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_updates_count(&self, doc_id: Option<String>) -> napi::Result<i32> {
|
||||
let count = match doc_id {
|
||||
Some(doc_id) => {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM updates WHERE doc_id = ?",
|
||||
doc_id
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?
|
||||
.count
|
||||
}
|
||||
None => {
|
||||
sqlx::query!("SELECT COUNT(*) as count FROM updates WHERE doc_id is NULL")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?
|
||||
.count
|
||||
}
|
||||
};
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_all_updates(&self) -> napi::Result<Vec<UpdateRow>> {
|
||||
let updates = sqlx::query_as!(UpdateRow, "SELECT id, timestamp, data, doc_id FROM updates")
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn insert_updates(&self, updates: Vec<InsertRow>) -> napi::Result<()> {
|
||||
let mut transaction = self.pool.begin().await.map_err(anyhow::Error::from)?;
|
||||
for InsertRow { data, doc_id } in updates {
|
||||
let update = data.as_ref();
|
||||
sqlx::query_as!(
|
||||
UpdateRow,
|
||||
"INSERT INTO updates (data, doc_id) VALUES ($1, $2)",
|
||||
update,
|
||||
doc_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
}
|
||||
transaction.commit().await.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn replace_updates(
|
||||
&self,
|
||||
doc_id: Option<String>,
|
||||
updates: Vec<InsertRow>,
|
||||
) -> napi::Result<()> {
|
||||
let mut transaction = self.pool.begin().await.map_err(anyhow::Error::from)?;
|
||||
|
||||
match doc_id {
|
||||
Some(doc_id) => sqlx::query!("DELETE FROM updates where doc_id = ?", doc_id)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?,
|
||||
None => sqlx::query!("DELETE FROM updates where doc_id is NULL",)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?,
|
||||
};
|
||||
|
||||
for InsertRow { data, doc_id } in updates {
|
||||
let update = data.as_ref();
|
||||
sqlx::query_as!(
|
||||
UpdateRow,
|
||||
"INSERT INTO updates (data, doc_id) VALUES ($1, $2)",
|
||||
update,
|
||||
doc_id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
}
|
||||
transaction.commit().await.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn init_version(&self) -> napi::Result<()> {
|
||||
// create version_info table
|
||||
sqlx::query!(
|
||||
"CREATE TABLE IF NOT EXISTS version_info (
|
||||
version NUMBER NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)"
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
// `3` is the first version that has version_info table,
|
||||
// do not modify the version number.
|
||||
sqlx::query!("INSERT INTO version_info (version) VALUES (3)")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn set_version(&self, version: i32) -> napi::Result<()> {
|
||||
if version > LATEST_VERSION {
|
||||
return Err(anyhow::Error::msg("Version is too new").into());
|
||||
}
|
||||
sqlx::query!("UPDATE version_info SET version = ?", version)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn get_max_version(&self) -> napi::Result<i32> {
|
||||
// 4 is the current version
|
||||
let version = sqlx::query!("SELECT COALESCE(MAX(version), 4) AS max_version FROM version_info")
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?
|
||||
.max_version;
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn close(&self) {
|
||||
self.pool.close().await;
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn is_close(&self) -> bool {
|
||||
self.pool.is_closed()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn validate(path: String) -> ValidationResult {
|
||||
let pool = match SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&path)
|
||||
.await
|
||||
{
|
||||
Ok(pool) => pool,
|
||||
Err(_) => return ValidationResult::GeneralError,
|
||||
};
|
||||
|
||||
let tables_res = sqlx::query("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
|
||||
let tables_exist = match tables_res {
|
||||
Ok(res) => {
|
||||
let names: Vec<String> = res.iter().map(|row| row.get(0)).collect();
|
||||
names.contains(&"updates".to_string()) && names.contains(&"blobs".to_string())
|
||||
}
|
||||
Err(_) => return ValidationResult::GeneralError,
|
||||
};
|
||||
|
||||
let tables_res = sqlx::query("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
|
||||
let version_exist = match tables_res {
|
||||
Ok(res) => {
|
||||
let names: Vec<String> = res.iter().map(|row| row.get(0)).collect();
|
||||
names.contains(&"version_info".to_string())
|
||||
}
|
||||
Err(_) => return ValidationResult::GeneralError,
|
||||
};
|
||||
|
||||
let columns_res = sqlx::query("PRAGMA table_info(updates)")
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
|
||||
let doc_id_exist = match columns_res {
|
||||
Ok(res) => {
|
||||
let names: Vec<String> = res.iter().map(|row| row.get(1)).collect();
|
||||
names.contains(&"doc_id".to_string())
|
||||
}
|
||||
Err(_) => return ValidationResult::GeneralError,
|
||||
};
|
||||
|
||||
if !tables_exist {
|
||||
ValidationResult::MissingTables
|
||||
} else if !doc_id_exist {
|
||||
ValidationResult::MissingDocIdColumn
|
||||
} else if !version_exist {
|
||||
ValidationResult::MissingVersionColumn
|
||||
} else {
|
||||
ValidationResult::Valid
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn migrate_add_doc_id(&self) -> napi::Result<()> {
|
||||
// ignore errors
|
||||
match sqlx::query("ALTER TABLE updates ADD COLUMN doc_id TEXT")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.to_string().contains("duplicate column name") {
|
||||
Ok(()) // Ignore error if it's due to duplicate column
|
||||
} else {
|
||||
Err(anyhow::Error::from(err).into()) // Propagate other errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user