feat(tasks): add periodic image disk cache cleanup
This commit is contained in:
parent
36554af5f5
commit
0b107c3f2e
@ -73,6 +73,11 @@ fn main() {
|
|||||||
tasks::post_purge::run_purge().await;
|
tasks::post_purge::run_purge().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 启动后台定时任务:图片磁盘缓存清理
|
||||||
|
tokio::spawn(async {
|
||||||
|
tasks::image_cache_cleanup::run_cleanup().await;
|
||||||
|
});
|
||||||
|
|
||||||
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖
|
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖
|
||||||
let config = ServeConfig::builder().incremental(
|
let config = ServeConfig::builder().incremental(
|
||||||
dioxus::server::IncrementalRendererConfig::default().invalidate_after(
|
dioxus::server::IncrementalRendererConfig::default().invalidate_after(
|
||||||
|
|||||||
235
src/tasks/image_cache_cleanup.rs
Normal file
235
src/tasks/image_cache_cleanup.rs
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
//! 图片磁盘缓存定期清理任务。
|
||||||
|
//!
|
||||||
|
//! 仅在 `server` feature 启用时编译,每小时运行一次。
|
||||||
|
//! 删除超过保留时间的文件,并在总大小超过上限时按修改时间删除最旧的文件。
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use tokio::time::interval;
|
||||||
|
|
||||||
|
const CACHE_DIR: &str = "uploads/.cache";
|
||||||
|
|
||||||
|
/// 启动图片磁盘缓存清理循环,每小时触发一次。
|
||||||
|
pub async fn run_cleanup() {
|
||||||
|
let mut ticker = interval(Duration::from_secs(3600));
|
||||||
|
loop {
|
||||||
|
if let Err(e) = cleanup_image_cache().await {
|
||||||
|
tracing::error!("Image disk cache cleanup error: {:?}", e);
|
||||||
|
}
|
||||||
|
ticker.tick().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取环境变量并清理默认磁盘缓存目录。
|
||||||
|
pub async fn cleanup_image_cache() -> io::Result<()> {
|
||||||
|
let base = Path::new(CACHE_DIR);
|
||||||
|
let max_mb = std::env::var("IMAGE_DISK_CACHE_MAX_MB")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(1024);
|
||||||
|
let max_age_hours = std::env::var("IMAGE_DISK_CACHE_MAX_AGE_HOURS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(168);
|
||||||
|
let (deleted, bytes_freed) = cleanup_image_cache_at(base, max_mb, max_age_hours).await?;
|
||||||
|
if !deleted.is_empty() {
|
||||||
|
tracing::info!(
|
||||||
|
"Image disk cache cleanup: removed {} files, freed {} bytes",
|
||||||
|
deleted.len(),
|
||||||
|
bytes_freed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理指定目录下的图片磁盘缓存。
|
||||||
|
///
|
||||||
|
/// 返回被删除文件的路径列表以及释放的总字节数。
|
||||||
|
pub async fn cleanup_image_cache_at(
|
||||||
|
base: &Path,
|
||||||
|
max_mb: u64,
|
||||||
|
max_age_hours: u64,
|
||||||
|
) -> io::Result<(Vec<PathBuf>, u64)> {
|
||||||
|
if !base.exists() {
|
||||||
|
return Ok((Vec::new(), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_age = Duration::from_secs(max_age_hours * 3600);
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let cutoff = now - max_age;
|
||||||
|
|
||||||
|
let mut entries: Vec<(PathBuf, u64, SystemTime)> = Vec::new();
|
||||||
|
collect_files(base, &mut entries).await?;
|
||||||
|
|
||||||
|
let mut deleted = Vec::new();
|
||||||
|
let mut bytes_freed: u64 = 0;
|
||||||
|
|
||||||
|
// 第一轮:删除超过保留期限的文件。
|
||||||
|
let mut remaining: Vec<(PathBuf, u64, SystemTime)> = Vec::new();
|
||||||
|
for (path, size, mtime) in entries {
|
||||||
|
if mtime < cutoff {
|
||||||
|
match tokio::fs::remove_file(&path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
deleted.push(path);
|
||||||
|
bytes_freed += size;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to remove expired cache file {:?}: {:?}", path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remaining.push((path, size, mtime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二轮:若总大小仍超过上限,按修改时间从旧到新删除。
|
||||||
|
let max_bytes = max_mb.saturating_mul(1024 * 1024);
|
||||||
|
let mut total: u64 = remaining.iter().map(|(_, size, _)| size).sum();
|
||||||
|
if total > max_bytes {
|
||||||
|
remaining.sort_by_key(|a| a.2);
|
||||||
|
for (path, size, _) in remaining {
|
||||||
|
if total <= max_bytes {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::fs::remove_file(&path).await {
|
||||||
|
Ok(_) => {
|
||||||
|
total -= size;
|
||||||
|
deleted.push(path);
|
||||||
|
bytes_freed += size;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to remove cache file {:?} for size cap: {:?}",
|
||||||
|
path,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((deleted, bytes_freed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 递归收集目录下的所有常规文件,返回路径、大小与修改时间。
|
||||||
|
async fn collect_files(
|
||||||
|
base: &Path,
|
||||||
|
entries: &mut Vec<(PathBuf, u64, SystemTime)>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let mut stack = vec![base.to_path_buf()];
|
||||||
|
while let Some(dir) = stack.pop() {
|
||||||
|
let mut reader = tokio::fs::read_dir(&dir).await?;
|
||||||
|
while let Some(entry) = reader.next_entry().await? {
|
||||||
|
let metadata = entry.metadata().await?;
|
||||||
|
if metadata.is_file() {
|
||||||
|
let mtime = metadata.modified()?;
|
||||||
|
entries.push((entry.path(), metadata.len(), mtime));
|
||||||
|
} else if metadata.is_dir() {
|
||||||
|
stack.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "server"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
fn temp_cache_dir() -> PathBuf {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("yggdrasil_image_cache_test_{}_{}", nanos, std::process::id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_ignores_missing_directory() {
|
||||||
|
let dir = temp_cache_dir();
|
||||||
|
let (deleted, freed) = cleanup_image_cache_at(&dir, 1024, 168).await.unwrap();
|
||||||
|
assert!(deleted.is_empty());
|
||||||
|
assert_eq!(freed, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_removes_expired_files_by_age() {
|
||||||
|
let dir = temp_cache_dir();
|
||||||
|
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||||
|
|
||||||
|
let old = dir.join("old.dat");
|
||||||
|
tokio::fs::write(&old, b"old content").await.unwrap();
|
||||||
|
// 确保文件的修改时间严格早于清理时计算的截止时间。
|
||||||
|
sleep(Duration::from_millis(1100)).await;
|
||||||
|
|
||||||
|
let (deleted, freed) = cleanup_image_cache_at(&dir, 1024, 0).await.unwrap();
|
||||||
|
assert_eq!(deleted.len(), 1);
|
||||||
|
assert!(!old.exists());
|
||||||
|
assert!(freed > 0);
|
||||||
|
|
||||||
|
tokio::fs::remove_dir_all(&dir).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_keeps_recent_files() {
|
||||||
|
let dir = temp_cache_dir();
|
||||||
|
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||||
|
|
||||||
|
let recent = dir.join("recent.dat");
|
||||||
|
tokio::fs::write(&recent, b"recent content").await.unwrap();
|
||||||
|
|
||||||
|
let (deleted, freed) = cleanup_image_cache_at(&dir, 1024, 168).await.unwrap();
|
||||||
|
assert!(deleted.is_empty());
|
||||||
|
assert_eq!(freed, 0);
|
||||||
|
assert!(recent.exists());
|
||||||
|
|
||||||
|
tokio::fs::remove_dir_all(&dir).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_enforces_size_cap_by_mtime() {
|
||||||
|
let dir = temp_cache_dir();
|
||||||
|
tokio::fs::create_dir_all(&dir).await.unwrap();
|
||||||
|
|
||||||
|
let f1 = dir.join("oldest.dat");
|
||||||
|
tokio::fs::write(&f1, vec![0u8; 1024 * 1024]).await.unwrap();
|
||||||
|
sleep(Duration::from_millis(1100)).await;
|
||||||
|
|
||||||
|
let f2 = dir.join("middle.dat");
|
||||||
|
tokio::fs::write(&f2, vec![0u8; 1024 * 1024]).await.unwrap();
|
||||||
|
sleep(Duration::from_millis(1100)).await;
|
||||||
|
|
||||||
|
let f3 = dir.join("newest.dat");
|
||||||
|
tokio::fs::write(&f3, vec![0u8; 1024 * 1024]).await.unwrap();
|
||||||
|
|
||||||
|
// 上限 2 MB,当前 3 MB,应删除最旧的一个文件。
|
||||||
|
let (deleted, freed) = cleanup_image_cache_at(&dir, 2, 1000).await.unwrap();
|
||||||
|
assert_eq!(deleted.len(), 1);
|
||||||
|
assert!(!f1.exists());
|
||||||
|
assert!(f2.exists());
|
||||||
|
assert!(f3.exists());
|
||||||
|
assert_eq!(freed, 1024 * 1024);
|
||||||
|
|
||||||
|
tokio::fs::remove_dir_all(&dir).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_recurses_into_subdirectories() {
|
||||||
|
let dir = temp_cache_dir();
|
||||||
|
let sub = dir.join("nested");
|
||||||
|
tokio::fs::create_dir_all(&sub).await.unwrap();
|
||||||
|
|
||||||
|
let nested = sub.join("nested.dat");
|
||||||
|
tokio::fs::write(&nested, b"nested content").await.unwrap();
|
||||||
|
sleep(Duration::from_millis(1100)).await;
|
||||||
|
|
||||||
|
let (deleted, _freed) = cleanup_image_cache_at(&dir, 1024, 0).await.unwrap();
|
||||||
|
assert_eq!(deleted.len(), 1);
|
||||||
|
assert!(!nested.exists());
|
||||||
|
|
||||||
|
tokio::fs::remove_dir_all(&dir).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,3 +11,6 @@ pub mod session_cleanup;
|
|||||||
/// 定时清理回收站中超过保留期的已删除文章。
|
/// 定时清理回收站中超过保留期的已删除文章。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub mod post_purge;
|
pub mod post_purge;
|
||||||
|
/// 定时清理图片磁盘缓存,避免缓存目录无限增长。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub mod image_cache_cleanup;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user