Files
rust-imager/src/main.rs
2026-07-21 02:17:46 +08:00

323 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

mod config;
mod models;
mod db;
mod engine;
mod cache;
mod routes;
mod services;
mod image_processor;
mod queue;
mod search_db;
use crate::cache::AlbumsCache;
use crate::search_db::SearchDb;
use actix_files as afs;
use actix_web::{App, HttpResponse, HttpServer, middleware, web};
use std::env;
use std::fs;
use std::sync::Arc;
use tokio::time::{Duration, interval};
use tracing::{error, info};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
let args: Vec<String> = env::args().collect();
let export_to_search = args.iter().any(|a| a == "--export-search");
let search_db_cli_path = args.iter().find(|a| a.starts_with("--search-db="))
.map(|s| s.strip_prefix("--search-db=").unwrap_or("search.db"))
.unwrap_or("search.db");
let clear_search = args.iter().any(|a| a == "--clear-search");
let wal_checkpoint = args.iter().any(|a| a == "--wal-checkpoint");
// 加载配置
let config = match config::Config::load() {
Ok(cfg) => cfg,
Err(e) => {
error!("加载配置失败: {}", e);
std::process::exit(1);
}
};
info!("启动图片展示服务 v2.0...");
info!("服务地址: http://{}", config.server_address());
// 初始化数据库
let db = match db::Database::new(&config).await {
Ok(db) => {
info!("数据库连接成功");
let db = Arc::new(db);
// 启动时先合并上次未完成的 WAL,确保数据库干净
info!("正在合并数据库 WAL...");
db.wal_checkpoint().await;
db
}
Err(e) => {
error!("数据库连接失败: {}", e);
std::process::exit(1);
}
};
if export_to_search {
info!("[搜索导出] 开始将元数据导出到 search.db...");
match services::search_export::export_to_search_db(&db, search_db_cli_path, clear_search).await {
Ok(result) => {
info!("[搜索导出] 导出完成: 相册{}, 标签{}, 人物{}, 别名{}, 关联{}",
result.albums_processed, result.tags_created, result.persons_created,
result.aliases_created, result.album_tags_created + result.album_persons_created);
std::process::exit(0);
}
Err(e) => {
error!("[搜索导出] 导出失败: {}", e);
std::process::exit(1);
}
}
}
if wal_checkpoint {
info!("[WAL合并] 主数据库 WAL checkpoint 已在初始化时完成");
info!("[WAL合并] 正在合并 search.db WAL...");
let search_db = SearchDb::new("data/search.db").await
.expect("search.db 连接失败");
search_db.wal_checkpoint().await;
info!("[WAL合并] 两个数据库均已合并完成");
std::process::exit(0);
}
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
// ── 初始化 search.db(运行时搜索专用)──
let search_db_path = "data/search.db";
let search_db = Arc::new(
SearchDb::new(search_db_path).await
.expect("search.db 连接失败")
);
info!("search.db 连接成功");
search_db.wal_checkpoint().await;
// ── 从 search.db 的 dict 表加载标签词典 ──
match engine::tag_dict::reload_from_search_db(&search_db).await {
msg => info!("{}", msg),
}
// ── 内存缓存(必须在管线任务之前创建)──
let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new()));
let persons_cache = Arc::new(tokio::sync::RwLock::new(cache::PersonCache::default()));
let tags_cache = Arc::new(tokio::sync::RwLock::new(cache::TagCache::default()));
// ── 启动管线(单次):导入 → 增量解析 → 条件全量 sync → 缓存刷新 ──
// 不做全量 reparse/merge,只处理新发现的相册
let boot_config = config.clone();
let boot_db = Arc::clone(&db);
let boot_search_db = Arc::clone(&search_db);
let boot_albums_cache = Arc::clone(&albums_cache);
let boot_persons_cache = Arc::clone(&persons_cache);
let boot_tags_cache = Arc::clone(&tags_cache);
tokio::spawn(async move {
// ① 扫描导入新相册
info!("[启动] ① 扫描导入新相册...");
match services::import::import_existing_data(&boot_config, Arc::clone(&boot_db), None).await {
Ok(_) => info!("[启动] 导入完成"),
Err(e) => error!("[启动] 导入失败: {}", e),
}
// ② 清理:删除空相册 + 去重
info!("[启动] ② 清理空相册和重复相册...");
let empty = engine::pipeline::phase_delete_empty_albums(&boot_db).await;
let dedup = engine::pipeline::phase_dedup_albums(&boot_db, &boot_search_db).await;
if empty > 0 || dedup > 0 {
info!("[启动] 清理完成:删除空相册 {},去重删除 {}", empty, dedup);
}
// ③ 查找新相册 / 半成品
info!("[启动] ③ 查找新相册...");
let dirty = engine::pipeline::phase_collect_new_albums(&boot_db).await;
let new_count = dirty.len();
if new_count > 0 {
info!("[启动] 发现 {} 个新相册,同步到 search.db...", new_count);
let synced = engine::pipeline::sync_dirty_albums(&boot_db, &boot_search_db, &dirty).await;
info!("[启动] 增量同步完成:{} 个相册(仅 tags/persons)", synced);
} else {
info!("[启动] 没有新相册");
}
// ④ 刷新所有内存缓存(从 search.db 读取)
info!("[启动] ④ 刷新所有内存缓存...");
cache::refresh_all_caches(&boot_db, &boot_search_db, &boot_albums_cache, &boot_persons_cache, &boot_tags_cache).await;
info!("[启动] 启动管线执行完毕(新相册={})", new_count);
});
// ── 定时管线(每 30 分钟):导入 → 增量解析 → 缓存刷新 ──
// 不做全量 reparse/merge
let timer_cache = Arc::clone(&albums_cache);
let timer_db = Arc::clone(&db);
let timer_search_db = Arc::clone(&search_db);
let timer_persons_cache = Arc::clone(&persons_cache);
let timer_tags_cache = Arc::clone(&tags_cache);
tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(30 * 60));
loop {
interval.tick().await;
// ① 扫描导入新相册
info!("[定时] ① 扫描导入新相册...");
match services::import::import_existing_data(&timer_db.config, Arc::clone(&timer_db), None).await {
Ok(_) => {},
Err(e) => error!("[定时] 导入失败: {}", e),
}
// ② 查找新相册
let new_albums = engine::pipeline::phase_collect_new_albums(&timer_db).await;
if new_albums.is_empty() {
continue; // 无新增,跳过
}
info!("[定时] 发现 {} 个新相册,增量解析...", new_albums.len());
engine::pipeline::phase_parse_dirty(&timer_db, &new_albums).await;
info!("[定时] 同步到 search.db...");
engine::pipeline::sync_dirty_albums(&timer_db, &timer_search_db, &new_albums).await;
// ③ 刷新缓存
info!("[定时] 刷新缓存...");
cache::refresh_all_caches(&timer_db, &timer_search_db, &timer_cache, &timer_persons_cache, &timer_tags_cache).await;
}
});
// 任务队列
let task_queue = Arc::new(queue::TaskQueue::new(
config.queue.max_concurrent_tasks,
config.queue.queue_size_limit,
config.queue.high_request_rate_threshold,
config.queue.idle_request_rate_threshold,
));
println!("API接口:");
println!(" - GET /api/albums - 获取所有相册");
println!(" - GET /api/search?q=关键词 - 搜索相册");
println!(" - GET /api/album/random - 随机相册");
println!(" - GET /api/album/:id - 获取相册详情");
println!(" - POST /api/albums - 创建新相册");
println!(" - GET /api/image/:id - 获取图片");
println!(" - GET /api/favorites - 收藏列表");
println!(" - POST /api/favorites - 添加收藏");
println!(" - DELETE /api/favorites/:id - 取消收藏");
println!("元数据接口:");
println!(" - GET /api/people - 人员列表");
println!(" - GET /api/people/search?q=关键词 - 搜索人员");
println!(" - GET /api/people/{{name}} - 人员详情");
println!(" - GET /api/people/{{id}}/aliases - 人员别名");
println!(" - GET /api/metadata/stats - 元数据统计");
println!(" - GET /api/tags/search?tag=标签 - 按标签搜索");
println!(" - GET /api/tags/popular?limit=50 - 热门标签");
println!("推荐接口:");
println!(" - GET /api/recommend/tags?q=xxx - 标签推荐");
println!(" - GET /api/recommend/persons?q=xxx - 人物推荐");
println!(" - GET /api/recommend/related?context=xxx - 关联推荐");
println!(" - GET /api/recommend/popular-persons - 热门人物");
println!("迁移接口:");
println!(" - GET /api/migration/stats - 迁移状态");
println!(" - POST /api/migration/run?path=persons.db - 执行迁移");
println!(" - GET /api/queue/stats - 队列状态");
println!("词典管理:");
println!(" - GET /api/dict/stats - 词典状态");
println!(" - POST /api/dict/reload - 热重载词典");
println!("按 Ctrl+C 停止服务");
fs::create_dir_all(&config.storage.images_dir).unwrap_or(());
// 在 move 闭包之前 clone 一份 db 用于关闭信号处理
let shutdown_db = Arc::clone(&db);
let server = HttpServer::new(move || {
App::new()
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.app_data(web::Data::new(Arc::clone(&db)))
.app_data(web::Data::new(Arc::clone(&search_db)))
.app_data(web::Data::new(Arc::clone(&image_processor)))
.app_data(web::Data::new(Arc::clone(&albums_cache)))
.app_data(web::Data::new(Arc::clone(&persons_cache)))
.app_data(web::Data::new(Arc::clone(&tags_cache)))
.app_data(web::Data::new(Arc::clone(&task_queue)))
// 相册 API
.route("/api/albums", web::get().to(routes::get_albums))
.route("/api/search", web::get().to(routes::search_albums))
.route("/api/album/random", web::get().to(routes::get_random_album))
.route("/api/album/{id}", web::get().to(routes::get_album_detail))
.route("/api/albums", web::post().to(routes::create_album))
.route("/api/image/{id}", web::get().to(routes::get_image))
// 收藏 API
.route("/api/favorites", web::get().to(routes::get_favorites))
.route("/api/favorites/status/{id}", web::get().to(routes::check_favorite_status))
.route("/api/favorites", web::post().to(routes::add_favorite))
.route("/api/favorites/{id}", web::delete().to(routes::remove_favorite))
.route("/api/favorites/image/{id}", web::delete().to(routes::remove_favorite_by_image))
// 人员 API
.route("/api/people", web::get().to(routes::get_people))
.route("/api/people/search", web::get().to(routes::search_people))
.route("/api/people/{name}", web::get().to(routes::get_person_detail))
.route("/api/people/{id}/aliases", web::get().to(routes::get_person_aliases))
// 标签 API
.route("/api/metadata/stats", web::get().to(routes::get_metadata_stats))
.route("/api/tags/search", web::get().to(routes::search_by_tag))
.route("/api/tags/popular", web::get().to(routes::get_popular_tags))
// 推荐 API
.route("/api/recommend/tags", web::get().to(routes::recommend_tags))
.route("/api/recommend/persons", web::get().to(routes::recommend_persons))
.route("/api/recommend/related", web::get().to(routes::recommend_related))
.route("/api/recommend/popular-persons", web::get().to(routes::popular_persons))
// 迁移 API
.route("/api/migration/stats", web::get().to(routes::migration_stats))
.route("/api/migration/run", web::post().to(routes::migration_run))
// 管线 API
.route("/api/pipeline/run", web::post().to(routes::pipeline_run))
// 队列 API
.route("/api/queue/stats", web::get().to(routes::get_queue_stats))
// 词典 API
.route("/api/dict/stats", web::get().to(routes::dict_stats))
.route("/api/dict/reload", web::post().to(routes::dict_reload))
// 静态文件
.service(
afs::Files::new("/assets", "./assets")
.show_files_listing()
.prefer_utf8(true)
.use_last_modified(true)
.use_etag(true),
)
.route("/", web::get().to(|| async {
match fs::read_to_string("./assets/index.html") {
Ok(content) => HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(content),
Err(_) => HttpResponse::NotFound().body("首页文件不存在"),
}
}))
.route("/album", web::get().to(|| async {
match fs::read_to_string("./assets/album.html") {
Ok(content) => HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(content),
Err(_) => HttpResponse::NotFound().body("相册页面不存在"),
}
}))
})
.bind(config.server_address())?
.run();
// ── 信号监听:捕获 Ctrl+C / SIGINT,优雅关闭 ──
let server_handle = server.handle();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.expect("监听关闭信号失败");
println!("\n[关闭] 收到关闭信号,正在优雅停止服务器...");
// 停止接受新请求,等待正在处理中的请求完成(最多 10s)
server_handle.stop(true).await;
// 服务器已停止 → 合并 WAL
println!("[关闭] 服务已停止,正在合并数据库 WAL...");
shutdown_db.wal_checkpoint().await;
println!("[关闭] 数据库文件已合并,再见!");
});
server.await
}