209 lines
7.9 KiB
Rust
209 lines
7.9 KiB
Rust
mod config;
|
||
mod database;
|
||
mod image_processor;
|
||
mod import;
|
||
mod models;
|
||
mod routes;
|
||
mod queue;
|
||
|
||
use actix_files as afs;
|
||
use actix_web::{App, HttpResponse, HttpServer, web, middleware};
|
||
use std::fs;
|
||
use std::sync::Arc;
|
||
use tokio::time::{interval, Duration};
|
||
use tracing::{info, error};
|
||
use crate::models::AlbumsCache;
|
||
|
||
#[actix_web::main]
|
||
async fn main() -> std::io::Result<()> {
|
||
// 初始化结构化日志
|
||
tracing_subscriber::fmt::init();
|
||
|
||
// 加载配置
|
||
let config = match config::Config::load() {
|
||
Ok(cfg) => cfg,
|
||
Err(e) => {
|
||
error!("加载配置失败: {}", e);
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
|
||
info!("启动图片展示服务...");
|
||
info!("服务地址: http://{}", config.server_address());
|
||
|
||
// 初始化数据库
|
||
let db = match database::Database::new(&config).await {
|
||
Ok(db) => {
|
||
info!("数据库连接成功");
|
||
Arc::new(db)
|
||
}
|
||
Err(e) => {
|
||
error!("数据库连接失败: {}", e);
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
|
||
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
|
||
|
||
// 初始化智能任务队列
|
||
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, // 空闲请求频率阈值
|
||
));
|
||
|
||
// 在后台线程中自动导入新相册(不阻塞HTTP服务启动)
|
||
let config_clone = config.clone();
|
||
let db_clone = Arc::clone(&db);
|
||
let task_queue_clone = Arc::clone(&task_queue);
|
||
tokio::spawn(async move {
|
||
info!("[后台任务] 开始扫描图片目录并导入新相册...");
|
||
match import::import_existing_data(&config_clone, db_clone, Some(task_queue_clone)).await {
|
||
Ok(_) => info!("[后台任务] 相册导入完成"),
|
||
Err(e) => error!("[后台任务] 相册导入失败: {}", e),
|
||
}
|
||
});
|
||
|
||
// 启动缩略图生成Worker
|
||
let thumbnail_worker = queue::ThumbnailWorker::new(
|
||
Arc::clone(&task_queue),
|
||
Arc::clone(&image_processor),
|
||
);
|
||
tokio::spawn(async move {
|
||
thumbnail_worker.start().await;
|
||
});
|
||
|
||
// 启动相册导入Worker
|
||
let album_import_worker = queue::AlbumImportWorker::new(
|
||
Arc::clone(&task_queue),
|
||
Arc::clone(&db),
|
||
config.storage.clone(),
|
||
);
|
||
tokio::spawn(async move {
|
||
album_import_worker.start().await;
|
||
});
|
||
|
||
// 初始化相册内存缓存
|
||
let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new()));
|
||
|
||
// 初始加载相册数据到缓存
|
||
let initial_cache = Arc::clone(&albums_cache);
|
||
let initial_db = Arc::clone(&db);
|
||
tokio::spawn(async move {
|
||
match initial_db.get_all_albums().await {
|
||
Ok(albums) => {
|
||
let mut cache = initial_cache.write().await;
|
||
cache.update(albums);
|
||
println!("✅ 初始相册缓存加载完成");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("❌ 初始缓存加载失败: {}", e);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 设置定时刷新缓存任务(每30分钟)
|
||
let refresh_cache = Arc::clone(&albums_cache);
|
||
let refresh_db = Arc::clone(&db);
|
||
tokio::spawn(async move {
|
||
let mut interval = interval(Duration::from_secs(30 * 60)); // 30分钟
|
||
|
||
loop {
|
||
interval.tick().await;
|
||
|
||
match refresh_db.get_all_albums().await {
|
||
Ok(albums) => {
|
||
let mut cache = refresh_cache.write().await;
|
||
cache.update(albums);
|
||
println!("🔄 相册缓存定时刷新完成");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("❌ 缓存刷新失败: {}", e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
println!("API接口:");
|
||
println!(" - GET /api/albums - 获取所有相册");
|
||
println!(" - GET /api/search?q=关键词 - 搜索相册");
|
||
println!(" - GET /api/album/:id - 获取相册详情");
|
||
println!(" - POST /api/albums - 创建新相册");
|
||
println!(" - GET /api/thumbnail/:id - 获取缩略图");
|
||
println!(" - GET /api/original/:id - 获取原始图片");
|
||
println!(" - GET /api/favorites - 获取收藏列表");
|
||
println!(" - GET /api/favorites/status/:id - 检查收藏状态");
|
||
println!(" - POST /api/favorites - 添加收藏");
|
||
println!(" - DELETE /api/favorites/:id - 取消收藏");
|
||
println!(" - DELETE /api/favorites/image/:id - 通过图片ID取消收藏");
|
||
println!("按 Ctrl+C 停止服务");
|
||
|
||
// 创建数据目录
|
||
fs::create_dir_all(&config.storage.images_dir).unwrap_or(());
|
||
fs::create_dir_all(&config.storage.thumbnails_dir).unwrap_or(());
|
||
fs::create_dir_all(&config.database.url[7..]).unwrap_or(());
|
||
|
||
// 在闭包外提取需要的路径
|
||
let server_address = config.server_address();
|
||
|
||
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(&image_processor)))
|
||
.app_data(web::Data::new(Arc::clone(&albums_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/thumbnail/{id}", web::get().to(routes::get_thumbnail))
|
||
.route("/api/original/{id}", web::get().to(routes::get_original_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/queue/stats", web::get().to(routes::get_queue_stats))
|
||
// 静态文件服务 - 启用缓存
|
||
.service(afs::Files::new("/assets", "./assets")
|
||
.show_files_listing()
|
||
.prefer_utf8(true)
|
||
.use_last_modified(true)
|
||
.use_etag(true))
|
||
// HTML页面路由
|
||
.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(server_address)?
|
||
.run()
|
||
.await
|
||
} |