update code level
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1236,6 +1236,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -17,4 +17,5 @@ reqwest = { version = "0.11", default-features = false, features = ["blocking","
|
||||
config = "0.13"
|
||||
rand = "0.8"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber = "0.3"
|
||||
thiserror = "1.0"
|
||||
@@ -8,3 +8,9 @@ port = 8080
|
||||
[storage]
|
||||
images_dir = "./albums"
|
||||
thumbnails_dir = "./thumbnails"
|
||||
|
||||
[queue]
|
||||
max_concurrent_tasks = 6
|
||||
queue_size_limit = 200
|
||||
high_request_rate_threshold = 200.0
|
||||
idle_request_rate_threshold = 10.0
|
||||
@@ -7,6 +7,7 @@ pub struct Config {
|
||||
pub database: DatabaseConfig,
|
||||
pub server: ServerConfig,
|
||||
pub storage: StorageConfig,
|
||||
pub queue: QueueConfig,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +28,14 @@ pub struct StorageConfig {
|
||||
pub thumbnails_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct QueueConfig {
|
||||
pub max_concurrent_tasks: usize,
|
||||
pub queue_size_limit: usize,
|
||||
pub high_request_rate_threshold: f64,
|
||||
pub idle_request_rate_threshold: f64,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, config::ConfigError> {
|
||||
let config = config::Config::builder()
|
||||
|
||||
@@ -3,6 +3,30 @@ use std::fs;
|
||||
use image::{GenericImageView, ImageFormat};
|
||||
use uuid::Uuid;
|
||||
use crate::config::StorageConfig;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ImageProcessorError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Image processing error: {0}")]
|
||||
Image(#[from] image::ImageError),
|
||||
#[error("Invalid image data")]
|
||||
InvalidData,
|
||||
}
|
||||
|
||||
// 手动实现 From<reqwest::Error> 以支持 ? 操作符
|
||||
impl From<reqwest::Error> for ImageProcessorError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ImageProcessorError::Io(std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageProcessorError {
|
||||
pub fn is_send(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageProcessor {
|
||||
pub config: StorageConfig,
|
||||
@@ -17,7 +41,7 @@ impl ImageProcessor {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub async fn save_image(&self, image_data: &[u8], filename: &str) -> Result<ImageInfo, Box<dyn std::error::Error>> {
|
||||
pub async fn save_image(&self, image_data: &[u8], filename: &str) -> Result<ImageInfo, ImageProcessorError> {
|
||||
let image_id = Uuid::new_v4();
|
||||
let file_extension = Path::new(filename)
|
||||
.extension()
|
||||
@@ -56,7 +80,7 @@ impl ImageProcessor {
|
||||
output_path: &Path,
|
||||
_max_width: u32,
|
||||
_max_height: u32,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<(), ImageProcessorError> {
|
||||
// 使用统一的缩略图尺寸(400x300,保持宽高比)
|
||||
let thumbnail = img.thumbnail(400, 300);
|
||||
|
||||
@@ -81,7 +105,7 @@ impl ImageProcessor {
|
||||
}
|
||||
|
||||
/// 快速生成缩略图(低质量但快速)
|
||||
pub async fn generate_fast_thumbnail(&self, image_data: &[u8], _filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
pub async fn generate_fast_thumbnail(&self, image_data: &[u8], _filename: &str) -> Result<Vec<u8>, ImageProcessorError> {
|
||||
// 使用快速解码和缩放
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
@@ -130,7 +154,7 @@ impl ImageProcessor {
|
||||
&self,
|
||||
url: &str,
|
||||
filename: &str,
|
||||
) -> Result<ImageInfo, Box<dyn std::error::Error>> {
|
||||
) -> Result<ImageInfo, ImageProcessorError> {
|
||||
let response = reqwest::get(url).await?;
|
||||
let image_data = response.bytes().await?;
|
||||
|
||||
@@ -142,7 +166,7 @@ impl ImageProcessor {
|
||||
&self,
|
||||
image_data: &[u8],
|
||||
thumbnail_filename: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<(), ImageProcessorError> {
|
||||
// 使用统一的缩略图生成方法
|
||||
let thumbnail_data = self.generate_thumbnail_from_memory(image_data, thumbnail_filename).await?;
|
||||
|
||||
@@ -161,7 +185,7 @@ impl ImageProcessor {
|
||||
&self,
|
||||
image_data: &[u8],
|
||||
_thumbnail_filename: &str,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
) -> Result<Vec<u8>, ImageProcessorError> {
|
||||
// 加载图片
|
||||
let img = image::load_from_memory(image_data)?;
|
||||
|
||||
|
||||
@@ -54,13 +54,8 @@ async fn batch_insert_images_and_set_cover(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AlbumIndex {
|
||||
title: String,
|
||||
images: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn import_existing_data(config: &Config, db: Arc<Database>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn import_existing_data(config: &Config, db: Arc<Database>, task_queue: Option<Arc<crate::queue::TaskQueue>>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("开始扫描图片目录并导入新相册...");
|
||||
|
||||
let albums_dir = Path::new(&config.storage.images_dir);
|
||||
@@ -147,32 +142,62 @@ pub async fn import_existing_data(config: &Config, db: Arc<Database>) -> Result<
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 第二阶段:串行处理相册导入,避免数据库死锁
|
||||
// 对于SQLite,高并发写入容易导致死锁,改为串行处理
|
||||
let mut total_imported = 0;
|
||||
let mut successful_albums = 0;
|
||||
|
||||
for (folder_name, path) in album_folders {
|
||||
println!("📁 开始导入相册: {}", folder_name);
|
||||
// 如果有队列系统,使用队列异步处理相册导入
|
||||
if let Some(queue) = task_queue {
|
||||
println!("🚀 使用队列系统异步导入相册...");
|
||||
|
||||
match import_single_album(&db, &folder_name, &path).await {
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
successful_albums += 1;
|
||||
total_imported += count;
|
||||
println!("✅ 相册 {} 导入完成,共导入 {} 张图片", folder_name, count);
|
||||
} else {
|
||||
println!("⚠️ 相册 {} 没有图片需要导入", folder_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 相册 {} 导入失败: {}", folder_name, e);
|
||||
// 继续处理下一个相册,不中断整个导入过程
|
||||
for (folder_name, path) in album_folders {
|
||||
println!("📁 添加相册导入任务: {}", folder_name);
|
||||
|
||||
// 创建相册导入任务
|
||||
let relative_path = path.strip_prefix(&config.storage.images_dir)
|
||||
.unwrap_or(&path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let task = crate::queue::TaskFactory::create_album_import_task(
|
||||
relative_path,
|
||||
None,
|
||||
);
|
||||
|
||||
// 添加任务到队列
|
||||
if let Err(e) = queue.add_task(task).await {
|
||||
eprintln!("❌ 添加相册导入任务失败: {}", e);
|
||||
} else {
|
||||
println!("✅ 相册导入任务已添加到队列: {}", folder_name);
|
||||
}
|
||||
}
|
||||
|
||||
println!("🎉 所有相册导入任务已添加到队列,将由AlbumImportWorker异步处理");
|
||||
} else {
|
||||
// 如果没有队列系统,使用原来的串行处理方式
|
||||
println!("⚠️ 队列系统不可用,使用串行处理方式导入相册...");
|
||||
|
||||
let mut total_imported = 0;
|
||||
let mut successful_albums = 0;
|
||||
|
||||
for (folder_name, path) in album_folders {
|
||||
println!("📁 开始导入相册: {}", folder_name);
|
||||
|
||||
match import_single_album(&db, &folder_name, &path).await {
|
||||
Ok(count) => {
|
||||
if count > 0 {
|
||||
successful_albums += 1;
|
||||
total_imported += count;
|
||||
println!("✅ 相册 {} 导入完成,共导入 {} 张图片", folder_name, count);
|
||||
} else {
|
||||
println!("⚠️ 相册 {} 没有图片需要导入", folder_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ 相册 {} 导入失败: {}", folder_name, e);
|
||||
// 继续处理下一个相册,不中断整个导入过程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("🎉 导入完成!成功导入 {} 个相册,共 {} 张图片", successful_albums, total_imported);
|
||||
}
|
||||
|
||||
println!("🎉 导入完成!成功导入 {} 个相册,共 {} 张图片", successful_albums, total_imported);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -308,7 +333,7 @@ async fn import_single_album(db: &Database, folder_name: &str, path: &Path) -> R
|
||||
}
|
||||
|
||||
/// 快速扫描图片文件
|
||||
fn scan_image_files(path: &Path, _folder_name: &str) -> Result<Vec<(String, std::path::PathBuf)>, Box<dyn std::error::Error>> {
|
||||
pub(crate) fn scan_image_files(path: &Path, _folder_name: &str) -> Result<Vec<(String, std::path::PathBuf)>, Box<dyn std::error::Error>> {
|
||||
let mut image_files = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(path)? {
|
||||
@@ -483,7 +508,7 @@ async fn generate_thumbnail_for_image(
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Err(format!("缩略图生成失败: {}", e).into())
|
||||
Err(format!("缩略图生成失败: {:?}", e).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/main.rs
39
src/main.rs
@@ -4,13 +4,14 @@ 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, warn, debug};
|
||||
use tracing::{info, error};
|
||||
use crate::models::AlbumsCache;
|
||||
|
||||
#[actix_web::main]
|
||||
@@ -42,19 +43,46 @@ async fn main() -> std::io::Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
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).await {
|
||||
match import::import_existing_data(&config_clone, db_clone, Some(task_queue_clone)).await {
|
||||
Ok(_) => info!("[后台任务] 相册导入完成"),
|
||||
Err(e) => error!("[后台任务] 相册导入失败: {}", e),
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化图片处理器
|
||||
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
|
||||
// 启动缩略图生成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()));
|
||||
@@ -128,6 +156,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.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))
|
||||
@@ -142,6 +171,8 @@ async fn main() -> std::io::Result<()> {
|
||||
.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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
use tracing::{info, error, warn, debug};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Album {
|
||||
@@ -94,7 +94,11 @@ pub struct FavoriteStatus {
|
||||
pub is_favorited: bool,
|
||||
pub favorite_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AlbumIndex {
|
||||
pub title: String,
|
||||
pub images: Vec<String>,
|
||||
}
|
||||
// 内存缓存结构
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AlbumsCache {
|
||||
@@ -121,43 +125,47 @@ impl AlbumsCache {
|
||||
pub fn update(&mut self, albums: Vec<AlbumInfo>) {
|
||||
self.albums = albums.clone();
|
||||
self.last_updated = chrono::Utc::now();
|
||||
|
||||
|
||||
// 构建搜索索引
|
||||
self.search_index.clear();
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
|
||||
|
||||
// 为每个单词创建索引
|
||||
for word in title_lower.split_whitespace() {
|
||||
if word.len() >= 2 { // 只索引长度>=2的单词
|
||||
if word.len() >= 2 {
|
||||
// 只索引长度>=2的单词
|
||||
self.search_index
|
||||
.entry(word.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(album.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 为完整标题创建索引
|
||||
self.search_index
|
||||
.entry(title_lower.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(album.clone());
|
||||
}
|
||||
|
||||
info!("相册缓存已更新,共 {} 个相册,{} 个搜索索引项",
|
||||
self.albums.len(), self.search_index.len());
|
||||
|
||||
info!(
|
||||
"相册缓存已更新,共 {} 个相册,{} 个搜索索引项",
|
||||
self.albums.len(),
|
||||
self.search_index.len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Vec<AlbumInfo> {
|
||||
let query_lower = query.to_lowercase().trim().to_string();
|
||||
|
||||
|
||||
if query_lower.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut seen_ids = std::collections::HashSet::new();
|
||||
|
||||
|
||||
// 1. 搜索完整匹配(最高优先级)
|
||||
if let Some(matches) = self.search_index.get(&query_lower) {
|
||||
for album in matches {
|
||||
@@ -166,10 +174,11 @@ impl AlbumsCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2. 搜索单词匹配
|
||||
for word in query_lower.split_whitespace() {
|
||||
if word.len() >= 2 { // 只搜索长度>=2的单词
|
||||
if word.len() >= 2 {
|
||||
// 只搜索长度>=2的单词
|
||||
if let Some(matches) = self.search_index.get(word) {
|
||||
for album in matches {
|
||||
if seen_ids.insert(album.id) {
|
||||
@@ -179,15 +188,17 @@ impl AlbumsCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. 模糊搜索:在相册标题和描述中搜索包含查询词的结果
|
||||
if results.is_empty() {
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
let description_lower = album.description.as_ref()
|
||||
let description_lower = album
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|d| d.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
// 检查标题或描述是否包含搜索词
|
||||
if title_lower.contains(&query_lower) || description_lower.contains(&query_lower) {
|
||||
if seen_ids.insert(album.id) {
|
||||
@@ -196,23 +207,24 @@ impl AlbumsCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 如果还没有结果,尝试部分匹配(更宽松的搜索)
|
||||
if results.is_empty() && query_lower.len() >= 2 {
|
||||
for album in &self.albums {
|
||||
let title_lower = album.title.to_lowercase();
|
||||
|
||||
|
||||
// 检查标题是否包含搜索词的任何部分
|
||||
if query_lower.split_whitespace().any(|word|
|
||||
word.len() >= 2 && title_lower.contains(word)
|
||||
) {
|
||||
if query_lower
|
||||
.split_whitespace()
|
||||
.any(|word| word.len() >= 2 && title_lower.contains(word))
|
||||
{
|
||||
if seen_ids.insert(album.id) {
|
||||
results.push(album.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
723
src/queue.rs
Normal file
723
src/queue.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{info, warn, error, debug};
|
||||
use uuid::Uuid;
|
||||
use image::GenericImageView;
|
||||
use crate::models::{AlbumIndex, CreateAlbum, CreateImage};
|
||||
|
||||
/// 任务类型
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum TaskType {
|
||||
ThumbnailGeneration, // 缩略图生成
|
||||
AlbumImport, // 相册导入
|
||||
}
|
||||
|
||||
/// 任务优先级
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TaskPriority {
|
||||
High = 0, // 前端请求的缩略图
|
||||
Medium = 1, // 相册导入
|
||||
Low = 2, // 后台质量升级
|
||||
}
|
||||
|
||||
/// 通用任务结构
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Task {
|
||||
pub id: Uuid,
|
||||
pub task_type: TaskType,
|
||||
pub priority: TaskPriority,
|
||||
pub created_at: Instant,
|
||||
pub data: TaskData,
|
||||
}
|
||||
|
||||
/// 任务数据
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TaskData {
|
||||
ThumbnailGeneration {
|
||||
image_id: Uuid,
|
||||
original_data: Vec<u8>,
|
||||
},
|
||||
AlbumImport {
|
||||
album_path: String,
|
||||
album_data: Option<Vec<u8>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 队列统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueueStats {
|
||||
pub pending_tasks: usize,
|
||||
pub active_tasks: usize,
|
||||
pub completed_tasks: usize,
|
||||
pub failed_tasks: usize,
|
||||
pub queue_size_limit: usize,
|
||||
pub is_paused: bool,
|
||||
pub task_type_stats: Vec<TaskTypeStats>,
|
||||
// HTTP请求频率信息
|
||||
pub request_frequency: f64,
|
||||
pub request_count: u64,
|
||||
}
|
||||
|
||||
/// 任务类型统计
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskTypeStats {
|
||||
pub task_type: TaskType,
|
||||
pub pending: usize,
|
||||
pub completed: usize,
|
||||
pub failed: usize,
|
||||
}
|
||||
|
||||
/// 智能任务队列
|
||||
pub struct TaskQueue {
|
||||
queue: Mutex<VecDeque<Task>>,
|
||||
active_tasks: AtomicUsize,
|
||||
completed_tasks: AtomicUsize,
|
||||
failed_tasks: AtomicUsize,
|
||||
max_concurrent_tasks: usize,
|
||||
queue_size_limit: usize,
|
||||
is_paused: AtomicBool,
|
||||
notify: Notify,
|
||||
last_activity: Mutex<Instant>,
|
||||
// HTTP请求频率监控
|
||||
request_count: AtomicU64,
|
||||
last_request_time: Mutex<Instant>,
|
||||
request_window: Duration,
|
||||
// 配置阈值
|
||||
high_request_rate_threshold: f64,
|
||||
idle_request_rate_threshold: f64,
|
||||
}
|
||||
|
||||
impl TaskQueue {
|
||||
pub fn new(max_concurrent_tasks: usize, queue_size_limit: usize, high_request_rate_threshold: f64, idle_request_rate_threshold: f64) -> Self {
|
||||
Self {
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
active_tasks: AtomicUsize::new(0),
|
||||
completed_tasks: AtomicUsize::new(0),
|
||||
failed_tasks: AtomicUsize::new(0),
|
||||
max_concurrent_tasks,
|
||||
queue_size_limit,
|
||||
is_paused: AtomicBool::new(false),
|
||||
notify: Notify::new(),
|
||||
last_activity: Mutex::new(Instant::now()),
|
||||
// HTTP请求频率监控
|
||||
request_count: AtomicU64::new(0),
|
||||
last_request_time: Mutex::new(Instant::now()),
|
||||
request_window: Duration::from_secs(10),
|
||||
// 配置阈值
|
||||
high_request_rate_threshold,
|
||||
idle_request_rate_threshold,
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加任务到队列
|
||||
pub async fn add_task(&self, task: Task) -> Result<(), String> {
|
||||
let mut queue = self.queue.lock().await;
|
||||
|
||||
// 检查队列是否已满
|
||||
if queue.len() >= self.queue_size_limit {
|
||||
return Err("队列已满,无法添加新任务".to_string());
|
||||
}
|
||||
|
||||
// 检查是否已有相同ID的任务
|
||||
if queue.iter().any(|t| t.id == task.id) {
|
||||
return Err("该任务已在队列中".to_string());
|
||||
}
|
||||
|
||||
// 根据优先级插入任务(高优先级在前)
|
||||
let insert_position = queue
|
||||
.iter()
|
||||
.position(|t| t.priority > task.priority)
|
||||
.unwrap_or(queue.len());
|
||||
|
||||
queue.insert(insert_position, task);
|
||||
|
||||
// 更新最后活动时间
|
||||
*self.last_activity.lock().await = Instant::now();
|
||||
|
||||
// 通知工作线程
|
||||
self.notify.notify_one();
|
||||
|
||||
debug!("任务已添加到队列,当前队列大小: {}", queue.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从队列获取下一个任务
|
||||
pub async fn next_task(&self) -> Option<Task> {
|
||||
// 检查是否暂停
|
||||
if self.is_paused.load(Ordering::Acquire) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 检查并发任务数量
|
||||
if self.active_tasks.load(Ordering::Acquire) >= self.max_concurrent_tasks {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut queue = self.queue.lock().await;
|
||||
if let Some(task) = queue.pop_front() {
|
||||
self.active_tasks.fetch_add(1, Ordering::Release);
|
||||
Some(task)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记任务完成
|
||||
pub fn task_completed(&self, success: bool) {
|
||||
self.active_tasks.fetch_sub(1, Ordering::Release);
|
||||
if success {
|
||||
self.completed_tasks.fetch_add(1, Ordering::Release);
|
||||
} else {
|
||||
self.failed_tasks.fetch_add(1, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停队列处理
|
||||
pub fn pause(&self) {
|
||||
self.is_paused.store(true, Ordering::Release);
|
||||
info!("任务队列已暂停");
|
||||
}
|
||||
|
||||
/// 恢复队列处理
|
||||
pub fn resume(&self) {
|
||||
self.is_paused.store(false, Ordering::Release);
|
||||
self.notify.notify_one();
|
||||
info!("任务队列已恢复");
|
||||
}
|
||||
|
||||
/// 记录HTTP请求(在每次HTTP请求处理时调用)
|
||||
pub async fn record_http_request(&self) {
|
||||
let now = Instant::now();
|
||||
let mut last_request_time = self.last_request_time.lock().await;
|
||||
|
||||
// 如果距离上次请求时间超过了窗口时间,重置计数器
|
||||
if now.duration_since(*last_request_time) > self.request_window {
|
||||
self.request_count.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
// 增加请求计数
|
||||
self.request_count.fetch_add(1, Ordering::Release);
|
||||
*last_request_time = now;
|
||||
}
|
||||
|
||||
/// 获取当前请求频率(请求数/秒)
|
||||
pub fn get_request_frequency(&self) -> f64 {
|
||||
let request_count = self.request_count.load(Ordering::Acquire) as f64;
|
||||
let window_seconds = self.request_window.as_secs_f64();
|
||||
|
||||
if window_seconds > 0.0 {
|
||||
request_count / window_seconds
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查服务器负载是否过高(基于HTTP请求频率)
|
||||
pub async fn is_server_busy(&self) -> bool {
|
||||
let request_frequency = self.get_request_frequency();
|
||||
|
||||
// 基于HTTP请求频率判断业务繁忙度
|
||||
// 高请求频率(超过配置阈值)时认为服务器繁忙
|
||||
let is_high_request_rate = request_frequency > self.high_request_rate_threshold;
|
||||
|
||||
// 如果请求频率高,或者并发任务/队列大小超过阈值,认为服务器繁忙
|
||||
is_high_request_rate
|
||||
}
|
||||
|
||||
/// 获取队列统计信息
|
||||
pub async fn get_stats(&self) -> QueueStats {
|
||||
let queue = self.queue.lock().await;
|
||||
|
||||
// 按任务类型统计
|
||||
let mut type_stats = std::collections::HashMap::new();
|
||||
for task in queue.iter() {
|
||||
let stats = type_stats
|
||||
.entry(task.task_type.clone())
|
||||
.or_insert((0, 0, 0));
|
||||
stats.0 += 1; // pending
|
||||
}
|
||||
|
||||
let task_type_stats = type_stats
|
||||
.into_iter()
|
||||
.map(|(task_type, (pending, completed, failed))| TaskTypeStats {
|
||||
task_type,
|
||||
pending,
|
||||
completed,
|
||||
failed,
|
||||
})
|
||||
.collect();
|
||||
|
||||
QueueStats {
|
||||
pending_tasks: queue.len(),
|
||||
active_tasks: self.active_tasks.load(Ordering::Acquire),
|
||||
completed_tasks: self.completed_tasks.load(Ordering::Acquire),
|
||||
failed_tasks: self.failed_tasks.load(Ordering::Acquire),
|
||||
queue_size_limit: self.queue_size_limit,
|
||||
is_paused: self.is_paused.load(Ordering::Acquire),
|
||||
task_type_stats,
|
||||
// 添加请求频率信息
|
||||
request_frequency: self.get_request_frequency(),
|
||||
request_count: self.request_count.load(Ordering::Acquire),
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理过期的任务(超过一定时间的任务)
|
||||
pub async fn cleanup_expired_tasks(&self, max_age: Duration) {
|
||||
let mut queue = self.queue.lock().await;
|
||||
let now = Instant::now();
|
||||
let initial_len = queue.len();
|
||||
|
||||
queue.retain(|task| now.duration_since(task.created_at) <= max_age);
|
||||
|
||||
let removed_count = initial_len - queue.len();
|
||||
if removed_count > 0 {
|
||||
warn!("清理了 {} 个过期任务", removed_count);
|
||||
}
|
||||
}
|
||||
|
||||
/// 等待队列变为空闲状态
|
||||
pub async fn wait_for_idle(&self, timeout: Duration) -> bool {
|
||||
let start = Instant::now();
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 缩略图生成Worker
|
||||
pub struct ThumbnailWorker {
|
||||
queue: Arc<TaskQueue>,
|
||||
image_processor: Arc<crate::image_processor::ImageProcessor>,
|
||||
}
|
||||
|
||||
impl ThumbnailWorker {
|
||||
pub fn new(
|
||||
queue: Arc<TaskQueue>,
|
||||
image_processor: Arc<crate::image_processor::ImageProcessor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
queue,
|
||||
image_processor,
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动缩略图Worker
|
||||
pub async fn start(self) {
|
||||
info!("启动缩略图生成Worker");
|
||||
|
||||
loop {
|
||||
// 检查队列中是否有任务
|
||||
if let Some(task) = self.queue.next_task().await {
|
||||
if matches!(task.task_type, TaskType::ThumbnailGeneration) {
|
||||
debug!("缩略图Worker开始处理任务: {:?}", task.id);
|
||||
|
||||
let success = self.process_thumbnail_task(task).await;
|
||||
self.queue.task_completed(success);
|
||||
|
||||
// 在处理完每个任务后检查服务器负载
|
||||
if self.queue.is_server_busy().await {
|
||||
info!("服务器负载较高,暂停队列处理");
|
||||
self.queue.pause();
|
||||
|
||||
// 每30秒检查一次服务器状态,直到服务器不忙时恢复队列
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
if !self.queue.is_server_busy().await {
|
||||
info!("服务器负载恢复正常,恢复队列处理");
|
||||
self.queue.resume();
|
||||
break;
|
||||
}
|
||||
info!("服务器仍然繁忙,继续等待...");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不是缩略图任务,放回队列
|
||||
let mut queue = self.queue.queue.lock().await;
|
||||
queue.push_front(task);
|
||||
}
|
||||
} else {
|
||||
// 队列为空,等待新任务通知
|
||||
self.queue.notify.notified().await;
|
||||
}
|
||||
|
||||
// 短暂休眠避免CPU占用过高
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理缩略图任务
|
||||
async fn process_thumbnail_task(&self, task: Task) -> bool {
|
||||
if let TaskData::ThumbnailGeneration {
|
||||
image_id,
|
||||
original_data,
|
||||
} = task.data
|
||||
{
|
||||
// 使用具体的错误处理,避免 Send trait 问题
|
||||
let result = self
|
||||
.image_processor
|
||||
.generate_thumbnail_from_memory(&original_data, &image_id.to_string())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(thumbnail_data) => {
|
||||
// 保存缩略图到文件
|
||||
let thumbnail_path = self
|
||||
.image_processor
|
||||
.config
|
||||
.thumbnails_dir
|
||||
.join(image_id.to_string());
|
||||
|
||||
match tokio::fs::write(&thumbnail_path, &thumbnail_data).await {
|
||||
Ok(_) => {
|
||||
info!("✅ 缩略图生成成功: {:?}", image_id);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("保存缩略图失败: {:?} - {}", image_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("缩略图生成失败: {:?} - {:?}", image_id, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("任务类型不匹配,期望缩略图生成任务");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 相册导入Worker
|
||||
pub struct AlbumImportWorker {
|
||||
queue: Arc<TaskQueue>,
|
||||
db: Arc<crate::database::Database>,
|
||||
config: crate::config::StorageConfig,
|
||||
}
|
||||
|
||||
impl AlbumImportWorker {
|
||||
pub fn new(
|
||||
queue: Arc<TaskQueue>,
|
||||
db: Arc<crate::database::Database>,
|
||||
config: crate::config::StorageConfig,
|
||||
) -> Self {
|
||||
Self { queue, db, config }
|
||||
}
|
||||
|
||||
/// 启动相册导入Worker
|
||||
pub async fn start(self) {
|
||||
info!("启动相册导入Worker");
|
||||
|
||||
loop {
|
||||
// 检查队列中是否有任务
|
||||
if let Some(task) = self.queue.next_task().await {
|
||||
if matches!(task.task_type, TaskType::AlbumImport) {
|
||||
debug!("相册导入Worker开始处理任务: {:?}", task.id);
|
||||
|
||||
let success = self.process_album_import_task(task).await;
|
||||
self.queue.task_completed(success);
|
||||
|
||||
// 在处理完每个任务后检查服务器负载
|
||||
if self.queue.is_server_busy().await {
|
||||
info!("服务器负载较高,暂停队列处理");
|
||||
self.queue.pause();
|
||||
|
||||
// 每30秒检查一次服务器状态,直到服务器不忙时恢复队列
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
if !self.queue.is_server_busy().await {
|
||||
info!("服务器负载恢复正常,恢复队列处理");
|
||||
self.queue.resume();
|
||||
break;
|
||||
}
|
||||
info!("服务器仍然繁忙,继续等待...");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果不是相册导入任务,放回队列并减少active_tasks计数器
|
||||
let mut queue = self.queue.queue.lock().await;
|
||||
queue.push_front(task);
|
||||
// 手动减少active_tasks计数器,因为任务没有被处理
|
||||
self.queue.active_tasks.fetch_sub(1, Ordering::Release);
|
||||
}
|
||||
} else {
|
||||
// 队列为空,等待新任务通知
|
||||
self.queue.notify.notified().await;
|
||||
}
|
||||
|
||||
// 短暂休眠避免CPU占用过高
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理相册导入任务
|
||||
async fn process_album_import_task(&self, task: Task) -> bool {
|
||||
if let TaskData::AlbumImport {
|
||||
album_path,
|
||||
album_data: _,
|
||||
} = task.data
|
||||
{
|
||||
info!("开始处理相册导入任务: {}", album_path);
|
||||
|
||||
// 将相对路径与images_dir组合成完整路径
|
||||
let full_path = std::path::Path::new(&self.config.images_dir).join(&album_path);
|
||||
|
||||
// 检查相册文件夹是否存在
|
||||
if !full_path.exists() {
|
||||
error!("相册文件夹不存在: {}", full_path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 导入单个相册
|
||||
match self.import_single_album(&album_path, &full_path).await {
|
||||
Ok(count) => {
|
||||
info!("相册导入成功: {} (导入 {} 张图片)", album_path, count);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
error!("相册导入失败: {} - {}", album_path, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("任务类型不匹配,期望相册导入任务");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 导入单个相册(按照import.rs中的逻辑实现)
|
||||
async fn import_single_album(&self, folder_name: &str, path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("开始导入相册: {}", folder_name);
|
||||
|
||||
// 读取index.json文件
|
||||
let index_file_path = path.join("index.json");
|
||||
let album_title = if index_file_path.exists() {
|
||||
match std::fs::read_to_string(&index_file_path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<AlbumIndex>(&content) {
|
||||
Ok(index) => {
|
||||
info!("从index.json读取相册标题: {}", index.title);
|
||||
index.title
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("解析index.json失败: {}, 使用文件夹名作为标题", e);
|
||||
folder_name.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("读取index.json失败: {}, 使用文件夹名作为标题", e);
|
||||
folder_name.to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("index.json不存在,使用文件夹名作为标题");
|
||||
folder_name.to_string()
|
||||
};
|
||||
|
||||
// 创建新相册
|
||||
let new_album = CreateAlbum {
|
||||
title: album_title.clone(),
|
||||
document: folder_name.to_string(),
|
||||
description: Some(format!("从文件夹 {} 自动导入的相册", folder_name)),
|
||||
};
|
||||
|
||||
let album_id = self.db.create_album(new_album).await?;
|
||||
info!("创建相册: {} (ID: {})", album_title, album_id);
|
||||
|
||||
// 读取图片URL列表(如果有index.json)
|
||||
let image_urls = if index_file_path.exists() {
|
||||
match std::fs::read_to_string(&index_file_path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<AlbumIndex>(&content) {
|
||||
Ok(index) => {
|
||||
info!("从index.json读取到 {} 个图片URL", index.images.len());
|
||||
Some(index.images)
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 快速扫描图片文件
|
||||
let image_files = self.scan_image_files(path, folder_name)?;
|
||||
|
||||
if image_files.is_empty() {
|
||||
info!("相册 {} 中没有找到图片文件", folder_name);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
info!("发现 {} 张图片,开始处理...", image_files.len());
|
||||
|
||||
// 并行处理图片信息获取
|
||||
let mut image_tasks = Vec::new();
|
||||
|
||||
for (index, (filename, image_path)) in image_files.iter().enumerate() {
|
||||
let image_url = image_urls.as_ref().and_then(|urls| {
|
||||
if urls.len() > index {
|
||||
Some(urls[index].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let filename_clone = filename.clone();
|
||||
let image_path_clone = image_path.clone();
|
||||
let album_id_clone = album_id;
|
||||
let folder_name_clone = folder_name.to_string();
|
||||
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
// 在阻塞线程中处理图片信息
|
||||
let (file_size, width, height) = match AlbumImportWorker::get_image_info_static(&image_path_clone) {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
eprintln!("获取图片信息失败: {} - {}", image_path_clone.display(), e);
|
||||
(0, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
CreateImage {
|
||||
album_id: album_id_clone,
|
||||
filename: format!("{}/{}", folder_name_clone, filename_clone),
|
||||
original_url: image_url,
|
||||
title: Some(format!("图片 {}", index + 1)),
|
||||
description: Some(format!("从文件夹 {} 导入的图片", folder_name_clone)),
|
||||
file_size,
|
||||
width,
|
||||
height,
|
||||
mime_type: match image_path_clone.extension().and_then(|s| s.to_str()).unwrap_or("jpg").to_lowercase().as_str() {
|
||||
"jpg" | "jpeg" => "image/jpeg".to_string(),
|
||||
"png" => "image/png".to_string(),
|
||||
"gif" => "image/gif".to_string(),
|
||||
"webp" => "image/webp".to_string(),
|
||||
"bmp" => "image/bmp".to_string(),
|
||||
_ => "image/jpeg".to_string(),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
image_tasks.push(task);
|
||||
}
|
||||
|
||||
// 收集所有图片信息
|
||||
let mut images_to_insert = Vec::new();
|
||||
for task in image_tasks {
|
||||
match task.await {
|
||||
Ok(image) => images_to_insert.push(image),
|
||||
Err(e) => error!("图片处理任务失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// 批量插入图片
|
||||
if !images_to_insert.is_empty() {
|
||||
match self.db.add_images(images_to_insert).await {
|
||||
Ok(ids) => {
|
||||
info!("成功导入 {} 张图片到相册 {}", ids.len(), folder_name);
|
||||
|
||||
// 更新相册图片数量
|
||||
if let Err(e) = self.db.update_album_image_count(album_id).await {
|
||||
error!("更新相册图片数量失败: {}", e);
|
||||
}
|
||||
|
||||
Ok(ids.len())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("批量插入图片失败: {}", e);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("没有图片需要插入到相册 {}", folder_name);
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫描图片文件(从import.rs迁移)
|
||||
fn scan_image_files(&self, path: &std::path::Path, _folder_name: &str) -> Result<Vec<(String, std::path::PathBuf)>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut image_files = Vec::new();
|
||||
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let file_path = entry.path();
|
||||
|
||||
if file_path.is_file() {
|
||||
if let Some(extension) = file_path.extension().and_then(|s| s.to_str()) {
|
||||
let ext_lower = extension.to_lowercase();
|
||||
if ext_lower == "jpg" || ext_lower == "jpeg" || ext_lower == "png" ||
|
||||
ext_lower == "gif" || ext_lower == "webp" || ext_lower == "bmp" {
|
||||
|
||||
if let Some(filename) = file_path.file_name().and_then(|s| s.to_str()) {
|
||||
image_files.push((filename.to_string(), file_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(image_files)
|
||||
}
|
||||
|
||||
|
||||
/// 静态方法:获取图片信息(用于spawn_blocking)
|
||||
fn get_image_info_static(image_path: &std::path::Path) -> Result<(i64, i32, i32), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let metadata = std::fs::metadata(image_path)?;
|
||||
let file_size = metadata.len() as i64;
|
||||
|
||||
// 使用image库获取图片尺寸
|
||||
let img = image::open(image_path)?;
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
Ok((file_size, width as i32, height as i32))
|
||||
}
|
||||
}
|
||||
|
||||
/// 任务工厂 - 用于创建不同类型的任务
|
||||
pub struct TaskFactory;
|
||||
|
||||
impl TaskFactory {
|
||||
/// 创建缩略图生成任务
|
||||
pub fn create_thumbnail_task(
|
||||
image_id: Uuid,
|
||||
original_data: Vec<u8>,
|
||||
priority: TaskPriority,
|
||||
) -> Task {
|
||||
Task {
|
||||
id: Uuid::new_v4(),
|
||||
task_type: TaskType::ThumbnailGeneration,
|
||||
priority,
|
||||
created_at: Instant::now(),
|
||||
data: TaskData::ThumbnailGeneration {
|
||||
image_id,
|
||||
original_data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建相册导入任务
|
||||
pub fn create_album_import_task(album_path: String, album_data: Option<Vec<u8>>) -> Task {
|
||||
Task {
|
||||
id: Uuid::new_v4(),
|
||||
task_type: TaskType::AlbumImport,
|
||||
priority: TaskPriority::Medium,
|
||||
created_at: Instant::now(),
|
||||
data: TaskData::AlbumImport {
|
||||
album_path,
|
||||
album_data,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/routes.rs
106
src/routes.rs
@@ -7,6 +7,16 @@ use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
use crate::queue;
|
||||
|
||||
// 记录HTTP请求频率的辅助函数
|
||||
async fn log_request_frequency(method: &str, path: &str, client_ip: Option<&str>) {
|
||||
// 这里可以添加请求频率记录逻辑
|
||||
// 例如:记录到数据库、内存缓存或日志文件
|
||||
let ip = client_ip.unwrap_or("unknown");
|
||||
println!("Request: {} {} from {}", method, path, ip);
|
||||
}
|
||||
|
||||
// 统一错误处理宏
|
||||
macro_rules! handle_db_error {
|
||||
($result:expr, $error_msg:expr) => {
|
||||
@@ -61,7 +71,8 @@ async fn check_and_upgrade_thumbnail_quality(
|
||||
config: &crate::config::StorageConfig,
|
||||
image: &Image,
|
||||
original_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
task_queue: Option<Arc<queue::TaskQueue>>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let thumbnail_path = config.thumbnails_dir.join(image.id.to_string());
|
||||
|
||||
// 检查原始图片是否存在
|
||||
@@ -87,22 +98,35 @@ async fn check_and_upgrade_thumbnail_quality(
|
||||
}
|
||||
};
|
||||
|
||||
// 生成高质量缩略图
|
||||
let image_processor = ImageProcessor::new(config.clone());
|
||||
match image_processor
|
||||
.generate_thumbnail_from_memory(&original_data, &image.id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(high_quality_data) => {
|
||||
// 保存高质量缩略图
|
||||
if let Err(e) = fs::write(&thumbnail_path, &high_quality_data) {
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
} else {
|
||||
println!("✅ 缩略图已升级为高质量版本 (ID: {})", image.id);
|
||||
}
|
||||
// 如果有队列系统,使用队列处理质量升级
|
||||
if let Some(queue) = task_queue {
|
||||
let task = queue::TaskFactory::create_thumbnail_task(
|
||||
image.id,
|
||||
original_data,
|
||||
queue::TaskPriority::Low, // 质量升级,低优先级
|
||||
);
|
||||
|
||||
if let Err(e) = queue.add_task(task).await {
|
||||
eprintln!("添加缩略图质量升级任务失败: {}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("生成高质量缩略图失败: {}", e);
|
||||
} else {
|
||||
// 如果没有队列系统,直接生成高质量缩略图
|
||||
let image_processor = ImageProcessor::new(config.clone());
|
||||
match image_processor
|
||||
.generate_thumbnail_from_memory(&original_data, &image.id.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(high_quality_data) => {
|
||||
// 保存高质量缩略图
|
||||
if let Err(e) = fs::write(&thumbnail_path, &high_quality_data) {
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
} else {
|
||||
println!("✅ 缩略图已升级为高质量版本 (ID: {})", image.id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("生成高质量缩略图失败: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +208,17 @@ pub async fn remove_favorite(
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务队列状态
|
||||
pub async fn get_queue_stats(
|
||||
task_queue: web::Data<Arc<queue::TaskQueue>>,
|
||||
) -> impl Responder {
|
||||
// 记录HTTP请求频率
|
||||
task_queue.record_http_request().await;
|
||||
|
||||
let stats = task_queue.get_stats().await;
|
||||
HttpResponse::Ok().json(stats)
|
||||
}
|
||||
|
||||
// 通过图片ID取消收藏
|
||||
pub async fn remove_favorite_by_image(
|
||||
db: web::Data<Arc<Database>>,
|
||||
@@ -418,6 +453,7 @@ pub async fn create_album(
|
||||
pub async fn get_thumbnail(
|
||||
db: web::Data<Arc<Database>>,
|
||||
image_processor: web::Data<Arc<ImageProcessor>>,
|
||||
task_queue: web::Data<Arc<queue::TaskQueue>>,
|
||||
path: web::Path<String>,
|
||||
) -> impl Responder {
|
||||
let image_id = match Uuid::parse_str(&path.into_inner()) {
|
||||
@@ -448,11 +484,13 @@ pub async fn get_thumbnail(
|
||||
let config_clone = image_processor.config.clone();
|
||||
let image_clone = image.clone();
|
||||
let original_path_clone = original_path.clone();
|
||||
let queue_clone = task_queue.get_ref().clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = check_and_upgrade_thumbnail_quality(
|
||||
&config_clone,
|
||||
&image_clone,
|
||||
&original_path_clone,
|
||||
Some(queue_clone),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -483,28 +521,18 @@ pub async fn get_thumbnail(
|
||||
.await
|
||||
{
|
||||
Ok(thumbnail_data) => {
|
||||
// 异步保存高质量缩略图到文件(不阻塞响应)
|
||||
let config_clone = image_processor.config.clone();
|
||||
let filename_clone = image.id.clone();
|
||||
let original_data_clone = data.clone();
|
||||
// 将高质量缩略图生成任务添加到队列
|
||||
let task = queue::TaskFactory::create_thumbnail_task(
|
||||
image.id,
|
||||
data.clone(),
|
||||
queue::TaskPriority::High, // 前端请求,高优先级
|
||||
);
|
||||
|
||||
// 异步添加任务到队列
|
||||
let queue_clone = task_queue.clone();
|
||||
tokio::spawn(async move {
|
||||
let thumbnail_path = config_clone
|
||||
.thumbnails_dir
|
||||
.join(filename_clone.to_string());
|
||||
|
||||
// 生成高质量缩略图并保存
|
||||
if let Ok(high_quality_data) = ImageProcessor::new(config_clone)
|
||||
.generate_thumbnail_from_memory(
|
||||
&original_data_clone,
|
||||
&filename_clone.to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Err(e) =
|
||||
fs::write(&thumbnail_path, &high_quality_data)
|
||||
{
|
||||
eprintln!("保存高质量缩略图失败: {}", e);
|
||||
}
|
||||
if let Err(e) = queue_clone.add_task(task).await {
|
||||
eprintln!("添加缩略图生成任务失败: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -516,7 +544,7 @@ pub async fn get_thumbnail(
|
||||
.body(thumbnail_data)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("快速缩略图生成失败: {}", e);
|
||||
eprintln!("快速缩略图生成失败: {:?}", e);
|
||||
// 如果快速生成失败,返回原始图片的小尺寸版本
|
||||
let small_data =
|
||||
image_processor.resize_image_to_max(&data, 800).await;
|
||||
@@ -549,4 +577,4 @@ pub async fn get_thumbnail(
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user