use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::error::{PkgError, Result};
use crate::model::PkgMetadata;
const CACHE_VERSION: u32 = 6;
#[derive(Serialize, Deserialize)]
pub struct PkgIndexCache {
pub version: u32,
pub packages: Vec<PkgMetadata>,
}
fn cache_dir() -> PathBuf {
let dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("pkq");
if let Err(e) = std::fs::create_dir_all(&dir) {
tracing::warn!("Failed to create cache dir {:?}: {}", dir, e);
}
dir
}
pub fn cache_status() -> Vec<(String, u64)> {
let base = cache_dir();
let mut items: Vec<(String, u64)> = Vec::new();
fn dir_size(dir: &Path) -> u64 {
let mut total = 0;
if let Ok(entries) = std::fs::read_dir(dir) {
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
total += dir_size(&p);
} else if let Ok(m) = e.metadata() {
total += m.len();
}
}
}
total
}
if let Ok(entries) = std::fs::read_dir(&base) {
for e in entries.flatten() {
let p = e.path();
let size = if p.is_dir() {
dir_size(&p)
} else {
e.metadata().map(|m| m.len()).unwrap_or(0)
};
if size > 0 {
let rel = p
.strip_prefix(&base)
.unwrap_or(&p)
.to_string_lossy()
.to_string();
items.push((rel, size));
}
}
}
items.sort_by_key(|(_, size)| std::cmp::Reverse(*size));
items
}
pub fn cache_dir_mtime() -> u64 {
fn walk(dir: &Path) -> u64 {
let mut max = 0u64;
if let Ok(entries) = std::fs::read_dir(dir) {
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
max = max.max(walk(&p));
} else if let Ok(m) = e.metadata() {
if let Ok(t) = m.modified() {
let s = t
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if s > max {
max = s;
}
}
}
}
}
max
}
walk(&cache_dir())
}
pub fn cache_target_size(target: &str) -> u64 {
let base = cache_dir();
fn dir_size(dir: &Path) -> u64 {
let mut total = 0;
if let Ok(entries) = std::fs::read_dir(dir) {
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
total += dir_size(&p);
} else if let Ok(m) = e.metadata() {
total += m.len();
}
}
}
total
}
match target {
"all" => dir_size(&base),
"index" => dir_size(&base.join("index")),
"repos" => dir_size(&base.join("repos")),
"contents" => [deb_contents_raw_cache_path(), deb_contents_raw_meta_path()]
.iter()
.map(|p| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0))
.sum(),
_ => 0,
}
}
pub fn cache_clean(target: &str) -> Result<u64> {
let base = cache_dir();
let before = cache_target_size(target);
let remove = |p: &Path| -> Result<()> {
if p.is_dir() {
std::fs::remove_dir_all(p)
} else if p.exists() {
std::fs::remove_file(p)
} else {
Ok(())
}
.map_err(|e| PkgError::IoError(format!("清理失败 {:?}: {}", p, e)))
};
match target {
"all" => remove(&base)?,
"index" => remove(&base.join("index"))?,
"repos" => remove(&base.join("repos"))?,
"contents" => {
remove(&deb_contents_raw_cache_path())?;
remove(&deb_contents_raw_meta_path())?;
}
other => {
return Err(PkgError::InvalidArgument(format!(
"未知清理目标: {}(可选 all|index|repos|contents)",
other
)))
}
}
let after: u64 = cache_status().iter().map(|(_, s)| *s).sum();
Ok(before.saturating_sub(after))
}
fn ensure_parent_dir(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
PkgError::IoError(format!("Failed to create cache dir {:?}: {}", parent, e))
})?;
}
Ok(())
}
const CACHE_MAGIC: &[u8; 4] = b"PKQ1";
fn encode_cache<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
let body = postcard::to_allocvec(value)
.map_err(|e| PkgError::IoError(format!("Failed to serialize cache: {}", e)))?;
let mut buf = Vec::with_capacity(body.len() + CACHE_MAGIC.len());
buf.extend_from_slice(CACHE_MAGIC);
buf.extend_from_slice(&body);
Ok(buf)
}
fn decode_cache<T: serde::de::DeserializeOwned>(data: &[u8]) -> Option<T> {
let body = data.get(CACHE_MAGIC.len()..)?;
if &data[..CACHE_MAGIC.len()] != CACHE_MAGIC {
return None; }
postcard::from_bytes(body).ok()
}
fn file_mtime(path: &Path) -> u64 {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|t| {
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
})
.unwrap_or(0)
}
#[derive(Serialize, Deserialize)]
pub struct CompletionNamesCache {
pub source_mtime: u64,
pub packages: Vec<(String, String)>,
}
impl CompletionNamesCache {
pub fn load(path: &Path, source_mtime: u64) -> Option<Self> {
if source_mtime == 0 {
return None;
}
let data = std::fs::read(path).ok()?;
let cache: Self = decode_cache(&data)?;
if cache.source_mtime != source_mtime {
return None;
}
if cache.packages.is_empty() {
return None;
}
Some(cache)
}
pub fn save(path: &Path, source_mtime: u64, packages: Vec<(String, String)>) -> Result<()> {
let cache = Self {
source_mtime,
packages,
};
let data = encode_cache(&cache)?;
let tmp = path.with_extension("tmp");
ensure_parent_dir(path)?;
std::fs::write(&tmp, &data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
}
pub fn completion_names_cache_path() -> PathBuf {
cache_dir().join("index").join("completion_names.bin")
}
fn max_source_mtime(paths: &[PathBuf]) -> u64 {
paths
.iter()
.filter_map(|p| {
let m = file_mtime(p);
if m > 0 {
Some(m)
} else {
None
}
})
.max()
.unwrap_or(0)
}
impl PkgIndexCache {
pub fn load(path: &Path, ttl: u64, force: bool, source_files: &[PathBuf]) -> Option<Self> {
if force {
return None;
}
let cache_mtime = file_mtime(path);
if cache_mtime == 0 {
return None;
}
let src_max = max_source_mtime(source_files);
if cache_mtime < src_max {
return None;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if ttl > 0 && now > cache_mtime && now - cache_mtime > ttl {
return None;
}
let data = std::fs::read(path).ok()?;
let cache: PkgIndexCache = decode_cache(&data)?;
if cache.version != CACHE_VERSION {
return None;
}
if cache.packages.is_empty() {
return None;
}
Some(cache)
}
pub fn save(path: &Path, packages: Vec<PkgMetadata>) -> Result<()> {
let cache = PkgIndexCache {
version: CACHE_VERSION,
packages,
};
let data = encode_cache(&cache)?;
let tmp = path.with_extension("tmp");
ensure_parent_dir(path)?;
std::fs::write(&tmp, &data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct ContentsMeta {
version: u32,
}
impl ContentsMeta {
pub fn check_valid(meta_path: &Path, ttl: u64, force: bool, source_files: &[PathBuf]) -> bool {
if force {
return false;
}
let meta_mtime = file_mtime(meta_path);
if meta_mtime == 0 {
return false;
}
let src_max = max_source_mtime(source_files);
if meta_mtime < src_max {
return false;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if ttl > 0 && now > meta_mtime && now - meta_mtime > ttl {
return false;
}
let data = match std::fs::read(meta_path) {
Ok(d) => d,
Err(_) => return false,
};
let meta: ContentsMeta = match decode_cache(&data) {
Some(m) => m,
None => return false,
};
meta.version == CACHE_VERSION
}
pub fn save_meta(path: &Path) -> Result<()> {
let meta = ContentsMeta {
version: CACHE_VERSION,
};
let data = encode_cache(&meta)?;
let tmp = path.with_extension("tmp");
ensure_parent_dir(path)?;
std::fs::write(&tmp, &data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
}
pub fn deb_cache_path() -> PathBuf {
cache_dir().join("index").join("deb_packages.bin")
}
pub fn rpm_local_cache_path() -> PathBuf {
cache_dir().join("index").join("rpm_local.bin")
}
pub fn rpm_repo_cache_path(repo_id: &str, baseurl: &str) -> PathBuf {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
baseurl.hash(&mut hasher);
let safe_id: String = repo_id
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
cache_dir()
.join("index")
.join(format!("rpm_repo_{}_{:016x}.bin", safe_id, hasher.finish()))
}
#[derive(Serialize, Deserialize)]
pub struct RpmRepoCache {
pub primary_ts: i64,
pub packages: Vec<PkgMetadata>,
}
impl RpmRepoCache {
pub fn load(path: &Path, expected_ts: i64) -> Option<Self> {
if expected_ts <= 0 {
return None;
}
let data = std::fs::read(path).ok()?;
let cache: RpmRepoCache = decode_cache(&data)?;
if cache.primary_ts != expected_ts {
return None;
}
if cache.packages.is_empty() {
return None;
}
Some(cache)
}
pub fn load_any(path: &Path) -> Option<Self> {
let data = std::fs::read(path).ok()?;
let cache: RpmRepoCache = decode_cache(&data)?;
if cache.packages.is_empty() {
return None;
}
Some(cache)
}
pub fn save(path: &Path, primary_ts: i64, packages: Vec<PkgMetadata>) -> Result<()> {
let cache = RpmRepoCache {
primary_ts,
packages,
};
let data = encode_cache(&cache)?;
let tmp = path.with_extension("tmp");
ensure_parent_dir(path)?;
std::fs::write(&tmp, &data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
}
pub fn deb_contents_raw_cache_path() -> PathBuf {
cache_dir().join("index").join("deb_contents_raw.txt")
}
pub fn deb_contents_raw_meta_path() -> PathBuf {
cache_dir().join("index").join("deb_contents_raw.meta")
}
pub fn rpm_filelists_cache_path() -> PathBuf {
cache_dir().join("index").join("rpm_filelists.txt")
}
pub fn rpm_filelists_meta_path() -> PathBuf {
cache_dir().join("index").join("rpm_filelists.meta")
}
#[derive(Serialize, Deserialize)]
pub struct RpmFilelistsMeta {
pub key: String,
}
impl RpmFilelistsMeta {
pub fn load(path: &Path) -> Option<Self> {
let data = std::fs::read(path).ok()?;
decode_cache(&data)
}
pub fn save(path: &Path, key: String) -> Result<()> {
let data = encode_cache(&RpmFilelistsMeta { key })?;
let tmp = path.with_extension("tmp");
ensure_parent_dir(path)?;
std::fs::write(&tmp, &data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
}
pub fn deb_apt_sources() -> Vec<PathBuf> {
let mut paths = vec![PathBuf::from("/etc/apt/sources.list")];
let sources_d = PathBuf::from("/etc/apt/sources.list.d");
if let Ok(entries) = std::fs::read_dir(&sources_d) {
for entry in entries.flatten() {
let p = entry.path();
let ext = p.extension().and_then(|e| e.to_str());
if ext == Some("list") || ext == Some("sources") {
paths.push(p);
}
}
}
paths
}
pub fn deb_apt_lists_packages() -> Vec<PathBuf> {
let mut paths = Vec::new();
let lists_dir = PathBuf::from("/var/lib/apt/lists");
if let Ok(entries) = std::fs::read_dir(&lists_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.to_string_lossy().ends_with("_Packages") {
paths.push(p);
}
}
}
paths
}
pub fn deb_apt_lists_contents() -> Vec<PathBuf> {
let mut paths = Vec::new();
let lists_dir = PathBuf::from("/var/lib/apt/lists");
if let Ok(entries) = std::fs::read_dir(&lists_dir) {
for entry in entries.flatten() {
let p = entry.path();
let name = p.to_string_lossy();
if name.contains("Contents-") && name.ends_with(".lz4") {
paths.push(p);
}
}
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_completion_names_cache_roundtrip_and_invalidation() {
let path = std::env::temp_dir().join(format!(
"pkq_completion_cache_test_{}.bin",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let pkgs = vec![("bash".to_string(), "GNU Bourne Again SHell".to_string())];
CompletionNamesCache::save(&path, 1234, pkgs.clone()).unwrap();
let loaded = CompletionNamesCache::load(&path, 1234).unwrap();
assert_eq!(loaded.packages, pkgs);
assert!(CompletionNamesCache::load(&path, 5678).is_none());
assert!(CompletionNamesCache::load(&path, 0).is_none());
std::fs::remove_file(&path).ok();
}
}