refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pub mod oauth_repo;
|
||||
pub mod session_repo;
|
||||
@@ -0,0 +1,52 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! Filesystem-backed `OAuthRepository` implementation.
|
||||
//!
|
||||
//! Tokens are stored as a single JSON file. Writes use a write-then-rename
|
||||
//! + fsync pattern for crash safety.
|
||||
use std::path::Path;
|
||||
|
||||
use crate::domain::oauth::OAuthToken;
|
||||
use crate::domain::repository::OAuthRepository;
|
||||
|
||||
/// Concrete filesystem OAuth token repository.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileSystemOAuthRepository;
|
||||
|
||||
impl FileSystemOAuthRepository {
|
||||
/// Create a new filesystem OAuth repository.
|
||||
pub fn new() -> Self {
|
||||
FileSystemOAuthRepository
|
||||
}
|
||||
}
|
||||
|
||||
impl OAuthRepository for FileSystemOAuthRepository {
|
||||
fn save_token(&self, path: &Path, token: &OAuthToken) -> anyhow::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let data = serde_json::to_string_pretty(token)?;
|
||||
let tmp = path.with_extension("tmp");
|
||||
std::fs::write(&tmp, data)?;
|
||||
let f = std::fs::File::open(&tmp)?;
|
||||
f.sync_all()?;
|
||||
std::fs::rename(&tmp, path)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::File::open(parent).and_then(|d| d.sync_all());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_token(&self, path: &Path) -> anyhow::Result<Option<OAuthToken>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
let token: OAuthToken = serde_json::from_str(&data)?;
|
||||
Ok(Some(token))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! Filesystem-backed `SessionRepository` implementation.
|
||||
//!
|
||||
//! Each session is stored as `<base_dir>/sessions/<id>/session.json`.
|
||||
//! Writes use a write-then-rename + fsync pattern for crash safety.
|
||||
use std::path::Path;
|
||||
|
||||
use crate::domain::repository::SessionRepository;
|
||||
use crate::domain::session::Session;
|
||||
|
||||
/// Concrete filesystem session repository.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileSystemSessionRepository;
|
||||
|
||||
impl FileSystemSessionRepository {
|
||||
/// Create a new filesystem session repository.
|
||||
pub fn new() -> Self {
|
||||
FileSystemSessionRepository
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionRepository for FileSystemSessionRepository {
|
||||
fn list_sessions(&self, base_dir: &Path) -> anyhow::Result<Vec<Session>> {
|
||||
let sessions_dir = base_dir.join("sessions");
|
||||
let Ok(entries) = std::fs::read_dir(&sessions_dir) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut sessions = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
if !entry.path().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let id = entry.file_name().to_string_lossy().to_string();
|
||||
if let Ok(session) = self.load_session(base_dir, &id) {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn load_session(&self, base_dir: &Path, id: &str) -> anyhow::Result<Session> {
|
||||
// Directory-traversal prevention.
|
||||
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
||||
anyhow::bail!(
|
||||
"invalid session id '{id}': must not contain path separators"
|
||||
);
|
||||
}
|
||||
let path = base_dir.join("sessions").join(id).join("session.json");
|
||||
if !path.exists() {
|
||||
anyhow::bail!("session not found: {id}");
|
||||
}
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let session: Session = serde_json::from_str(&data)?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn save_session(&self, base_dir: &Path, session: &Session) -> anyhow::Result<()> {
|
||||
let dir = session.session_dir(base_dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("session.json");
|
||||
let data = serde_json::to_string_pretty(session)?;
|
||||
let tmp = dir.join("session.json.tmp");
|
||||
std::fs::write(&tmp, data)?;
|
||||
// fsync before rename ensures the data is on disk.
|
||||
let f = std::fs::File::open(&tmp)?;
|
||||
f.sync_all()?;
|
||||
std::fs::rename(&tmp, path)?;
|
||||
// fsync the parent directory so the rename survives a crash.
|
||||
if let Some(parent) = dir.parent() {
|
||||
let _ = std::fs::File::open(parent).and_then(|d| d.sync_all());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_session(&self, base_dir: &Path, id: &str) -> anyhow::Result<()> {
|
||||
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
||||
anyhow::bail!(
|
||||
"invalid session id '{id}': must not contain path separators"
|
||||
);
|
||||
}
|
||||
let dir = base_dir.join("sessions").join(id);
|
||||
if dir.exists() {
|
||||
std::fs::remove_dir_all(&dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user