MediaCleaner Pro is structured as an event-driven hexagonal monolith. The intent is clean internal boundaries that allow each layer to evolve independently — or be split into separate services later — without sacrificing the performance and deployment simplicity of a single native binary. The domain logic (image classification, duplicate detection, quality ranking) lives in a pure-Rust crate with zero I/O dependencies. All filesystem, database, and network concerns are isolated in a separate adapter crate. The Axum API layer sits on top, wiring everything together with Tokio async channels and a bounded streaming pipeline.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/xcoder-es/media-cleaner-pro/llms.txt
Use this file to discover all available pages before exploring further.
Layers
API Layer
Axum 0.7 handles all HTTP concerns: REST JSON endpoints, OpenAPI spec generation via
utoipa, and Server-Sent Events for real-time pipeline progress. No domain logic lives here.Domain Core (mc-core)
A pure-Rust library crate. No
tokio, no filesystem, no database. Declares all domain types and port traits as Rust traits, and implements all 10 pipeline stages as pure functions that take &ImageMetadata and return StageResult.Adapters (mc-infra)
Concrete implementations of every port trait:
tokio::fs filesystem, sha2-based exact hasher, dHash image hasher, rusqlite job repository, walkdir file scanner, and the in-process notification bus.Hexagonal Port / Adapter Pattern
All cross-boundary communication flows through Rust traits defined inmc-core/src/ports.rs. The root binary and mc-infra depend on these traits; mc-core itself depends on nothing outside the standard library and a handful of pure data crates (serde, thiserror, chrono).
FileSystem (async)
Abstracts all file and directory operations. The concrete adapter in mc-infra uses tokio::fs for non-blocking I/O.
JobRepository
Persists job state. The mc-infra adapter stores jobs in SQLite via rusqlite (bundled). The trait is designed so the backing store can be replaced with Supabase Postgres without touching domain code.
FileScanner
Recursively collects image paths matching a list of extensions.
ImageHasher
Computes perceptual difference hashes and Hamming distances for near-duplicate detection.
ExactHasher
Computes SHA-256 fingerprints for byte-perfect duplicate detection.
ImageDecoder
Decodes raw image bytes into a typed ImageInfo struct (width, height, format). Implemented in mc-infra using the image crate.
PipelineStage
The uniform interface that every one of the 10 processing stages implements.
StageContext carries the current PipelineConfig, the set of already-seen SHA-256 hashes, and the list of duplicate paths accumulated so far — all pure in-memory data, no I/O.
NotificationBus
Broadcasts PipelineEvent values to any registered listeners. The in-process implementation fans events out to the SSE stream that clients subscribe to via GET /api/progress.
PipelineEvent covers the full lifecycle: StageStarted, StageProgress, StageCompleted, JobCompleted, JobPaused, JobResumed, JobCancelled, Error, and Log.
Pipeline Concurrency Model
The streaming pipeline insrc/pipeline/mod.rs is designed around backpressure: no phase can outrun the one downstream of it, and memory usage is bounded regardless of how many images are in the source directory.
- Phase 2 uses
tokio::sync::Semaphore::new(16)— at most 16 files are read concurrently from disk, preventing fd exhaustion on large directories. - Phase 3 calls
tokio::task::block_in_placeto execute SHA-256 and dHash computation on the current Tokio worker thread without yielding, keeping CPU-bound work off the async scheduler so it does not block other async tasks from making progress. - Both channels are bounded with a capacity of 1 000 messages. If the stage loop falls behind, the reader pool backs off automatically — the semaphore ensures the system never buffers more than
16 + 1000images worth of raw bytes in memory at once. - CancellationToken (from
tokio-util) propagates a cancel signal across all phases without requiring explicitArc<AtomicBool>polling.
Job State Machine
Job lifecycle is managed byStateMachine in src/state/machine.rs. All transitions mutate AppState behind an RwLock and emit a structured log entry.
| Method | Effect |
|---|---|
StateMachine::start_job | Sets is_running = true, clears logs, resets all stage statuses to Pending |
StateMachine::start_stage(idx) | Marks stage idx as Running, records started_at timestamp |
StateMachine::update_stage_progress(idx, processed, total) | Updates progress percentage, recalculates speed and ETA |
StateMachine::complete_stage(idx) | Marks stage idx as Completed, sets progress to 100 % |
StateMachine::fail_stage(idx, error) | Marks stage as Failed, records the error string |
StateMachine::complete_job | Sets is_running = false |
StateMachine::pause_job | Sets is_paused = true |
StateMachine::resume_job | Sets is_paused = false |
StateMachine::cancel_job | Sets both is_running and is_paused to false; the CancellationToken signals the pipeline task to stop reading from the channel |
OpenAPI Generation
Endpoint types, request bodies, and response schemas are annotated withutoipa derive macros directly on the Rust handler functions and domain structs. The spec is assembled at startup and served at /api/openapi.json as OpenAPI 3.1 JSON.
openapi-typescript or any OpenAPI codegen tool.
Temporal Integration
docker-compose.yml includes a Temporal server (temporalio/auto-setup:1.24.2) backed by PostgreSQL. When the TEMPORAL_HOST environment variable is set (e.g. TEMPORAL_HOST=temporal:7233), the Rust binary connects to it for durable workflow orchestration — job steps can survive process restarts and machine reboots. In standalone mode (no TEMPORAL_HOST), the binary runs the in-process streaming pipeline directly without any external dependency.src/temporal/ module contains three files — client.rs, workflows.rs, and activities.rs — that implement the Temporal worker interface. The Temporal UI (port 8080 in Docker Compose) provides a visual workflow history dashboard.