cache-manager 0.1.2

Simple managed directory system for project-scoped caches with optional eviction policies.
Documentation
# cache-manager

[![made-with-rust][rust-logo]][rust-src-page] [![crates.io][crates-badge]][crates-page] [![MIT licensed][mit-license-badge]][mit-license-page] [![Apache 2.0 licensed][apache-2.0-license-badge]][apache-2.0-license-page] [![Coverage][coveralls-badge]][coveralls-page]

Directory-based cache and artifact path management with crate-root discovery, 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(...)`).

> Tested on macOS, Linux, and Windows.

## Usage

Basic terminology and examples showing common operations:

### CacheRoot

The primary root type is `CacheRoot`, which represents a filesystem root
under which cache groups (`CacheGroup`) live.

### CacheGroup

A `CacheGroup` represents a subdirectory under a `CacheRoot` and manages
cache entries stored in that directory.

### Discovering cache paths

Discover a cache path for the current crate/workspace and resolve an entry path.

> Note: `discover_cache_path` only computes a filesystem path — it does not
> create directories or files.

Behavior:

- Searches upward from the current working directory for a `Cargo.toml` and
  uses that crate root when found; otherwise it falls back to the current
  working directory.
- The discovered root is canonicalized when possible to avoid surprising
  differences between logically-equal paths.
- If the `relative_path` argument is absolute, it is returned unchanged.

```rust
use cache_manager::CacheRoot;
use std::path::Path;

// Compute a path like <crate-root>/.cache/tool/data.bin without creating it.
let cache_path = CacheRoot::discover_cache_path(".cache", "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.
assert!(!cache_path.exists());

// If you already have an absolute entry path, it's returned unchanged:
let absolute = std::path::PathBuf::from("/tmp/custom/cache.json");
let kept = CacheRoot::discover_cache_path(".cache", &absolute);
assert_eq!(kept, absolute);
```

**Filesystem effects**

- **Pure (no I/O):** `CacheRoot::discover`, `CacheRoot::discover_cache_path`, `CacheRoot::cache_path`, `CacheRoot::group`, `CacheGroup::entry_path`, `CacheGroup::subgroup`
- **Create dirs:** `CacheRoot::ensure_group`, `CacheGroup::ensure_dir`
- **Create dirs + optional eviction:** `CacheRoot::ensure_group_with_policy`, `CacheGroup::ensure_dir_with_policy`
- **Create file (creates parents as needed):** `CacheGroup::touch`

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

### Eviction Policy

Use `EvictPolicy` with:

- `CacheGroup::ensure_dir_with_policy(...)`
- `CacheRoot::ensure_group_with_policy(...)`
- `CacheGroup::eviction_report(...)` to preview which files would be evicted.

Policy fields:

- `max_age`: remove files older than or equal to the age threshold.
- `max_files`: keep at most N files.
- `max_bytes`: keep total file bytes at or below the threshold.

Eviction order is always:

1. `max_age`
2. `max_files`
3. `max_bytes`

For `max_files` and `max_bytes`, files are evicted oldest-first by modified time (ascending), then by path for deterministic tie-breaking.

`eviction_report(...)` and `ensure_*_with_policy(...)` use the same selection logic.

#### How `max_bytes` works

- Scans regular files recursively under the managed directory.
- Sums `metadata.len()` across those files.
- If total exceeds `max_bytes`, removes files oldest-first until total is `<= max_bytes`.
- Directories are not counted as bytes.
- Enforcement happens only during policy-aware `ensure_*_with_policy` calls (not continuously in the background).

### More examples

Create a `CacheRoot` from an explicit path and apply an eviction policy to a group:

```rust
use cache_manager::{CacheRoot, EvictPolicy};
use std::time::Duration;

let root = CacheRoot::from_root("/tmp/project");
let group = root.group("artifacts");

let policy = EvictPolicy {
	max_files: Some(100),
	max_age: Some(Duration::from_secs(60 * 60 * 24 * 30)), // 30 days
	..Default::default()
};

group.ensure_dir_with_policy(Some(&policy)).expect("ensure and evict");
```

Preview which files would be removed without applying deletions:

```rust
use cache_manager::{CacheRoot, EvictPolicy};

let root = CacheRoot::from_root("/tmp/project");
let group = root.group("artifacts");
let policy = EvictPolicy {
	max_files: Some(10),
	..Default::default()
};

let report = group.eviction_report(&policy).expect("eviction report");
for p in report.marked_for_eviction {
	println!("would remove: {}", p.display());
}
```

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

```rust
use cache_manager::CacheRoot;

let root = CacheRoot::from_root("/tmp/project");
let group = root.group("artifacts/json");

let entry = group.touch("v1/index.bin").expect("touch entry");
println!("touched: {}", entry.display());
```

Per-subdirectory policies

Different subdirectories under the same `CacheRoot` can use independent policies; call `ensure_dir_with_policy` on each `CacheGroup` separately to apply per-group rules.

Get the root path

To obtain the underlying filesystem path for a `CacheRoot`, use `path()`:

```rust
use cache_manager::CacheRoot;

let root = CacheRoot::from_root("/tmp/project");
let root_path = root.path();
println!("root path: {}", root_path.display());
```

Also obtain a `CacheGroup` path and resolve an entry path under that group:

```rust
use cache_manager::CacheRoot;

let root = CacheRoot::from_root("/tmp/project");
let group = root.group("artifacts");

let group_path = group.path();
println!("group path: {}", group_path.display());

let entry_path = group.entry_path("v1/index.bin");
println!("entry path: {}", entry_path.display());
```

## License

`cache-manager` is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).

See [LICENSE-APACHE](./LICENSE-APACHE) and [LICENSE-MIT](./LICENSE-MIT) for details.

[rust-src-page]: https://www.rust-lang.org/
[rust-logo]: https://img.shields.io/badge/Made%20with-Rust-black

[crates-page]: https://crates.io/crates/cache-manager
[crates-badge]: https://img.shields.io/crates/v/cache-manager.svg

[mit-license-page]: ./LICENSE-MIT
[mit-license-badge]: https://img.shields.io/badge/license-MIT-blue.svg

[apache-2.0-license-page]: ./LICENSE-APACHE
[apache-2.0-license-badge]: https://img.shields.io/badge/license-Apache%202.0-blue.svg

[coveralls-page]: https://coveralls.io/github/jzombie/rust-cache-manager?branch=main
[coveralls-badge]: https://img.shields.io/coveralls/github/jzombie/rust-cache-manager