# microsandbox-image

Pull OCI container images and cache ready-to-mount filesystem artifacts locally. This crate handles the full image lifecycle for [microsandbox](https://github.com/superradcompany/microsandbox) — from resolving a multi-platform manifest to producing per-layer EROFS images, fsmeta, and the VMDK descriptor used by the VM.

- **Multi-platform resolution** — automatically picks the right manifest for your OS and architecture
- **Parallel materialization** — layers download, verify, ingest, and materialize concurrently
- **Content-addressable caching** — duplicate layers across images are stored once, with cross-process safety via `flock()`
- **Progress streaming** — real-time events for download, layer materialization, and fsmeta/VMDK stitching stages

## Quick Start

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

use microsandbox_image::{GlobalCache, Platform, PullOptions, Registry};

let cache = GlobalCache::new(Path::new("/path/to/cache"))?;
let platform = Platform::host_linux();
let registry = Registry::new(platform, cache)?;

let reference = "docker.io/library/alpine".parse()?;
let result = registry.pull(&reference, &PullOptions::default()).await?;

// Materialized layer diff_ids, bottom-to-top
for diff_id in &result.layer_diff_ids {
    println!("{diff_id}");
}

// Parsed image config (env, cmd, entrypoint, user, etc.)
println!("entrypoint: {:?}", result.config.entrypoint);
println!("env: {:?}", result.config.env);
```

## Authentication

```rust
use microsandbox_image::RegistryAuth;

// Public registries — no credentials needed
let registry = Registry::new(platform, cache)?;

// Private registries
let auth = RegistryAuth::Basic {
    username: "user".into(),
    password: "token".into(),
};
let registry = Registry::builder(platform, cache).auth(auth).build()?;
```

## Pull Policies

Control when the crate contacts the registry vs. uses the local cache.

```rust
use microsandbox_image::{PullOptions, PullPolicy};

// Use cache if available, download otherwise (default)
let opts = PullOptions { pull_policy: PullPolicy::IfMissing, ..Default::default() };

// Always fetch a fresh manifest, but reuse cached layers by digest
let opts = PullOptions { pull_policy: PullPolicy::Always, ..Default::default() };

// Cache-only — error if the image isn't already cached
let opts = PullOptions { pull_policy: PullPolicy::Never, ..Default::default() };
```

## Progress Reporting

Stream progress events while the download runs in the background.

```rust
use microsandbox_image::PullProgress;

let (mut progress, task) = registry.pull_with_progress(&reference, &PullOptions::default());

tokio::spawn(async move {
    while let Some(event) = progress.recv().await {
        match event {
            PullProgress::Resolved { layer_count, .. } => {
                println!("Pulling {layer_count} layers");
            }
            PullProgress::LayerDownloadProgress { downloaded_bytes, total_bytes, .. } => {
                println!("Downloaded {downloaded_bytes}/{total_bytes:?} bytes");
            }
            PullProgress::Complete { .. } => {
                println!("Done!");
            }
            _ => {}
        }
    }
});

let result = task.await??;
```

## Multi-Platform Images

Fat manifests (OCI Image Index) are automatically resolved to your target platform. `Platform::host_linux()` detects the current architecture by default.

```rust
use microsandbox_image::Platform;

// Auto-detect (e.g. linux/amd64 or linux/arm64)
let platform = Platform::host_linux();

// Explicit platform
let platform = Platform::new("linux", "arm64");

// With variant (e.g. armv7)
let platform = Platform::with_variant("linux", "arm", "v7");
```
