use std::collections::{BTreeMap, HashSet};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use rusqlite::{Connection, ErrorCode, OpenFlags};
use serde_json::{json, Map, Value};
use super::schema::{self, Schema};
use super::tx::Worker;
use super::{Core, DbFile, FULL_INTERVAL, READERS};
use crate::error::{Error, Result};
use crate::paths;
use crate::secure::Secure;
const USERS_VERSION: u64 = 1;
const DAY_MS: i64 = 86_400_000;
pub(crate) struct OpenParams {
pub app_key: String,
pub username: String,
pub version: u32,
pub schema: Arc<Schema>,
pub encrypt: bool,
pub touch: bool,
}
pub(crate) struct Physical {
pub file: DbFile,
pub old_version: u32,
pub reset: Option<&'static str>,
}
pub(crate) fn open_file(core: &Core, p: OpenParams) -> Result<Physical> {
let path = paths::db_path(&core.data_dir, &p.app_key, &p.username);
core.wait_closing(&path);
if let Some(dir) = path.parent() {
fs::create_dir_all(dir)?;
}
let users_path = paths::app_dir(&core.data_dir, &p.app_key).join("users.json");
let mut users = Users::read(users_path);
let mut reset = None;
if let Some(previous) = users.encrypted(&p.username) {
if previous != p.encrypt && path.exists() {
log::warn!("本地数据库的加密设置变了({previous} → {}),删除重建", p.encrypt);
delete_db_files(&path)?;
reset = Some("encryption_changed");
}
}
let key = db_key(&core.secure, &p.app_key, &p.username, p.encrypt)?;
let (mut conn, mut stored) = match probe(&path, key.as_deref()) {
Ok(opened) => opened,
Err(err) if is_corrupt(&err) => {
let reason = if p.encrypt && plain_header(&path) { "encryption_changed" } else { "corrupted" };
log::warn!("本地数据库打不开({err}),删除重建");
delete_db_files(&path)?;
reset = Some(reset.unwrap_or(reason));
probe(&path, key.as_deref())?
}
Err(err) => return Err(err.into()),
};
if stored > p.version {
log::warn!("本地数据库的结构版本 {stored} 比本 SDK 的 {} 新(App 降级),删除后重新同步", p.version);
drop(conn);
delete_db_files(&path)?;
(conn, stored) = probe(&path, key.as_deref())?;
reset = Some("schema_downgrade");
}
let old_version = stored;
if stored < p.version {
schema::upgrade(&mut conn, &p.schema, p.version)?;
}
let readers = (0..READERS).map(|_| open_conn(&path, key.as_deref(), true)).collect::<rusqlite::Result<Vec<_>>>()?;
let tag: String = path.file_stem().map(|s| s.to_string_lossy().chars().take(6).collect()).unwrap_or_default();
let writer = Worker::spawn(format!("drim-db-w-{tag}"), conn)?;
let readers = readers.into_iter().enumerate().map(|(i, c)| Worker::spawn(format!("drim-db-r{i}-{tag}"), c)).collect::<Result<Vec<_>>>()?;
if p.touch && users.touch(&p.username, p.encrypt, now_ms()) {
if let Err(err) = users.write() {
log::warn!("更新 users.json 失败:{err}");
}
}
let file = DbFile::new(p.app_key, p.username, path, p.encrypt, p.schema, p.version, writer, readers);
Ok(Physical { file, old_version, reset })
}
fn probe(path: &Path, key: Option<&str>) -> rusqlite::Result<(Connection, u32)> {
let conn = open_conn(path, key, false)?;
let version = schema::read_version(&conn)?;
Ok((conn, version))
}
fn open_conn(path: &Path, key: Option<&str>, readonly: bool) -> rusqlite::Result<Connection> {
let flags = if readonly { OpenFlags::SQLITE_OPEN_READ_ONLY } else { OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE };
let conn = Connection::open_with_flags(path, flags | OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
if let Some(key) = key {
conn.execute_batch(&format!("PRAGMA key = \"x'{key}'\""))?;
}
conn.busy_timeout(Duration::from_secs(5))?;
conn.set_prepared_statement_cache_capacity(64);
if !readonly {
conn.pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get::<_, String>(0))?;
conn.execute_batch("PRAGMA synchronous = NORMAL")?;
}
Ok(conn)
}
fn is_corrupt(err: &rusqlite::Error) -> bool {
matches!(err, rusqlite::Error::SqliteFailure(e, _) if matches!(e.code, ErrorCode::NotADatabase | ErrorCode::DatabaseCorrupt))
}
fn plain_header(path: &Path) -> bool {
let mut header = [0u8; 16];
fs::File::open(path).and_then(|mut f| f.read_exact(&mut header)).is_ok() && &header == b"SQLite format 3\0"
}
pub(crate) fn delete_db_files(path: &Path) -> Result<()> {
for suffix in ["", "-wal", "-shm", "-journal"] {
let mut name = path.as_os_str().to_owned();
name.push(suffix);
match fs::remove_file(PathBuf::from(name)) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(Error::from_io(&e)),
}
}
Ok(())
}
fn delete_user_files(data_dir: &Path, app_key: &str, username: &str) -> Result<()> {
delete_db_files(&paths::db_path(data_dir, app_key, username))?;
match fs::remove_dir_all(paths::user_dir(data_dir, app_key, username)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::from_io(&e)),
}
}
pub(crate) fn delete_user(core: &Core, app_key: &str, username: &str) -> Result<()> {
delete_user_files(&core.data_dir, app_key, username)?;
let mut users = Users::read(paths::app_dir(&core.data_dir, app_key).join("users.json"));
if users.remove(&[username.to_string()]) {
users.write()?;
}
forget_db_key(&core.secure, app_key, username);
Ok(())
}
impl Core {
pub(crate) fn storage_full(&self, app_key: &str, current: &str) {
{
let mut map = self.full_at.lock().unwrap();
if map.get(app_key).is_some_and(|t| t.elapsed() < FULL_INTERVAL) {
return;
}
map.insert(app_key.to_string(), Instant::now());
}
let Some(core) = self.me.upgrade() else { return };
let app_key = app_key.to_string();
let current = current.to_string();
let spawned = std::thread::Builder::new().name("drim-db-full".into()).spawn(move || {
let freed = core.free_space(&app_key, ¤t);
(core.on_full)(app_key, freed);
});
if let Err(err) = spawned {
log::error!("创建磁盘清理线程失败:{err}");
}
}
fn free_space(&self, app_key: &str, current: &str) -> usize {
let _guard = self.open_lock.blocking_lock();
let users_path = paths::app_dir(&self.data_dir, app_key).join("users.json");
let open: HashSet<String> = self.state.lock().unwrap().files.values().filter(|f| f.app_key == app_key).map(|f| f.username.clone()).collect();
let mut freed = Vec::new();
for name in Users::read(users_path.clone()).by_age_except(current) {
if open.contains(&name) {
continue;
}
self.wait_closing(&paths::db_path(&self.data_dir, app_key, &name));
match delete_user_files(&self.data_dir, app_key, &name) {
Ok(()) => freed.push(name),
Err(err) => log::warn!("磁盘已满时删除其他用户的数据库失败:{err}"),
}
}
if !freed.is_empty() {
let mut users = Users::read(users_path);
if users.remove(&freed) {
if let Err(err) = users.write() {
log::warn!("更新 users.json 失败:{err}");
}
}
for name in &freed {
forget_db_key(&self.secure, app_key, name);
}
}
log::warn!("本地数据库写入时磁盘已满:删除了 {} 个其他用户的本地数据库", freed.len());
freed.len()
}
}
struct Users {
path: PathBuf,
items: Vec<Map<String, Value>>,
newer: bool,
}
impl Users {
fn read(path: PathBuf) -> Users {
let doc: Option<Value> = fs::read_to_string(&path).ok().and_then(|t| serde_json::from_str(&t).ok());
let Some(Value::Object(mut doc)) = doc else { return Users { path, items: Vec::new(), newer: false } };
if doc.get("v").and_then(Value::as_u64).unwrap_or(USERS_VERSION) > USERS_VERSION {
return Users { path, items: Vec::new(), newer: true };
}
let items = match doc.remove("items") {
Some(Value::Array(items)) => items
.into_iter()
.filter_map(|i| match i {
Value::Object(m) if m.get("username").is_some_and(Value::is_string) => Some(m),
_ => None,
})
.collect(),
_ => Vec::new(),
};
Users { path, items, newer: false }
}
fn find(&mut self, username: &str) -> Option<&mut Map<String, Value>> {
self.items.iter_mut().find(|i| i.get("username").and_then(Value::as_str) == Some(username))
}
fn encrypted(&mut self, username: &str) -> Option<bool> {
self.find(username)?.get("encrypted")?.as_bool()
}
fn touch(&mut self, username: &str, encrypted: bool, now: i64) -> bool {
let Some(item) = self.find(username) else {
let item = json!({ "username": username, "created_at": now, "last_used_at": now, "pending_delete": false, "encrypted": encrypted });
if let Value::Object(map) = item {
self.items.push(map);
}
return true;
};
let mut changed = false;
if item.get("encrypted").and_then(Value::as_bool) != Some(encrypted) {
item.insert("encrypted".into(), encrypted.into());
changed = true;
}
if item.get("pending_delete").and_then(Value::as_bool) == Some(true) {
item.insert("pending_delete".into(), false.into());
changed = true;
}
if now - item.get("last_used_at").and_then(Value::as_i64).unwrap_or(0) >= DAY_MS {
item.insert("last_used_at".into(), now.into());
changed = true;
}
changed
}
fn remove(&mut self, names: &[String]) -> bool {
let before = self.items.len();
self.items.retain(|i| !i.get("username").and_then(Value::as_str).is_some_and(|u| names.iter().any(|n| n == u)));
self.items.len() != before
}
fn by_age_except(&self, current: &str) -> Vec<String> {
let mut list: Vec<(i64, String)> = self
.items
.iter()
.filter_map(|i| Some((i.get("last_used_at").and_then(Value::as_i64).unwrap_or(0), i.get("username")?.as_str()?.to_string())))
.filter(|(_, u)| u != current)
.collect();
list.sort();
list.into_iter().map(|(_, u)| u).collect()
}
fn write(&self) -> Result<()> {
if self.newer {
return Ok(());
}
if self.items.is_empty() {
return match fs::remove_file(&self.path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::from_io(&e)),
};
}
let text = json!({ "v": USERS_VERSION, "items": self.items }).to_string();
write_unique_atomic(&self.path, text.as_bytes())
}
}
fn write_unique_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
static SEQ: AtomicU64 = AtomicU64::new(0);
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
let tmp = path.with_file_name(format!(".{name}.{}.{}.tmp", std::process::id(), SEQ.fetch_add(1, Ordering::Relaxed)));
let result = (|| {
let mut file = fs::File::create(&tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
fs::rename(&tmp, path)
})();
if let Err(e) = result {
let _ = fs::remove_file(&tmp);
return Err(Error::from_io(&e));
}
Ok(())
}
fn now_ms() -> i64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0)
}
fn load_keys(secure: &Secure, app_key: &str) -> Result<BTreeMap<String, String>> {
let values = secure.load(app_key)?;
let Some(text) = values.get("db_keys") else { return Ok(BTreeMap::new()) };
let doc: Value = serde_json::from_str(text).unwrap_or(Value::Null);
if doc.get("v").and_then(Value::as_u64) != Some(1) {
log::warn!("db_keys 的格式不认识,按没有处理");
return Ok(BTreeMap::new());
}
let keys = doc.get("keys").and_then(Value::as_object).map(|m| m.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect());
Ok(keys.unwrap_or_default())
}
fn save_keys(secure: &Secure, app_key: &str, keys: &BTreeMap<String, String>) -> Result<()> {
let value = if keys.is_empty() { None } else { Some(json!({ "v": 1, "keys": keys }).to_string()) };
secure.set(app_key, "db_keys", value)
}
#[cfg(feature = "sqlcipher")]
fn db_key(secure: &Secure, app_key: &str, username: &str, encrypt: bool) -> Result<Option<String>> {
use rand::RngCore;
if !encrypt {
return Ok(None);
}
let hash = paths::short_hash(username);
let mut keys = load_keys(secure, app_key)?;
if let Some(key) = keys.get(&hash) {
if key.len() == 64 && key.bytes().all(|b| b.is_ascii_hexdigit()) {
return Ok(Some(key.to_ascii_lowercase()));
}
}
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
let key = hex::encode(bytes);
keys.insert(hash, key.clone());
save_keys(secure, app_key, &keys)?;
Ok(Some(key))
}
#[cfg(not(feature = "sqlcipher"))]
fn db_key(_secure: &Secure, _app_key: &str, _username: &str, encrypt: bool) -> Result<Option<String>> {
if encrypt {
return Err(Error::unsupported("sqlcipher_required"));
}
Ok(None)
}
fn forget_db_key(secure: &Secure, app_key: &str, username: &str) {
let hash = paths::short_hash(username);
let result = load_keys(secure, app_key).and_then(|mut keys| if keys.remove(&hash).is_some() { save_keys(secure, app_key, &keys) } else { Ok(()) });
if let Err(err) = result {
log::warn!("删除数据库密钥失败:{err}");
}
}
pub(crate) fn sizes(data_dir: &Path, open: &HashSet<PathBuf>) -> Value {
let size = |p: &Path| fs::metadata(p).map(|m| m.len()).unwrap_or(0);
let mut out = Vec::new();
let Ok(apps) = fs::read_dir(data_dir) else { return Value::Array(out) };
for app in apps.flatten() {
if !app.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let Ok(files) = fs::read_dir(app.path()) else { continue };
for f in files.flatten() {
let path = f.path();
if path.extension().and_then(|e| e.to_str()) != Some("db") {
continue;
}
let mut wal = path.as_os_str().to_owned();
wal.push("-wal");
out.push(json!({
"file": format!("{}/{}", app.file_name().to_string_lossy(), f.file_name().to_string_lossy()),
"bytes": size(&path),
"wal_bytes": size(Path::new(&wal)),
"open": open.contains(&path),
}));
}
}
out.sort_by(|a, b| a["file"].as_str().cmp(&b["file"].as_str()));
Value::Array(out)
}