feat(db): 内容库迁入 SQLite,分类与侧栏导航改为数据库驱动
This commit is contained in:
@@ -17,6 +17,7 @@ use tower_http::{
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::db::Db;
|
||||
use crate::error::StashError;
|
||||
use crate::listing::{AppProject, scan_projects};
|
||||
use crate::watcher;
|
||||
@@ -33,6 +34,8 @@ const HTML_CACHE: &str = "no-cache";
|
||||
pub struct AppState {
|
||||
pub config: Arc<Config>,
|
||||
pub project_updates: broadcast::Sender<Vec<AppProject>>,
|
||||
/// 内容库:分类 / 分组 / 条目的唯一数据源
|
||||
pub db: Arc<Db>,
|
||||
}
|
||||
|
||||
pub struct ServerHandle {
|
||||
@@ -138,6 +141,77 @@ async fn list_projects_api(State(state): State<AppState>) -> impl IntoResponse {
|
||||
}))
|
||||
}
|
||||
|
||||
/// API: 完整内容目录(分类 / 分组 / 条目 + 侧栏导航)
|
||||
///
|
||||
/// 数据量很小(几十 KB),一次全量下发,前端搜索仍然在客户端做。
|
||||
async fn catalog_api(State(state): State<AppState>) -> Response {
|
||||
let db = state.db.clone();
|
||||
|
||||
// rusqlite 是同步的,查询扔进阻塞线程池,别占住 async 执行器
|
||||
match tokio::task::spawn_blocking(move || db.load_catalog()).await {
|
||||
Ok(Ok(catalog)) => Json(serde_json::json!({
|
||||
"categories": catalog.categories,
|
||||
"nav": catalog.nav,
|
||||
}))
|
||||
.into_response(),
|
||||
Ok(Err(error)) => {
|
||||
// 典型原因是用户手改坏了 JSON,错误信息里已带条目 id
|
||||
warn!("读取内容库失败: {error}");
|
||||
error.into_response()
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("内容库查询任务崩溃: {error}");
|
||||
StashError::Database(format!("内容库查询任务崩溃: {error}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SSE:内容库被外部修改时通知前端重新拉取。
|
||||
///
|
||||
/// 这里轮询 `PRAGMA data_version` 而不是监听数据库文件:
|
||||
/// SQLite 在 journal 模式下会产生临时文件的频繁读写,文件监听会误报,
|
||||
/// 而 data_version 恰好在「其它连接提交了事务」时才变化,语义正好对上。
|
||||
async fn catalog_events(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let db = state.db.clone();
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
let mut last = db.data_version().unwrap_or(0);
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
|
||||
let current = match db.data_version() {
|
||||
Ok(version) => version,
|
||||
Err(error) => {
|
||||
warn!("读取内容库版本失败: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 只在其它连接提交之后才推送。
|
||||
// 当前服务对内容库是只读的;将来若加入写接口,必须在那里主动广播,
|
||||
// 因为 data_version 不会因本连接自己的提交而变化。
|
||||
tracing::debug!(last, current, "内容库版本检查");
|
||||
if current == last {
|
||||
continue;
|
||||
}
|
||||
last = current;
|
||||
|
||||
// 只告诉前端「变了」,具体内容由它重新 GET /api/catalog
|
||||
yield Ok::<Event, Infallible>(
|
||||
Event::default().event("catalog").data(r#"{"changed":true}"#),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Sse::new(stream).keep_alive(
|
||||
KeepAlive::new()
|
||||
.interval(Duration::from_secs(15))
|
||||
.text("keep-alive"),
|
||||
)
|
||||
}
|
||||
|
||||
/// SSE:先发送当前快照,再持续推送文件监听产生的项目列表变化。
|
||||
async fn project_events(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let root = state.config.root.clone();
|
||||
@@ -188,9 +262,13 @@ pub async fn start(config: Config) -> Result<ServerHandle, StashError> {
|
||||
let (project_updates, _) = broadcast::channel(32);
|
||||
watcher::start(config.root.clone(), project_updates.clone())?;
|
||||
|
||||
// 内容库在启动时打开:建库 / 建表 / 空库时灌种子都在这里完成
|
||||
let db = Arc::new(Db::open(&config)?);
|
||||
|
||||
let shared_state = AppState {
|
||||
config: Arc::new(config.clone()),
|
||||
project_updates,
|
||||
db,
|
||||
};
|
||||
|
||||
// 托管目录里找不到的路径,交给 404 页面而不是 SPA 回落:
|
||||
@@ -205,6 +283,8 @@ pub async fn start(config: Config) -> Result<ServerHandle, StashError> {
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/projects", get(list_projects_api))
|
||||
.route("/api/projects/events", get(project_events))
|
||||
.route("/api/catalog", get(catalog_api))
|
||||
.route("/api/catalog/events", get(catalog_events))
|
||||
.route("/", get(web_index))
|
||||
.route("/index.html", get(web_index))
|
||||
.route("/favicon.svg", get(web_root_file))
|
||||
|
||||
Reference in New Issue
Block a user