#![deny(missing_docs)]
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate)]
use moka::future::{Cache, CacheBuilder}; use once_cell::sync::Lazy; use rand::seq::SliceRandom; use std::sync::Arc;
use std::time::SystemTime;
use sysinfo::System; use tokio::fs::File;
use tokio::io::{AsyncReadExt, BufReader};
static TOTAL_MEMORY: Lazy<u64> = Lazy::new(|| {
let mem = System::new_all().total_memory();
mem.max(1024 * 1024 * 1024) });
type CachedLines = Arc<Vec<String>>;
#[derive(Debug, Clone)]
pub struct AsyncLineCache {
pub lines: Cache<String, CachedLines>,
pub contents: Cache<String, String>,
metadata: Cache<String, (SystemTime, u64)>,
}
impl AsyncLineCache {
pub fn new() -> Self {
let total_limit = ((*TOTAL_MEMORY as f64) * 0.85) as u64;
let per_cache_limit = total_limit / 2;
let lines_weigher = |_k: &String, v: &CachedLines| -> u32 {
let vec_cap = v.capacity() * std::mem::size_of::<String>();
let str_cap: usize = v.iter().map(|s| s.capacity()).sum();
let overhead = 128; ((vec_cap + str_cap + overhead) as u64)
.min(u32::MAX as u64) as u32
};
let content_weigher = |_k: &String, s: &String| -> u32 {
(s.capacity() as u64 + 128).min(u32::MAX as u64) as u32
};
Self {
lines: CacheBuilder::new(per_cache_limit)
.weigher(lines_weigher)
.build(),
contents: CacheBuilder::new(per_cache_limit)
.weigher(content_weigher)
.build(),
metadata: Cache::new(8192),
}
}
pub async fn get_line(&self, filename: &str, lineno: usize) -> std::io::Result<Option<String>> {
if self.is_file_modified(filename).await? {
self.invalidate(filename).await;
}
let lines = self.load_or_get_lines(filename).await?;
Ok(lines.get(lineno.wrapping_sub(1)).cloned())
}
pub async fn random_line(&self, filename: &str) -> std::io::Result<Option<String>> {
if self.is_file_modified(filename).await? {
self.invalidate(filename).await;
}
if let Some(lines) = self.lines.get(filename).await {
if lines.is_empty() {
Ok(None)
} else {
Ok(lines.choose(&mut rand::thread_rng()).cloned())
}
} else {
let lines = self.load_or_get_lines(filename).await?;
Ok(lines.choose(&mut rand::thread_rng()).cloned())
}
}
pub async fn random_sign_char(&self, filename: &str) -> std::io::Result<Option<char>> {
let Some(line) = self.random_line(filename).await? else { return Ok(None); };
let chars: Vec<char> = line.chars().collect();
Ok(chars.choose(&mut rand::thread_rng()).copied())
}
pub async fn random_sign(&self, filename: &str) -> std::io::Result<Option<String>> {
Ok(self.random_sign_char(filename).await?.map(|c| c.to_string()))
}
pub async fn get_lines(&self, filename: &str) -> std::io::Result<Option<Vec<String>>> {
if self.is_file_modified(filename).await? {
self.invalidate(filename).await;
}
let lines = self.load_or_get_lines(filename).await?;
if lines.is_empty() {
Ok(None)
} else {
Ok(Some((*lines).clone())) }
}
pub async fn get_content(&self, filename: &str) -> std::io::Result<Option<String>> {
if self.is_file_modified(filename).await? {
self.invalidate(filename).await;
}
let key = filename.to_string();
if let Some(content) = self.contents.get(&key).await {
return Ok(Some(content));
}
match tokio::fs::read_to_string(filename).await {
Ok(content) => {
self.contents.insert(key.clone(), content.clone()).await;
Ok(Some(content))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
self.invalidate(filename).await;
Ok(None)
}
Err(e) => Err(e),
}
}
pub async fn invalidate(&self, filename: &str) {
let key = filename.to_string();
self.lines.remove(&key).await;
self.contents.remove(&key).await;
self.metadata.remove(&key).await;
}
pub async fn clear(&self) {
self.lines.invalidate_all();
self.contents.invalidate_all();
self.metadata.invalidate_all();
}
#[deprecated(since = "0.2.0", note = "请使用 clear() 替代 | use clear() instead")]
pub async fn clear_cache(&self) {
self.clear().await;
}
async fn load_or_get_lines(&self, filename: &str) -> std::io::Result<CachedLines> {
let key = filename.to_string();
if let Some(lines) = self.lines.get(&key).await {
return Ok(lines);
}
self.load_file_into_cache(filename).await
}
async fn load_file_into_cache(&self, filename: &str) -> std::io::Result<CachedLines> {
let file = match File::open(filename).await {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
self.invalidate(filename).await;
return Ok(Arc::new(vec![]));
}
Err(e) => return Err(e),
};
let meta = tokio::fs::metadata(filename).await?;
let mut reader = BufReader::new(file);
let mut content = String::with_capacity(meta.len() as usize + 1);
reader.read_to_string(&mut content).await?;
let mut lines: Vec<String> = content.lines().map(String::from).collect();
if content.ends_with('\n') && !content.is_empty() {
lines.push(String::new());
}
let lines_arc = Arc::new(lines);
let key = filename.to_string();
self.lines.insert(key.clone(), lines_arc.clone()).await;
self.metadata.insert(key, (meta.modified()?, meta.len())).await;
Ok(lines_arc)
}
async fn is_file_modified(&self, filename: &str) -> std::io::Result<bool> {
match tokio::fs::metadata(filename).await {
Ok(meta) => {
let mtime = meta.modified()?;
let size = meta.len();
if let Some((cached_mtime, cached_size)) = self.metadata.get(filename).await {
Ok(mtime != cached_mtime || size != cached_size)
} else {
Ok(true) }
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
self.invalidate(filename).await;
Ok(true)
}
Err(e) => Err(e),
}
}
}
impl Default for AsyncLineCache {
fn default() -> Self {
Self::new()
}
}