This commit is contained in:
lychang
2026-03-20 02:31:07 +08:00
commit 5893cd5e31
19 changed files with 5653 additions and 0 deletions

41
src/config.rs Normal file
View File

@@ -0,0 +1,41 @@
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub database: DatabaseConfig,
pub server: ServerConfig,
pub storage: StorageConfig,
}
#[derive(Debug, Deserialize, Clone)]
pub struct DatabaseConfig {
pub url: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
#[derive(Debug, Deserialize, Clone)]
pub struct StorageConfig {
pub images_dir: PathBuf,
pub thumbnails_dir: PathBuf,
}
impl Config {
pub fn load() -> Result<Self, config::ConfigError> {
let config = config::Config::builder()
.add_source(config::File::with_name("config"))
.build()?;
config.try_deserialize()
}
pub fn server_address(&self) -> String {
format!("{}:{}", self.server.host, self.server.port)
}
}

407
src/database.rs Normal file
View File

@@ -0,0 +1,407 @@
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, Row};
use crate::config::Config;
use crate::models::*;
use uuid::Uuid;
pub struct Database {
pool: SqlitePool,
}
impl Database {
pub async fn new(config: &Config) -> Result<Self, sqlx::Error> {
// 从URL中提取文件路径
let url = &config.database.url;
let file_path = if url.starts_with("sqlite:") {
&url[7..] // 移除 "sqlite:" 前缀
} else {
url
};
// 确保数据目录存在
if let Some(parent) = std::path::Path::new(file_path).parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
// 如果数据库文件不存在,创建一个空文件
if !std::path::Path::new(file_path).exists() {
std::fs::File::create(file_path)?;
println!("创建新的数据库文件: {}", file_path);
}
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(url)
.await?;
// 创建表
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(Database { pool })
}
// 相册操作
pub async fn get_all_albums(&self) -> Result<Vec<AlbumInfo>, sqlx::Error> {
let albums = sqlx::query(
r#"
SELECT
a.id,
a.title,
a.document,
a.description,
a.cover_image_id,
COUNT(img.id) as image_count,
a.created_at
FROM albums a
LEFT JOIN images img ON img.album_id = a.id
LEFT JOIN images i ON i.id = a.cover_image_id
ORDER BY a.document DESC
"#
)
.fetch_all(&self.pool)
.await?;
let mut result = Vec::new();
for row in albums {
let id_str = row.get::<String, _>("id");
let id = match Uuid::parse_str(&id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的UUID格式: {} - 错误: {}", id_str, e);
continue; // 跳过无效的记录
}
};
result.push(AlbumInfo {
id,
title: row.get("title"),
document: row.get("document"),
description: row.get("description"),
cover_image_id: row.get("cover_image_id"),
image_count: row.get("image_count"),
created_at: row.get::<String, _>("created_at"),
});
}
Ok(result)
}
pub async fn search_albums(&self, query: &str) -> Result<Vec<AlbumInfo>, sqlx::Error> {
let search_pattern = format!("%{}%", query);
let albums = sqlx::query(
r#"
SELECT
a.id,
a.title,
a.document,
a.description,
a.cover_image_id,
COUNT(img.id) as image_count,
a.created_at
FROM albums a
LEFT JOIN images img ON img.album_id = a.id
LEFT JOIN images i ON i.id = a.cover_image_id
WHERE a.title LIKE ?
GROUP BY a.id, a.created_at
ORDER BY a.created_at DESC
"#
)
.bind(&search_pattern)
.fetch_all(&self.pool)
.await?;
let mut result = Vec::new();
for row in albums {
let id_str = row.get::<String, _>("id");
let id = match Uuid::parse_str(&id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的UUID格式: {} - 错误: {}", id_str, e);
continue; // 跳过无效的记录
}
};
result.push(AlbumInfo {
id,
title: row.get("title"),
document: row.get("document"),
description: row.get("description"),
cover_image_id: row.get("cover_image_id"),
image_count: row.get("image_count"),
created_at: row.get::<String, _>("created_at"),
});
}
Ok(result)
}
pub async fn get_album_with_images(&self, album_id: Uuid) -> Result<Option<AlbumWithImages>, sqlx::Error> {
// 获取相册基本信息
let album_row = sqlx::query(
"SELECT * FROM albums WHERE id = ?"
)
.bind(album_id.to_string())
.fetch_optional(&self.pool)
.await?;
if let Some(row) = album_row {
let id_str = row.get::<String, _>("id");
let id = match Uuid::parse_str(&id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的相册UUID格式: {} - 错误: {}", id_str, e);
return Ok(None);
}
};
let cover_image_id = match row.get::<Option<String>, _>("cover_image_id") {
Some(s) => match Uuid::parse_str(&s) {
Ok(uuid) => Some(uuid),
Err(e) => {
eprintln!("无效的封面图片UUID格式: {} - 错误: {}", s, e);
None
}
},
None => None
};
let album = Album {
id,
title: row.get("title"),
description: row.get("description"),
cover_image_id,
created_at: row.get::<String, _>("created_at"),
updated_at: row.get::<String, _>("updated_at"),
};
// 获取相册中的图片
let image_rows = sqlx::query(
"SELECT * FROM images WHERE album_id = ? ORDER BY created_at ASC"
)
.bind(album_id.to_string())
.fetch_all(&self.pool)
.await?;
let mut images = Vec::new();
for row in image_rows {
let image_id_str = row.get::<String, _>("id");
let image_id = match Uuid::parse_str(&image_id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的图片UUID格式: {} - 错误: {}", image_id_str, e);
continue;
}
};
let album_id_str = row.get::<String, _>("album_id");
let album_id = match Uuid::parse_str(&album_id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的相册UUID格式: {} - 错误: {}", album_id_str, e);
continue;
}
};
images.push(Image {
id: image_id,
album_id,
filename: row.get("filename"),
original_url: row.get("original_url"),
title: row.get("title"),
description: row.get("description"),
file_size: row.get("file_size"),
width: row.get("width"),
height: row.get("height"),
mime_type: row.get("mime_type"),
created_at: row.get::<String, _>("created_at"),
});
}
let image_count = images.len() as i64;
Ok(Some(AlbumWithImages {
album,
images,
image_count,
}))
} else {
Ok(None)
}
}
pub async fn create_album(&self, album: CreateAlbum) -> Result<Uuid, sqlx::Error> {
let id = Uuid::new_v4();
let now = chrono::Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO albums (id, title, document, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(id.to_string())
.bind(album.title)
.bind(album.document)
.bind(album.description)
.bind(&now)
.bind(&now)
.execute(&self.pool)
.await?;
Ok(id)
}
// 图片操作
pub async fn add_image(&self, image: CreateImage) -> Result<Uuid, sqlx::Error> {
let id = Uuid::new_v4();
let now = chrono::Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO images (id, album_id, filename, original_url, title, description, file_size, width, height, mime_type, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(id.to_string())
.bind(image.album_id.to_string())
.bind(image.filename)
.bind(image.original_url)
.bind(image.title)
.bind(image.description)
.bind(image.file_size)
.bind(image.width)
.bind(image.height)
.bind(image.mime_type)
.bind(&now)
.execute(&self.pool)
.await?;
// 如果这是相册的第一张图片,设置为封面
let image_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM images WHERE album_id = ?"
)
.bind(image.album_id.to_string())
.fetch_one(&self.pool)
.await?;
if image_count == 1 {
sqlx::query(
"UPDATE albums SET cover_image_id = ? WHERE id = ?"
)
.bind(id.to_string())
.bind(image.album_id.to_string())
.execute(&self.pool)
.await?;
}
Ok(id)
}
pub async fn add_images(&self, images: Vec<CreateImage>) -> Result<Vec<Uuid>, sqlx::Error> {
if images.is_empty() {
return Ok(Vec::new());
}
let now = chrono::Utc::now().to_rfc3339();
let mut ids = Vec::new();
// 保存第一个相册ID用于封面设置
let first_album_id = images.first().map(|img| img.album_id);
// 使用事务确保批量插入的原子性
let mut tx = self.pool.begin().await?;
for image in &images {
let id = Uuid::new_v4();
ids.push(id);
sqlx::query(
"INSERT INTO images (id, album_id, filename, original_url, title, description, file_size, width, height, mime_type, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(id.to_string())
.bind(image.album_id.to_string())
.bind(&image.filename)
.bind(&image.original_url)
.bind(&image.title)
.bind(&image.description)
.bind(image.file_size)
.bind(image.width)
.bind(image.height)
.bind(&image.mime_type)
.bind(&now)
.execute(&mut *tx)
.await?;
}
// 提交事务
tx.commit().await?;
// 批量设置封面图片(只对每个相册的第一批图片设置)
if let Some(album_id) = first_album_id {
// 检查相册是否已有图片
let existing_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM images WHERE album_id = ?"
)
.bind(album_id.to_string())
.fetch_one(&self.pool)
.await?;
// 如果这是相册的第一批图片,设置第一张为封面
if existing_count == images.len() as i64 {
if let Some(first_id) = ids.first() {
sqlx::query(
"UPDATE albums SET cover_image_id = ? WHERE id = ?"
)
.bind(first_id.to_string())
.bind(album_id.to_string())
.execute(&self.pool)
.await?;
}
}
}
Ok(ids)
}
pub async fn get_image(&self, image_id: Uuid) -> Result<Option<Image>, sqlx::Error> {
let row = sqlx::query(
"SELECT * FROM images WHERE id = ?"
)
.bind(image_id.to_string())
.fetch_optional(&self.pool)
.await?;
if let Some(row) = row {
let image_id_str = row.get::<String, _>("id");
let image_id = match Uuid::parse_str(&image_id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的图片UUID格式: {} - 错误: {}", image_id_str, e);
return Ok(None);
}
};
let album_id_str = row.get::<String, _>("album_id");
let album_id = match Uuid::parse_str(&album_id_str) {
Ok(uuid) => uuid,
Err(e) => {
eprintln!("无效的相册UUID格式: {} - 错误: {}", album_id_str, e);
return Ok(None);
}
};
Ok(Some(Image {
id: image_id,
album_id,
filename: row.get("filename"),
original_url: row.get("original_url"),
title: row.get("title"),
description: row.get("description"),
file_size: row.get("file_size"),
width: row.get("width"),
height: row.get("height"),
mime_type: row.get("mime_type"),
created_at: row.get::<String, _>("created_at"),
}))
} else {
Ok(None)
}
}
}

113
src/image_processor.rs Normal file
View File

@@ -0,0 +1,113 @@
use std::path::Path;
use std::fs;
use image::{GenericImageView, ImageFormat};
use uuid::Uuid;
use crate::config::StorageConfig;
pub struct ImageProcessor {
pub config: StorageConfig,
}
impl ImageProcessor {
pub fn new(config: StorageConfig) -> Self {
// 确保目录存在
fs::create_dir_all(&config.images_dir).unwrap_or(());
fs::create_dir_all(&config.thumbnails_dir).unwrap_or(());
Self { config }
}
pub async fn save_image(&self, image_data: &[u8], filename: &str) -> Result<ImageInfo, Box<dyn std::error::Error>> {
let image_id = Uuid::new_v4();
let file_extension = Path::new(filename)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("jpg");
let new_filename = format!("{}.{}", image_id, file_extension);
let image_path = self.config.images_dir.join(&new_filename);
// 保存原始图片
fs::write(&image_path, image_data)?;
// 获取图片信息
let img = image::load_from_memory(image_data)?;
let (width, height) = img.dimensions();
let file_size = image_data.len() as i64;
// 生成缩略图
let thumbnail_path = self.config.thumbnails_dir.join(format!("thumb_{}", new_filename));
self.generate_thumbnail(&img, &thumbnail_path, 300, 200)?;
Ok(ImageInfo {
id: image_id,
filename: new_filename,
width: width as i32,
height: height as i32,
file_size,
mime_type: self.detect_mime_type(file_extension),
})
}
pub fn generate_thumbnail(
&self,
img: &image::DynamicImage,
output_path: &Path,
max_width: u32,
max_height: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let (width, height) = img.dimensions();
// 计算缩略图尺寸
let ratio = width as f32 / height as f32;
let (new_width, new_height) = if width > height {
(max_width, (max_width as f32 / ratio) as u32)
} else {
((max_height as f32 * ratio) as u32, max_height)
};
let thumbnail = img.thumbnail(new_width, new_height);
thumbnail.save_with_format(output_path, ImageFormat::Jpeg)?;
Ok(())
}
pub fn detect_mime_type(&self, extension: &str) -> String {
match extension.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(),
_ => "application/octet-stream".to_string(),
}
}
pub fn get_image_url(&self, filename: &str) -> String {
format!("/images/{}", filename)
}
pub fn get_thumbnail_url(&self, filename: &str) -> String {
format!("/thumbnails/thumb_{}", filename)
}
pub async fn download_and_save_image(
&self,
url: &str,
filename: &str,
) -> Result<ImageInfo, Box<dyn std::error::Error>> {
let response = reqwest::get(url).await?;
let image_data = response.bytes().await?;
self.save_image(&image_data, filename).await
}
}
#[derive(Debug)]
pub struct ImageInfo {
pub id: Uuid,
pub filename: String,
pub width: i32,
pub height: i32,
pub file_size: i64,
pub mime_type: String,
}

250
src/import.rs Normal file
View File

@@ -0,0 +1,250 @@
use std::fs;
use std::path::Path;
use image::GenericImageView;
use serde::Deserialize;
use crate::config::Config;
use crate::database::Database;
use crate::models::*;
#[derive(Debug, Deserialize)]
struct AlbumIndex {
title: String,
images: Vec<String>,
}
pub async fn import_existing_data(config: &Config, db: &Database) -> Result<(), Box<dyn std::error::Error>> {
println!("开始扫描图片目录并导入新相册...");
let albums_dir = Path::new(&config.storage.images_dir);
if !albums_dir.exists() {
println!("albums目录不存在,跳过导入");
return Ok(());
}
// 获取数据库中现有的相册列表
let existing_albums = db.get_all_albums().await?;
let existing_titles: Vec<String> = existing_albums.iter()
.map(|album| album.document.to_lowercase())
.collect();
println!("数据库中现有相册数量: {}", existing_albums.len());
let entries = fs::read_dir(albums_dir)?;
let mut imported_count = 0;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(folder_name) = path.file_name().and_then(|s| s.to_str()) {
// 检查是否已存在同名相册
if existing_titles.contains(&folder_name.to_lowercase()) {
println!("跳过已存在的相册: {}", folder_name);
continue;
}
println!("发现新相册: {}", folder_name);
// 读取index.json文件
let index_file_path = path.join("index.json");
let album_title = if index_file_path.exists() {
match fs::read_to_string(&index_file_path) {
Ok(content) => {
match serde_json::from_str::<AlbumIndex>(&content) {
Ok(index) => {
println!(" 从index.json读取相册标题: {}", index.title);
index.title
}
Err(e) => {
println!(" 解析index.json失败: {}, 使用文件夹名作为标题", e);
folder_name.to_string()
}
}
}
Err(e) => {
println!(" 读取index.json失败: {}, 使用文件夹名作为标题", e);
folder_name.to_string()
}
}
} else {
println!(" 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 = db.create_album(new_album).await?;
println!(" 创建相册: {} (ID: {})", album_title, album_id);
// 读取图片URL列表(如果有index.json)
let image_urls = if index_file_path.exists() {
match fs::read_to_string(&index_file_path) {
Ok(content) => {
match serde_json::from_str::<AlbumIndex>(&content) {
Ok(index) => {
println!(" 从index.json读取到 {} 个图片URL", index.images.len());
Some(index.images)
}
Err(_) => None
}
}
Err(_) => None
}
} else {
None
};
// 扫描文件夹中的图片文件
let image_entries = fs::read_dir(&path)?;
let mut image_files = Vec::new();
for image_entry in image_entries {
let image_entry = image_entry?;
let image_path = image_entry.path();
if image_path.is_file() {
if let Some(extension) = image_path.extension().and_then(|s| s.to_str()) {
let valid_extensions = ["jpg", "jpeg", "png", "gif", "webp", "bmp"];
if valid_extensions.contains(&extension.to_lowercase().as_str()) {
let filename = image_path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("unknown.jpg")
.to_string();
image_files.push((filename, image_path));
}
}
}
}
// 按数字顺序排序文件名
image_files.sort_by(|a, b| {
// 提取文件名中的数字部分进行排序
let num_a = a.0.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse::<u32>()
.unwrap_or(0);
let num_b = b.0.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse::<u32>()
.unwrap_or(0);
num_a.cmp(&num_b)
});
let mut image_count = 0;
let mut images_to_insert = Vec::new();
// 收集所有图片信息
for (filename, image_path) in image_files {
image_count += 1;
// 获取对应的URL(如果有的话)
let original_url = if let Some(ref urls) = image_urls {
if urls.len() >= image_count {
Some(urls[image_count - 1].clone())
} else {
None
}
} else {
None
};
// 生成图片标题
let image_title = if let Some(ref urls) = image_urls {
if urls.len() >= image_count {
Some(format!("图片 {}", image_count))
} else {
Some(format!("图片 {}", image_count))
}
} else {
Some(format!("图片 {}", image_count))
};
// 获取图片的实际信息
let (file_size, width, height) = match get_image_info(&image_path) {
Ok(info) => info,
Err(e) => {
eprintln!("获取图片信息失败: {} - {}", image_path.display(), e);
(0, 0, 0)
}
};
let image = CreateImage {
album_id,
filename: format!("{}/{}", folder_name, filename).clone(),
original_url: original_url.clone(),
title: image_title,
description: Some(format!("从文件夹 {} 导入的图片", folder_name)),
file_size,
width,
height,
mime_type: match image_path.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(),
},
};
images_to_insert.push(image.clone());
}
// 批量插入图片
if !images_to_insert.is_empty() {
println!(" 批量插入 {} 张图片...", images_to_insert.len());
match db.add_images(images_to_insert.clone()).await {
Ok(ids) => {
println!(" 批量插入成功,共插入 {} 张图片", ids.len());
}
Err(e) => {
eprintln!(" 批量插入失败: {}", e);
// 如果批量插入失败,尝试逐条插入
println!(" 尝试逐条插入图片...");
let mut success_count = 0;
for image in images_to_insert {
match db.add_image(image).await {
Ok(_) => success_count += 1,
Err(e) => eprintln!(" 单条插入失败: {}", e),
}
}
println!(" 逐条插入完成,成功 {} 张", success_count);
}
}
}
println!(" 相册 {} 导入完成,共导入 {} 张图片", folder_name, image_count);
imported_count += 1;
}
}
}
println!("导入完成!共导入 {} 个新相册", imported_count);
Ok(())
}
/// 获取图片的实际信息(文件大小、宽度、高度)
fn get_image_info(image_path: &Path) -> Result<(i64, i32, i32), Box<dyn std::error::Error>> {
// 获取文件大小
let metadata = 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))
}

104
src/main.rs Normal file
View File

@@ -0,0 +1,104 @@
mod config;
mod models;
mod database;
mod image_processor;
mod routes;
mod import;
use actix_files as afs;
use actix_web::{web, App, HttpServer, HttpResponse};
use std::sync::Arc;
use std::fs;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 加载配置
let config = match config::Config::load() {
Ok(cfg) => cfg,
Err(e) => {
eprintln!("加载配置失败: {}", e);
std::process::exit(1);
}
};
println!("启动图片展示服务...");
println!("服务地址: http://{}", config.server_address());
// 初始化数据库
let db = match database::Database::new(&config).await {
Ok(db) => {
println!("数据库连接成功");
Arc::new(db)
}
Err(e) => {
eprintln!("数据库连接失败: {}", e);
std::process::exit(1);
}
};
// 在后台线程中自动导入新相册(不阻塞HTTP服务启动)
let config_clone = config.clone();
let db_clone = Arc::clone(&db);
tokio::spawn(async move {
println!("[后台任务] 开始扫描图片目录并导入新相册...");
match import::import_existing_data(&config_clone, &db_clone).await {
Ok(_) => println!("[后台任务] 相册导入完成"),
Err(e) => eprintln!("[后台任务] 相册导入失败: {}", e),
}
});
// 初始化图片处理器
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
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!("按 Ctrl+C 停止服务");
// 创建数据目录
std::fs::create_dir_all(&config.storage.images_dir).unwrap_or(());
std::fs::create_dir_all(&config.storage.thumbnails_dir).unwrap_or(());
std::fs::create_dir_all("./data").unwrap_or(());
// 在闭包外提取需要的路径
let server_address = config.server_address();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(Arc::clone(&db)))
.app_data(web::Data::new(Arc::clone(&image_processor)))
// API路由
.route("/api/albums", web::get().to(routes::get_albums))
.route("/api/search", web::get().to(routes::search_albums))
.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))
// 静态文件服务
.service(afs::Files::new("/assets", "./assets").show_files_listing())
// 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
}

65
src/models.rs Normal file
View File

@@ -0,0 +1,65 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
pub struct Album {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub cover_image_id: Option<Uuid>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Image {
pub id: Uuid,
pub album_id: Uuid,
pub filename: String,
pub original_url: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub file_size: i64,
pub width: i32,
pub height: i32,
pub mime_type: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AlbumWithImages {
pub album: Album,
pub images: Vec<Image>,
pub image_count: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AlbumInfo {
pub id: Uuid,
pub title: String,
pub document: String,
pub description: Option<String>,
pub cover_image_id: Option<String>,
pub image_count: i64,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAlbum {
pub title: String,
pub document: String,
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CreateImage {
pub album_id: Uuid,
pub filename: String,
pub original_url: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub file_size: i64,
pub width: i32,
pub height: i32,
pub mime_type: String,
}

266
src/routes.rs Normal file
View File

@@ -0,0 +1,266 @@
use crate::database::Database;
use crate::image_processor::ImageProcessor;
use crate::models::*;
use actix_web::{HttpResponse, Responder, web};
use std::fs;
use std::sync::Arc;
use uuid::Uuid;
pub async fn get_albums(db: web::Data<Arc<Database>>) -> impl Responder {
match db.get_all_albums().await {
Ok(albums) => HttpResponse::Ok().json(albums),
Err(e) => {
eprintln!("获取相册列表失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "获取相册列表失败"
}))
}
}
}
pub async fn get_original_image(
db: web::Data<Arc<Database>>,
image_processor: web::Data<Arc<ImageProcessor>>,
path: web::Path<String>,
) -> impl Responder {
let image_id = match Uuid::parse_str(&path.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "无效的图片ID"
}));
}
};
match db.get_image(image_id).await {
Ok(Some(image)) => {
let original_path = image_processor.config.images_dir.join(&image.filename);
if original_path.exists() {
match fs::read(&original_path) {
Ok(data) => HttpResponse::Ok().content_type(image.mime_type).body(data),
Err(e) => {
eprintln!("读取原始图片失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "读取原始图片失败"
}))
}
}
} else {
HttpResponse::NotFound().json(serde_json::json!({
"error": "原始图片文件不存在"
}))
}
}
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
"error": "图片不存在"
})),
Err(e) => {
eprintln!("获取图片信息失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "获取图片信息失败"
}))
}
}
}
pub async fn search_albums(
db: web::Data<Arc<Database>>,
query: web::Query<std::collections::HashMap<String, String>>,
) -> impl Responder {
let q = query.get("q").map(|s| s.as_str()).unwrap_or("");
if q.is_empty() {
return match db.get_all_albums().await {
Ok(albums) => HttpResponse::Ok().json(albums),
Err(e) => {
eprintln!("获取相册列表失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "获取相册列表失败"
}))
}
};
}
match db.search_albums(q).await {
Ok(albums) => HttpResponse::Ok().json(albums),
Err(e) => {
eprintln!("搜索相册失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "搜索失败"
}))
}
}
}
pub async fn get_album_detail(
db: web::Data<Arc<Database>>,
path: web::Path<String>,
) -> impl Responder {
let album_id = match Uuid::parse_str(&path.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "无效的相册ID"
}));
}
};
match db.get_album_with_images(album_id).await {
Ok(Some(album)) => HttpResponse::Ok().json(album),
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
"error": "相册不存在"
})),
Err(e) => {
eprintln!("获取相册详情失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "获取相册详情失败"
}))
}
}
}
pub async fn create_album(
db: web::Data<Arc<Database>>,
album_data: web::Json<CreateAlbum>,
) -> impl Responder {
match db.create_album(album_data.into_inner()).await {
Ok(album_id) => HttpResponse::Created().json(serde_json::json!({
"id": album_id,
"message": "相册创建成功"
})),
Err(e) => {
eprintln!("创建相册失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "创建相册失败"
}))
}
}
}
pub async fn get_thumbnail(
db: web::Data<Arc<Database>>,
image_processor: web::Data<Arc<ImageProcessor>>,
path: web::Path<String>,
) -> impl Responder {
let image_id = match Uuid::parse_str(&path.into_inner()) {
Ok(id) => id,
Err(_) => {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "无效的图片ID"
}));
}
};
match db.get_image(image_id).await {
Ok(Some(image)) => {
// 优先返回缩略图,如果不存在则生成
let thumbnail_path = image_processor
.config
.thumbnails_dir
.join(image.id.to_string());
let original_path = image_processor.config.images_dir.join(&image.filename);
if thumbnail_path.exists() {
// 返回缩略图
match fs::read(&thumbnail_path) {
Ok(data) => {
let mime_type = image_processor.detect_mime_type("jpg"); // 缩略图总是JPEG格式
HttpResponse::Ok().content_type(mime_type).body(data)
}
Err(e) => {
eprintln!("读取缩略图失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "读取缩略图失败"
}))
}
}
} else if original_path.exists() {
// 生成缩略图并返回
match fs::read(&original_path) {
Ok(data) => {
match image::load_from_memory(&data) {
Ok(img) => {
// 在内存中生成缩略图,避免重复文件读写
let thumbnail = img.thumbnail(800, 600);
// 使用临时文件来生成缩略图数据
let temp_dir = std::env::temp_dir();
let temp_file =
temp_dir.join(format!("temp_thumb_{}", Uuid::new_v4()));
if let Err(e) =
thumbnail.save_with_format(&temp_file, image::ImageFormat::Jpeg)
{
eprintln!("生成缩略图失败: {}", e);
// 如果生成缩略图失败,返回原始图片
return HttpResponse::Ok()
.content_type(image.mime_type)
.body(data);
}
// 读取缩略图数据
match fs::read(&temp_file) {
Ok(thumbnail_data) => {
// 异步保存缩略图到文件(不阻塞响应)
let config_clone = image_processor.config.clone();
let filename_clone = image.id.clone();
let thumbnail_data_clone = thumbnail_data.clone();
tokio::spawn(async move {
let thumbnail_path = config_clone
.thumbnails_dir
.join(filename_clone.to_string());
if let Err(e) =
fs::write(&thumbnail_path, &thumbnail_data_clone)
{
eprintln!("保存缩略图文件失败: {}", e);
}
});
// 清理临时文件
let _ = fs::remove_file(&temp_file);
// 返回缩略图数据
HttpResponse::Ok()
.content_type("image/jpeg")
.body(thumbnail_data)
}
Err(e) => {
eprintln!("读取缩略图数据失败: {}", e);
// 如果读取失败,返回原始图片
HttpResponse::Ok().content_type(image.mime_type).body(data)
}
}
}
Err(e) => {
eprintln!("加载图片失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "加载图片失败"
}))
}
}
}
Err(e) => {
eprintln!("读取原始图片失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "读取图片失败"
}))
}
}
} else {
HttpResponse::NotFound().json(serde_json::json!({
"error": "图片文件不存在"
}))
}
}
Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
"error": "图片不存在"
})),
Err(e) => {
eprintln!("获取图片信息失败: {}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "获取图片信息失败"
}))
}
}
}