use lru::LruCache;
use std::path::Path;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FileState {
pub content: String,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_partial_view: Option<bool>,
}
pub const READ_FILE_STATE_CACHE_SIZE: usize = 100;
pub const DEFAULT_MAX_CACHE_SIZE_BYTES: usize = 25 * 1024 * 1024;
pub struct FileStateCache {
cache: LruCache<String, FileState>,
max_size_bytes: usize,
}
impl FileStateCache {
pub fn new(max_entries: usize, max_size_bytes: usize) -> Self {
Self {
cache: LruCache::new(
std::num::NonZeroUsize::new(max_entries)
.unwrap_or(std::num::NonZeroUsize::new(1).unwrap()),
),
max_size_bytes,
}
}
pub fn get(&mut self, key: &str) -> Option<FileState> {
let normalized = normalize_path(key);
self.cache.get(&normalized).cloned()
}
pub fn set(&mut self, key: String, value: FileState) -> &mut Self {
let normalized = normalize_path(&key);
self.cache.push(normalized, value);
self
}
pub fn contains(&mut self, key: &str) -> bool {
let normalized = normalize_path(key);
self.cache.contains(&normalized)
}
pub fn remove(&mut self, key: &str) -> Option<FileState> {
let normalized = normalize_path(key);
self.cache.pop(&normalized)
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn max_entries(&self) -> Option<usize> {
self.cache.cap().get().try_into().ok()
}
pub fn max_size(&self) -> usize {
self.max_size_bytes
}
pub fn iter(&mut self) -> impl Iterator<Item = (&String, &FileState)> {
self.cache.iter()
}
pub fn keys(&mut self) -> impl Iterator<Item = &String> {
self.cache.iter().map(|(k, _)| k)
}
pub fn entries(&mut self) -> impl Iterator<Item = (&String, &FileState)> {
self.cache.iter()
}
}
fn normalize_path(path: &str) -> String {
let path_obj = Path::new(path);
let components: Vec<String> = path_obj
.components()
.filter_map(|c| match c {
std::path::Component::Normal(s) => Some(s.to_string_lossy().to_string()),
std::path::Component::ParentDir => Some("..".to_string()),
_ => None,
})
.collect();
if components.is_empty() {
path.to_string()
} else {
components.join(std::path::MAIN_SEPARATOR_STR)
}
}
pub fn create_file_state_cache_with_size_limit(
max_entries: usize,
max_size_bytes: usize,
) -> FileStateCache {
FileStateCache::new(max_entries, max_size_bytes)
}
pub fn cache_to_object(cache: &mut FileStateCache) -> std::collections::HashMap<String, FileState> {
cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
pub fn cache_keys(cache: &mut FileStateCache) -> Vec<String> {
cache.keys().cloned().collect()
}
pub fn clone_file_state_cache(cache: &FileStateCache) -> FileStateCache {
let max_entries = cache.max_entries().unwrap_or(READ_FILE_STATE_CACHE_SIZE);
let max_size = cache.max_size();
FileStateCache::new(max_entries, max_size)
}
pub fn merge_file_state_caches(
first: &mut FileStateCache,
second: &mut FileStateCache,
) -> FileStateCache {
let max_entries = first.max_entries().unwrap_or(READ_FILE_STATE_CACHE_SIZE);
let max_size = first.max_size();
let mut merged = FileStateCache::new(max_entries, max_size);
for (file_path, file_state) in first.entries() {
merged.set(file_path.clone(), file_state.clone());
}
for (file_path, file_state) in second.entries() {
if let Some(existing) = merged.get(file_path) {
if file_state.timestamp > existing.timestamp {
merged.set(file_path.clone(), file_state.clone());
}
} else {
merged.set(file_path.clone(), file_state.clone());
}
}
merged
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_state_cache_basic() {
let mut cache = FileStateCache::new(10, 1000);
let state = FileState {
content: "hello".to_string(),
timestamp: 1000,
..Default::default()
};
cache.set("test.txt".to_string(), state.clone());
let retrieved = cache.get("test.txt");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().content, "hello");
}
#[test]
fn test_file_state_cache_normalize_path() {
let mut cache = FileStateCache::new(10, 1000);
let state = FileState {
content: "hello".to_string(),
timestamp: 1000,
..Default::default()
};
cache.set("test.txt".to_string(), state.clone());
assert!(cache.contains("test.txt"));
}
#[test]
fn test_read_file_state_cache_size() {
assert_eq!(READ_FILE_STATE_CACHE_SIZE, 100);
}
#[test]
fn test_default_max_cache_size() {
assert_eq!(DEFAULT_MAX_CACHE_SIZE_BYTES, 25 * 1024 * 1024);
}
}