Skip to content
Merged
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 13 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
[package]
name = "cache-manager"
version = "0.2.1"
version = "0.3.0"
edition = "2024"
description = "Simple managed directory system for project-scoped caches with optional eviction policies."
license = "MIT OR Apache-2.0"
authors = ["Jeremy <jeremy@example.com>"]
authors = ["Jeremy Harris <jeremy.harris@zenosmosis.com>"]
repository = "https://github.com/jzombie/rust-cache-manager"
categories = ["development-tools", "caching", "filesystem"]
keywords = [
"cache",
"artifact-cache",
"eviction-policy",
"filesystem",
"workspace",
]
keywords = ["cache", "artifact-cache", "eviction-policy", "filesystem", "workspace"]

# Optional/test dependencies
[workspace.dependencies]
tempfile = "3.27.0"

[features]
default = []
process-scoped-cache = ["dep:tempfile"]

[dependencies]
tempfile = { workspace = true, optional = true }

[dev-dependencies]
tempfile = "3.25.0"
tempfile.workspace = true
109 changes: 97 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

Directory-based cache and artifact path management with discovered `.cache` roots, grouped cache paths, and optional eviction on directory initialization.

**This crate is intentionally tool-agnostic** — it only manages cache/artifact directory layout and paths and does not assume or depend on any specific consumer tooling. Any tool or library that reads or writes files can use `cache-manager` to compute/manage project-scoped cache paths and apply eviction rules.

**It has zero runtime dependencies (standard library only for library consumers).**

It is suitable for:

- Artifact storage (build outputs, generated files, intermediate data, etc.).
- Monorepos or multi-crate workspaces that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`).
- **Tool-agnostic:** any tool or library that can write to the filesystem can use `cache-manager` as a managed cache/artifact path layout layer.
- **Zero runtime dependencies** in the standard install (library consumers use only the Rust standard library).
- **Optional feature `process-scoped-cache`:** adds one runtime dependency, [`tempfile`](https://docs.rs/tempfile), to support process/thread scoped sub-caches with automatic cleanup on normal shutdown.
- **Open-source + commercial-friendly licensing:** dual-licensed under MIT or Apache-2.0, so it can be used in open-source and commercial projects.
- **Built-in eviction policies:** enforce cache limits by file age, file count, and total bytes, with deterministic oldest-first trimming.
- **Predictable discovery + root control:** discover `<crate-root>/.cache` automatically or pin an explicit root with `CacheRoot::from_root(...)`.
- **Composable cache layout API:** create groups/subgroups and entry paths consistently across tools without custom path-joining logic.
- **Suitable for artifact storage** (build outputs, generated files, intermediate data, etc.).
- **Suitable for monorepos or multi-crate workspaces** that need centralized cache/artifact management via a shared root (for example with `CacheRoot::from_root(...)`). _This tool was designed to facilitate common cache directory management in a multi-crate workspace._

> Tested on macOS, Linux, and Windows.

Expand All @@ -35,10 +36,10 @@ use cache_manager::CacheRoot;
let root = CacheRoot::from_root("/tmp/project");
let group = root.group("artifacts/json");

// Create the group directory if needed.
// Create the group directory if needed
group.ensure_dir().expect("ensure group");

// `index.bin` is just an example artifact filename that another program might generate.
// `index.bin` is just an example artifact filename that another program might generate
let entry: std::path::PathBuf = group.touch("v1/index.bin").expect("touch entry");
println!("{}", entry.display());
```
Expand Down Expand Up @@ -69,6 +70,12 @@ println!("{}", entry_without_touch.display());
- **Create dirs + optional eviction:** `CacheRoot::ensure_group_with_policy`, `CacheGroup::ensure_dir_with_policy`
- **Create file (creates parents):** `CacheGroup::touch`

With feature `process-scoped-cache` enabled:

- **Process-scoped group:** `ProcessScopedCacheGroup::new`, `ProcessScopedCacheGroup::from_group`
- **Per-thread subgroup:** `ProcessScopedCacheGroup::thread_group`, `ProcessScopedCacheGroup::ensure_thread_group`
- **Per-thread entry helpers:** `ProcessScopedCacheGroup::thread_entry_path`, `ProcessScopedCacheGroup::touch_thread_entry`

> Note: eviction only runs when you pass a policy to the `*_with_policy` methods.

### Discovering cache paths
Expand All @@ -88,14 +95,16 @@ Behavior:
use cache_manager::CacheRoot;
use std::path::Path;

// Compute a path like <crate-root>/.cache/tool/data.bin without creating it.
// Compute a path like <crate-root>/.cache/tool/data.bin without creating it
let cache_path = CacheRoot::from_discovery()
.expect("discover cache root")
.cache_path("tool", "data.bin");
println!("cache path: {}", cache_path.display());

// Expected relative location under the discovered crate root:
assert!(cache_path.ends_with(Path::new(".cache").join("tool").join("data.bin")));
// The call only computes the path; it does not create files or directories.

// The call only computes the path; it does not create files or directories
assert!(!cache_path.exists());

// If you already have an absolute entry path, it's returned unchanged:
Expand Down Expand Up @@ -215,6 +224,82 @@ For `max_files` and `max_bytes`, files are evicted oldest-first by modified time
- Directories are not counted as bytes.
- Enforcement happens only during policy-aware `ensure_*_with_policy` calls (not continuously in the background).

### Optional process/thread scoped caches

Enable feature flag:

```bash
cargo add cache-manager --features process-scoped-cache
```

Or, if editing `Cargo.toml` manually:

```toml
[dependencies]
cache-manager = { version = "<latest>", features = ["process-scoped-cache"] }
```

Use `ProcessScopedCacheGroup` to create an auto-generated process subdirectory
under your assigned root/group, then derive a stable subgroup for each thread:

```rust
#[cfg(feature = "process-scoped-cache")]
fn main() {
use cache_manager::{CacheRoot, ProcessScopedCacheGroup};
use std::path::Path;

// 1) Build the root and the base group where process directories will live
let root = CacheRoot::from_root("/tmp/project");
let base_group = root.group("artifacts/session");

// 2) Create a process-scoped directory (name starts with `pid-<pid>-...`)
let scoped = ProcessScopedCacheGroup::new(&root, "artifacts/session")
.expect("create process-scoped cache");

// 3) Resolve this thread's subgroup and touch an entry under it
let thread_group = scoped.ensure_thread_group().expect("ensure thread group");
let entry = thread_group.touch("v1/index.bin").expect("touch thread entry");

// 4) Verify the static pieces of the structure
assert!(entry.starts_with(base_group.path()));
assert!(entry.ends_with(Path::new("v1/index.bin")));

// 5) Verify the dynamic thread segment (`thread-<n>`)
let thread_dir = entry
.parent()
.and_then(|p| p.parent())
.expect("thread dir");

assert!(thread_dir
.file_name()
.and_then(|s| s.to_str())
.expect("thread dir name")
.starts_with("thread-"));

// 6) Verify the dynamic process segment (`pid-<current-pid>-<random>`)
let process_dir = thread_dir.parent().expect("process dir");
let expected_pid_prefix = format!("pid-{}-", std::process::id());

assert!(process_dir
.file_name()
.and_then(|s| s.to_str())
.expect("process dir name")
.starts_with(&expected_pid_prefix));

// Example output path
println!("{}", entry.display());
}

#[cfg(not(feature = "process-scoped-cache"))]
fn main() {}
```

Behavior notes:

- Respects all configured roots/groups because process-scoped paths are always created under your provided `CacheRoot`/`CacheGroup`.
- The process subdirectory is deleted when the handle is dropped during normal process shutdown.
- Cleanup is best-effort; abnormal termination (for example `SIGKILL` or crash) can leave stale directories.

### Additional examples

Create or update a cache entry (ensures parent directories exist):
Expand Down
Loading
Loading