refactor: complete v0.8 core architecture

This commit is contained in:
thakares committed 2026-08-21 13:56:36 +05:30
1 parent f138a645f8
commit d7e0ac7679
98 files changed
+8413 -3044

No files matched your search

+345
View File
@@ -0,0 +1,345 @@
//! Frozen v0.8.0 database topology under a configurable physical root.
//!
//! The logical layout is:
//!
//! ```text
//! <data_dir>/
//! ├── admin/{admin,system,users}.db
//! ├── slugs/{global_urls,global_landing_pages,reserved}.db
//! └── users/<user-key>/{profile,content,analytics}.db
//! └── extensions/<extension>/<extension>.db
//! ```
//!
//! `<data_dir>` is `Config.data_dir` (env `DATA_DIR`, default `./data`).
//! It is not renamed to `database/`. Production Docker, CasaOS, and
//! `deploy.sh` bind this physical root.
use std::path::{Path, PathBuf};
use crate::identity::TenantId;
/// Directory name of the migration-era `legacy_admin` tenant (`users.db` id = 1).
pub const LEGACY_ADMIN_USER_KEY: &str = "1";
/// Frozen first-party extension names. There is no runtime plugin loader.
pub const FIRST_PARTY_EXTENSIONS: &[&str] = &["cv", "certificates", "documents", "portfolio"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TopologyError {
InvalidUserDir { name: String },
InvalidExtension { name: String },
}
impl std::fmt::Display for TopologyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidUserDir { name } => {
write!(f, "invalid tenant directory name: {name:?}")
}
Self::InvalidExtension { name } => {
write!(f, "invalid extension name: {name:?}")
}
}
}
}
impl std::error::Error for TopologyError {}
/// Authoritative resolver for every BZOD database path.
#[derive(Clone, Debug)]
pub struct Topology {
root: PathBuf,
}
impl Topology {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn admin_dir(&self) -> PathBuf {
self.root.join("admin")
}
pub fn slugs_dir(&self) -> PathBuf {
self.root.join("slugs")
}
pub fn users_dir(&self) -> PathBuf {
self.root.join("users")
}
pub fn admin_db(&self) -> PathBuf {
self.admin_dir().join("admin.db")
}
pub fn system_db(&self) -> PathBuf {
self.admin_dir().join("system.db")
}
pub fn users_registry_db(&self) -> PathBuf {
self.admin_dir().join("users.db")
}
pub fn global_urls_db(&self) -> PathBuf {
self.slugs_dir().join("global_urls.db")
}
pub fn global_landing_pages_db(&self) -> PathBuf {
self.slugs_dir().join("global_landing_pages.db")
}
pub fn reserved_db(&self) -> PathBuf {
self.slugs_dir().join("reserved.db")
}
/// Pre-multi-tenant files that may still exist at the physical root.
pub fn legacy_flat_admin_db(&self) -> PathBuf {
self.root.join("admin.db")
}
pub fn legacy_flat_system_db(&self) -> PathBuf {
self.root.join("system.db")
}
pub fn legacy_flat_users_db(&self) -> PathBuf {
self.root.join("users.db")
}
pub fn legacy_flat_content_db(&self) -> PathBuf {
self.root.join("content.db")
}
pub fn legacy_flat_analytics_db(&self) -> PathBuf {
self.root.join("analytics.db")
}
pub fn legacy_admin_dir(&self) -> PathBuf {
self.users_dir().join(LEGACY_ADMIN_USER_KEY)
}
/// Frozen tenant directory: `users/<12-hex-TenantId>/`.
pub fn tenant_dir(&self, tenant_id: TenantId) -> PathBuf {
self.users_dir().join(tenant_id.as_str())
}
pub fn tenant_content_db(&self, tenant_id: TenantId) -> PathBuf {
self.tenant_dir(tenant_id).join("content.db")
}
pub fn tenant_analytics_db(&self, tenant_id: TenantId) -> PathBuf {
self.tenant_dir(tenant_id).join("analytics.db")
}
pub fn tenant_profile_db(&self, tenant_id: TenantId) -> PathBuf {
self.tenant_dir(tenant_id).join("profile.db")
}
pub fn user_dir(&self, user_key: &str) -> Result<PathBuf, TopologyError> {
if !is_valid_user_dir_name(user_key) {
return Err(TopologyError::InvalidUserDir {
name: user_key.to_string(),
});
}
Ok(self.users_dir().join(user_key))
}
pub fn user_dir_i64(&self, user_id: i64) -> Result<PathBuf, TopologyError> {
self.user_dir(&user_id.to_string())
}
pub fn content_db(&self, user_key: &str) -> Result<PathBuf, TopologyError> {
Ok(self.user_dir(user_key)?.join("content.db"))
}
pub fn analytics_db(&self, user_key: &str) -> Result<PathBuf, TopologyError> {
Ok(self.user_dir(user_key)?.join("analytics.db"))
}
pub fn profile_db(&self, user_key: &str) -> Result<PathBuf, TopologyError> {
Ok(self.user_dir(user_key)?.join("profile.db"))
}
pub fn content_db_i64(&self, user_id: i64) -> Result<PathBuf, TopologyError> {
self.content_db(&user_id.to_string())
}
pub fn analytics_db_i64(&self, user_id: i64) -> Result<PathBuf, TopologyError> {
self.analytics_db(&user_id.to_string())
}
pub fn profile_db_i64(&self, user_id: i64) -> Result<PathBuf, TopologyError> {
self.profile_db(&user_id.to_string())
}
pub fn extensions_dir(&self, user_key: &str) -> Result<PathBuf, TopologyError> {
Ok(self.user_dir(user_key)?.join("extensions"))
}
pub fn extensions_dir_i64(&self, user_id: i64) -> Result<PathBuf, TopologyError> {
self.extensions_dir(&user_id.to_string())
}
pub fn extension_db(&self, user_key: &str, extension: &str) -> Result<PathBuf, TopologyError> {
if !is_valid_extension_name(extension) {
return Err(TopologyError::InvalidExtension {
name: extension.to_string(),
});
}
Ok(self
.extensions_dir(user_key)?
.join(extension)
.join(format!("{extension}.db")))
}
/// Create `admin/`, `slugs/`, and `users/` under the physical root.
pub fn ensure_core_dirs(&self) -> std::io::Result<()> {
std::fs::create_dir_all(self.admin_dir())?;
std::fs::create_dir_all(self.slugs_dir())?;
std::fs::create_dir_all(self.users_dir())?;
Ok(())
}
/// Create a tenant directory and its `extensions/` folder.
pub fn ensure_user_dirs(&self, user_key: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
let dir = self.user_dir(user_key)?;
std::fs::create_dir_all(&dir)?;
std::fs::create_dir_all(dir.join("extensions"))?;
Ok(dir)
}
pub fn ensure_user_dirs_i64(
&self,
user_id: i64,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
self.ensure_user_dirs(&user_id.to_string())
}
}
/// Tenant directory names: 12 lowercase hex (v0.8) or a positive decimal id (v0.7).
pub fn is_valid_user_dir_name(name: &str) -> bool {
if name.is_empty() || name == "." || name == ".." {
return false;
}
if name
.as_bytes()
.iter()
.any(|b| *b == b'/' || *b == b'\\' || *b == 0 || *b == b'.')
{
return false;
}
if is_v08_user_id(name) {
return true;
}
is_legacy_integer_user_id(name)
}
pub fn is_v08_user_id(name: &str) -> bool {
name.len() == 12 && name.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
pub fn is_legacy_integer_user_id(name: &str) -> bool {
if name.is_empty() || name.as_bytes()[0] == b'0' {
return false;
}
name.bytes().all(|b| b.is_ascii_digit()) && name.parse::<i64>().map(|n| n > 0).unwrap_or(false)
}
/// First-party extension directory names: `^[a-z][a-z0-9_]{0,31}$`.
pub fn is_valid_extension_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_lowercase() => {}
_ => return false,
}
name.len() <= 32
&& name
.bytes()
.all(|b| matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'_'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn physical_root_is_not_renamed_to_database() {
let t = Topology::new("/var/lib/bzod/data");
assert_eq!(t.root(), Path::new("/var/lib/bzod/data"));
assert!(t.admin_dir().ends_with("data/admin"));
assert!(t.slugs_dir().ends_with("data/slugs"));
assert!(t.users_dir().ends_with("data/users"));
assert!(!t.root().ends_with("database"));
}
#[test]
fn frozen_core_paths() {
let t = Topology::new("/app/data");
assert_eq!(t.admin_db(), PathBuf::from("/app/data/admin/admin.db"));
assert_eq!(t.system_db(), PathBuf::from("/app/data/admin/system.db"));
assert_eq!(
t.users_registry_db(),
PathBuf::from("/app/data/admin/users.db")
);
assert_eq!(
t.global_urls_db(),
PathBuf::from("/app/data/slugs/global_urls.db")
);
assert_eq!(
t.global_landing_pages_db(),
PathBuf::from("/app/data/slugs/global_landing_pages.db")
);
assert_eq!(
t.reserved_db(),
PathBuf::from("/app/data/slugs/reserved.db")
);
}
#[test]
fn tenant_paths_legacy_integer_and_v08_hex() {
let t = Topology::new("/app/data");
assert_eq!(
t.content_db_i64(2).unwrap(),
PathBuf::from("/app/data/users/2/content.db")
);
let tid = crate::identity::TenantId::parse("a1b2c3d4e5f6").unwrap();
assert_eq!(
t.tenant_content_db(tid),
PathBuf::from("/app/data/users/a1b2c3d4e5f6/content.db")
);
assert_eq!(
t.extension_db("a1b2c3d4e5f6", "cv").unwrap(),
PathBuf::from("/app/data/users/a1b2c3d4e5f6/extensions/cv/cv.db")
);
}
#[test]
fn rejects_path_traversal_and_forged_names() {
let t = Topology::new("/app/data");
assert!(t.user_dir("..").is_err());
assert!(t.user_dir("../2").is_err());
assert!(t.user_dir("2/../3").is_err());
assert!(t.user_dir("2/foo").is_err());
assert!(t.user_dir("-1").is_err());
assert!(t.user_dir("0").is_err());
assert!(t.user_dir("01").is_err());
assert!(t.user_dir("").is_err());
assert!(t.user_dir("ABCDEFABCDEF").is_err());
assert!(t.content_db_i64(-5).is_err());
assert!(t.content_db_i64(0).is_err());
assert!(t.extension_db("2", "../cv").is_err());
assert!(t.extension_db("2", "cv/db").is_err());
assert!(t.extension_db("2", "").is_err());
assert!(t.extension_db("2", "CV").is_err());
}
#[test]
fn first_party_extension_names_are_valid() {
for name in FIRST_PARTY_EXTENSIONS {
assert!(is_valid_extension_name(name), "{name}");
}
}
}