use crate::{detail, gap, hint, section, success};
use anyhow::Result;
use colored::Colorize;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
pub fn cache_dir() -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("rfast")
}
pub fn project_dir(hash: &str) -> PathBuf {
cache_dir().join(hash)
}
pub fn root_dir() -> PathBuf {
dirs::cache_dir()
.expect("Could not determine cache directory")
.join("rfast")
}
fn stamp_path(hash: &str) -> PathBuf {
project_dir(hash).join(".build_stamp")
}
pub fn hash_file(path: &Path) -> Result<String> {
let content = fs::read(path)?;
let mut h = Sha256::new();
h.update(&content);
h.update(path.to_string_lossy().as_bytes());
Ok(hex::encode(h.finalize()))
}
pub fn is_cache_valid(hash: &str) -> bool {
let ok = binary_path(hash).exists()
&& stamp_path(hash).exists()
&& fs::read_to_string(stamp_path(hash))
.map(|s| s.trim() == hash)
.unwrap_or(false);
ok
}
pub fn write_stamp(hash: &str) -> Result<()> {
fs::write(stamp_path(hash), hash)?;
Ok(())
}
pub fn invalidate(hash: &str) {
let _ = fs::remove_file(stamp_path(hash));
}
pub fn info() -> Result<()> {
let dir = cache_dir();
gap!();
section!("cache");
if !dir.exists() {
detail!("empty — nothing compiled yet");
gap!();
return Ok(());
}
let entries: Vec<_> = fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.collect();
let size = dir_size(&dir);
detail!("location {}", dir.display());
detail!("entries {}", entries.len());
detail!("size {}", fmt_bytes(size));
if !entries.is_empty() {
gap!();
let mut sorted = entries;
sorted.sort_by_key(|e| {
e.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
for e in sorted.iter().rev().take(5) {
let hash = e.file_name().to_string_lossy().to_string();
let ok = binary_path(&hash).exists();
let mark = if ok { crate::ui::ok("✔") } else { crate::ui::err("✘") };
let short_hash = if hash.len() >= 16 { &hash[..16] } else { &hash };
detail!("{} {}", mark, short_hash);
}
}
gap!();
hint!("clear with rfast clear");
Ok(())
}
pub fn clear() -> Result<()> {
let dir = cache_dir();
if !dir.exists() {
gap!();
detail!("cache is already empty");
gap!();
return Ok(());
}
let size = dir_size(&dir);
fs::remove_dir_all(&dir)?;
gap!();
success!("cache cleared (freed {})", crate::ui::hi(&fmt_bytes(size)));
gap!();
Ok(())
}
fn dir_size(p: &Path) -> u64 {
if p.is_file() {
return p.metadata().map(|m| m.len()).unwrap_or(0);
}
fs::read_dir(p)
.map(|it| it.filter_map(|e| e.ok()).map(|e| dir_size(&e.path())).sum())
.unwrap_or(0)
}
fn fmt_bytes(b: u64) -> String {
match b {
0..=1023 => format!("{b} B"),
1024..=1_048_575 => format!("{:.1} KB", b as f64 / 1024.0),
1_048_576..=1_073_741_823 => format!("{:.1} MB", b as f64 / 1_048_576.0),
_ => format!("{:.2} GB", b as f64 / 1_073_741_824.0),
}
}
pub fn short_cache_path(hash: &str) -> String {
#[cfg(windows)]
{
format!("%LOCALAPPDATA%\\rfast\\{}", &hash[..12])
}
#[cfg(not(windows))]
{
format!("~/.cache/rfast/{}", &hash[..12])
}
}
pub fn binary_path(hash: &str) -> PathBuf {
#[cfg(windows)]
{
project_dir(hash).join("target/debug/script.exe")
}
#[cfg(not(windows))]
{
project_dir(hash).join("target/debug/script")
}
}