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;
|
||||
});
|
||||
|
||||
// 启动后台定时任务:图片磁盘缓存清理
|
||||
tokio::spawn(async {
|
||||
tasks::image_cache_cleanup::run_cleanup().await;
|
||||
});
|
||||
|
||||
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖
|
||||
let config = ServeConfig::builder().incremental(
|
||||
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")]
|
||||
pub mod post_purge;
|
||||
/// 定时清理图片磁盘缓存,避免缓存目录无限增长。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod image_cache_cleanup;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user