media:cpp globs, cache

This commit is contained in:
lovebird 2026-04-13 18:16:06 +02:00
parent 0e634546b1
commit 0b1f1e3e23
362 changed files with 996 additions and 2521 deletions

View File

@ -60,6 +60,36 @@ FetchContent_Declare(
FetchContent_MakeAvailable(cli11 asio nlohmann_json cpp_httplib)
# PicoSHA2 header-only SHA256 for cache keys.
FetchContent_Declare(
picosha2
GIT_REPOSITORY https://github.com/okdshin/PicoSHA2.git
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_GetProperties(picosha2)
if(NOT picosha2_POPULATED)
FetchContent_Populate(picosha2)
endif()
# p-ranav/glob same as packages/kbot/cpp (glob / rglob for **).
FetchContent_Declare(
pranav_glob
GIT_REPOSITORY https://github.com/p-ranav/glob.git
GIT_TAG master
GIT_SHALLOW TRUE
)
FetchContent_GetProperties(pranav_glob)
if(NOT pranav_glob_POPULATED)
FetchContent_Populate(pranav_glob)
endif()
add_library(pranav_glob STATIC ${pranav_glob_SOURCE_DIR}/source/glob.cpp)
target_include_directories(pranav_glob PUBLIC ${pranav_glob_SOURCE_DIR}/include)
target_compile_features(pranav_glob PUBLIC cxx_std_17)
if(MSVC)
target_compile_options(pranav_glob PRIVATE /permissive-)
endif()
# laserpants/dotenv-cpp load .env (same pattern as packages/kbot/cpp).
FetchContent_Declare(
laserpants_dotenv
@ -79,6 +109,8 @@ find_package(Vips REQUIRED)
add_executable(media-img
src/main.cpp
src/core/cache.cpp
src/core/glob_paths.cpp
src/core/resize.cpp
src/http/serve.cpp
src/ipc/ipc_serve.cpp
@ -87,6 +119,7 @@ add_executable(media-img
target_include_directories(media-img PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${asio_SOURCE_DIR}/asio/include
${picosha2_SOURCE_DIR}
)
target_compile_definitions(media-img PRIVATE
@ -107,6 +140,7 @@ target_link_libraries(media-img PRIVATE
nlohmann_json::nlohmann_json
httplib::httplib
laserpants::dotenv
pranav_glob
Vips::vips
)

View File

@ -11,7 +11,7 @@ CMake-based **`media-img`** binary: **CLI**, **HTTP REST** (`serve`), and **line
| CMake | ≥ 3.20 |
| C++ compiler | C++17 |
| **libvips** | **Required** — pkg-config (Unix) or `third_party/vips-dev-*` / `VIPS_ROOT` (Windows) |
| Git | For `FetchContent` (CLI11, Asio, httplib, json, dotenv) |
| Git | For `FetchContent` (CLI11, Asio, httplib, json, dotenv, [p-ranav/glob](https://github.com/p-ranav/glob), [PicoSHA2](https://github.com/okdshin/PicoSHA2) for cache keys) |
| Node.js | Optional — `npm run test:media` (Node 18+) |
### Installing libvips
@ -79,9 +79,104 @@ Sharp wraps libvips: **decode → process → encode**. We do the same with `vip
| `flip` / `flop` | `flip` / `flop` | |
| Letterbox | `background` | `#rrggbb` for `contain` |
**REST** `POST /v1/resize` accepts the same keys as columns in the table (plus `input`, `output` paths).
**REST** `POST /v1/resize` and **IPC** use the same JSON keys as the table, plus the fields in the **“Batch paths & cache”** section below.
**IPC** sends one JSON object per line with the same keys.
---
## Batch paths & cache: globs, variables, caching
Use this when **one** `resize` invocation (CLI, or a single REST/IPC request) should match **many** inputs or build **per-file** output paths. See also [docs/Examples.md](../docs/Examples.md) (TypeScript / Sharp reference).
### Summary
| Topic | What it does |
|-------|----------------|
| **Input glob** | `*` / `?` / `**` in `input` expand to a list of files ([p-ranav/glob](https://github.com/p-ranav/glob)). |
| **Destination variables** | `output` may contain `${SRC_DIR}`, `${SRC_NAME}`, `${SRC_FILE_EXT}` (or `&{…}`) — expanded **per matched input**. |
| **expand_glob** | JSON `false`: treat `input` / `output` as literal paths (no glob expansion). Templates still apply if `output` contains `${SRC_` / `&{SRC_`. |
| **Output cache** | SHA-256 key from input path + size + mtime + options; default dir `<cwd>/cache/images/`. |
**Not supported in C++:** Bash **extglob** (e.g. `*.+(jpg)`). Use `*.jpg`, `**/*.jpg`, or separate runs. **Bare** `{SRC_NAME}` without `$` / `&` is not a placeholder — use `${SRC_NAME}` or `&{SRC_NAME}`.
### Input globs
- **Syntax:** `*`, `?`, and `**` for recursion. Paths are resolved from the current working directory unless absolute.
- **Multiple files → directory output:** `output` must be an **existing** directory, **or** a **new** directory given with a **trailing** `/` or `\` (parent dirs are created).
- **Single file** from a literal path or a glob that matches one file: `output` can be a full file path, or a directory (trailing sep) to keep the original filename.
**CLI:** use positional `input`/`output` or `--src` / `--dst` together.
```bash
./dist/media-img resize './photos/**/*.jpg' ./out/
./dist/media-img resize --src './shots/*.png' --dst ./thumbs/
```
**JSON:** same strings in `"input"` and `"output"`. When `expand_glob` is true (default), glob expansion runs when the pattern contains `*`, `?`, or `**`.
### Destination variables (`${SRC_*}` / `&{SRC_*}`)
Placeholders are expanded **after** inputs are resolved (glob or single file). Each output path is built from the **absolute** input file for that row.
| Placeholder | Meaning |
|-------------|---------|
| **SRC_DIR** | Parent directory of the current input (generic path, `/` separators). |
| **SRC_NAME** | Filename **stem** without extension (`photo` for `photo.JPG`). |
| **SRC_FILE_EXT** | Extension **with** leading dot (e.g. `.jpg`), or empty if none. |
Use cases: write beside each source (`${SRC_DIR}/out/${SRC_NAME}.webp`), suffix stems (`${SRC_NAME}_thumb.jpg`), or change extension via `format` / path.
```bash
./dist/media-img resize --src ./photo.jpg --dst '${SRC_DIR}/${SRC_NAME}_medium.jpg' --max-width 800
./dist/media-img resize --src './shots/*.jpg' --dst '${SRC_DIR}/${SRC_NAME}.webp' --max-width 1920
```
**REST / IPC** — same strings in JSON (escape quotes in shell as needed):
```bash
curl -s -X POST http://127.0.0.1:8080/v1/resize \
-H 'Content-Type: application/json' \
-d '{"input":"/data/in.png","output":"/out/${SRC_NAME}_thumb.webp","max_width":256}'
```
**Responses:** `{"ok":true}` for a single output; if more than one file is produced, `{"ok":true,"count":N,"outputs":["..."]}`.
### JSON reference (batch + cache)
| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `input` | string | required | Source path or glob. |
| `output` | string | required | File path, directory, or template with `${SRC_*}` / `&{SRC_*}`. |
| `expand_glob` | bool | `true` | If `false`, no glob expansion; paths are literal. |
| `cache` | bool | `true` (or server default from `serve` / `ipc` flags) | Enable/disable cache for this request. |
| `cache_dir` | string | empty → `<cwd>/cache/images` (or server `--cache-dir`) | Root directory for cached blobs. |
All **resize** options (`max_width`, `fit`, …) participate in the same JSON body.
### Output cache
- **Default:** caching is **on**; root dir **`cache/images`** under the process **current working directory** (override with `--cache-dir` or JSON `cache_dir`).
- **Key:** **SHA-256** (PicoSHA2) over canonical input path, file size, modification time, and a stable encoding of **all** resize options — change any of these and you get a miss.
- **Storage:** `<cache_dir>/XX/<hex>` (two-letter shard).
- **Hit:** copy cached bytes to `output`; **libvips is not initialized** for that job.
- **Miss:** run resize, then **best-effort** store into cache (failure to store does not fail the request).
**CLI (`resize`):** `--no-cache`, `--cache-dir <path>`.
**`serve` / `ipc`:** same flags set **defaults** for requests that omit `cache` / `cache_dir`. Per-request JSON can still set `"cache": true` or `"cache_dir": "/path"` to override.
**Batch + cache:** glob batches run **sequentially** (one file after another); each file may hit or miss the cache independently.
### `serve` example with cache and glob
```bash
./dist/media-img serve --host 127.0.0.1 -p 8080 --cache-dir /var/cache/media-img
```
```bash
curl -s -X POST http://127.0.0.1:8080/v1/resize \
-H 'Content-Type: application/json' \
-d '{"input":"/data/in/*.jpg","output":"/data/out/","max_width":400,"cache":true,"cache_dir":"/var/cache/media-img"}'
```
## Concurrency
@ -174,14 +269,18 @@ curl -s -X POST http://127.0.0.1:8080/v1/resize \
-d '{"input":"/path/in.png","output":"/path/out.webp","max_width":400,"quality":80}'
```
Optional: `"cache":false`, `"expand_glob":false`, `"cache_dir":"..."` — see **Batch paths & cache** above.
### `ipc` — one JSON line per connection (TCP; Unix socket on Linux/macOS)
```bash
./dist/media-img ipc --host 127.0.0.1 -p 9333
./dist/media-img ipc --host 127.0.0.1 -p 9333 --cache-dir ./cache/images
# elsewhere: send a single line, read one line back, e.g.
# {"input":"/tmp/a.jpg","output":"/tmp/b.webp","max_width":320,"format":"webp"}
# {"input":"/tmp/a.jpg","output":"/tmp/b.webp","max_width":320,"format":"webp","cache":true}
```
Same JSON fields as REST (`input`, `output`, globs, `expand_glob`, `cache`, `cache_dir`, resize options).
### `kbot` — forward to another binary (optional)
Requires **`KBOT_EXE`** pointing at the kbot executable; remaining args are passed through.
@ -199,6 +298,8 @@ npm run test:media
Requires a built `dist/media-img` **linked against libvips** and fixture PNGs (`npm run generate:assets` if missing).
The suite covers **REST**, **IPC (TCP)**, optional **Unix socket** on non-Windows, **destination templates** (`npm run test:media:templates`), and **recursive glob + `${SRC_DIR}` / `${SRC_NAME}`** (`npm run test:media:glob` — outputs under `tests/assets/glob-in/**/out/`, gitignored, for manual inspection).
## License
See [LICENSE](LICENSE) in this directory when present.

View File

@ -1,50 +0,0 @@
# Media image service (C++) — rollout plan
## Goals
- **CLI**: resize files on disk (batch-friendly, scripts).
- **REST**: HTTP server for resize jobs (Node or other clients).
- **IPC**: async socket server — **Unix domain socket** on Linux/macOS; **TCP loopback** on Windows (Asio does not ship portable UDS on Windows; optional **named pipe** phase later).
## Dependencies (CMake / FetchContent)
| Component | Choice | Notes |
|-----------|--------|--------|
| CLI | [CLI11](https://github.com/CLIUtils/CLI11) | Same pattern as `kbot/cpp`. |
| Async I/O | [Asio](https://think-async.com/Asio/) (standalone) | UDS + accept loop; no Boost linkage. |
| HTTP | [cpp-httplib](https://github.com/yhirose/cpp-httplib) | Header-only REST; good for a dedicated worker. |
| JSON | [nlohmann/json](https://github.com/nlohmann/json) | Request/response bodies. |
| Images (v1) | [stb](https://github.com/nothings/stb) | No system install; PNG/JPEG in-tree. |
| Images (later) | **libvips** | Optional `find_package` / vcpkg when you need parity with Sharp speed/quality. |
## Phases
### Phase 0 — Scaffold (this PR)
- CMake presets `dev` / `release`, output `dist/media-img(.exe)`.
- `npm run build:release` green on Windows MSVC.
### Phase 1 — Core + CLI
- `resize` command: input path, output path, max width/height, format (png/jpeg).
- Single-threaded; deterministic errors to stderr.
### Phase 2 — REST
- `serve --bind --port`: `GET /health`, `POST /v1/resize` (JSON with paths or raw body + query params — v1 uses file paths for simplicity).
### Phase 3 — IPC
- `ipc --listen <path|host:port>`: line-delimited or length-prefixed JSON requests (documented in `docs/ipc-protocol.md` stub).
- Linux: Unix socket. Windows: TCP `127.0.0.1:<port>` (or named pipe in a follow-up).
### Phase 4 — Production hardening
- Optional libvips backend behind `MEDIA_USE_VIPS`.
- Worker pool, request limits, metrics.
- CI: Linux + Windows matrix.
## npm scripts
- `npm run build:release` — configure + build Release.
- `npm run run``dist/media-img --help`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Some files were not shown because too many files have changed in this diff Show More