withcache 0.10.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {withcache-0.10.0 → withcache-0.12.0}/PKG-INFO +22 -21
- {withcache-0.10.0 → withcache-0.12.0}/README.md +21 -20
- {withcache-0.10.0 → withcache-0.12.0}/shim/build.zig.zon +1 -1
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/__init__.py +5 -4
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_api.py +33 -50
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_app.py +349 -145
- withcache-0.12.0/src/withcache/_table_state.py +152 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_templates/ui/_layout.html +12 -20
- withcache-0.12.0/src/withcache/_templates/ui/_table_macros.html +126 -0
- withcache-0.12.0/src/withcache/_templates/ui/catalog.html +195 -0
- withcache-0.12.0/src/withcache/_templates/ui/dashboard.html +142 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_templates/ui/misses.html +32 -15
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_templates/ui/settings.html +84 -47
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/client.py +11 -15
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/server.py +3 -63
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_admin_forms.py +145 -53
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_catalog_api.py +40 -8
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_scaffold.py +11 -11
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_settings_persistence.py +16 -16
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_ui_pages.py +48 -28
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_withcache.py +0 -83
- withcache-0.10.0/src/withcache/_templates/ui/cached.html +0 -78
- withcache-0.10.0/src/withcache/_templates/ui/catalog.html +0 -269
- withcache-0.10.0/src/withcache/_templates/ui/downloads.html +0 -100
- {withcache-0.10.0 → withcache-0.12.0}/.gitignore +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/LICENSE +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/deploy/Containerfile +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/deploy/compose.local-build.yml +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/deploy/compose.yml +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/deploy/envvars.example +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/hatch_build.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/pyproject.toml +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/shim/build.zig +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/shim/shim.zig +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_settings_store.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_shim.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/_templates/ui/login.html +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/oras.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/static/bootstrap-icons.min.css +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/static/bootstrap.min.css +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_differential.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_blob.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_fastapi_uvicorn_smoke.py +0 -0
- {withcache-0.10.0 → withcache-0.12.0}/tests/test_oras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/withcache
|
|
6
6
|
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
@@ -46,21 +46,23 @@ Artifacts are cached **by their origin URL as a key**; the shim opts in by
|
|
|
46
46
|
re-pointing the URL at the cache. No transparent proxy, no TLS interception, no
|
|
47
47
|
client CA. The URL is a lookup key, not a connection target.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
only box that needs internet egress
|
|
55
|
-
never write to it.
|
|
49
|
+
A miss falls through to origin (the caller gets its file straight away) and
|
|
50
|
+
withcache records the miss on the operator's **`/ui/misses`** page. The operator
|
|
51
|
+
reviews the miss list, picks what's worth caching, and one click **Fetch**s a URL
|
|
52
|
+
into the catalog + downloads it. Every entry in the catalog is a byte-perfect
|
|
53
|
+
copy of the origin at the time the operator hit Download; subsequent requests
|
|
54
|
+
hit the local cache. The cache-host is the only box that needs internet egress
|
|
55
|
+
(and any vendor credentials); clients never write to it.
|
|
56
56
|
|
|
57
57
|
## Why not just curl + a caching proxy?
|
|
58
58
|
|
|
59
59
|
For `https://` (i.e. every vendor download) a forward proxy can't cache without
|
|
60
60
|
**SSL-bump / MITM**: curl tunnels TLS end-to-end via `CONNECT`, so the proxy
|
|
61
61
|
only sees ciphertext. The shim sidesteps that entirely by *re-pointing the URL*
|
|
62
|
-
to the cache instead of intercepting the connection. And
|
|
63
|
-
|
|
62
|
+
to the cache instead of intercepting the connection. And a proxy that
|
|
63
|
+
auto-fetches everything a client asks for isn't what you want in a lab; the
|
|
64
|
+
**operator-curated** model here means only bytes the operator chose live on
|
|
65
|
+
disk.
|
|
64
66
|
|
|
65
67
|
## Components
|
|
66
68
|
|
|
@@ -117,9 +119,8 @@ WITHCACHE_ADMIN_PASSWORD=change-me withcache-server --data-dir ./data --port 808
|
|
|
117
119
|
Data (blobs + `cache.db` + `session-secret`) lives in the `/data` volume (or
|
|
118
120
|
`--data-dir`). Artifacts are immutable per version, so there's no cache
|
|
119
121
|
invalidation. `--workers N` sets the number of concurrent download workers,
|
|
120
|
-
`--
|
|
121
|
-
|
|
122
|
-
and you free space by deleting artifacts in the UI.
|
|
122
|
+
`--max-bytes` (e.g. `50G`) caps the cache: when full it refuses new fills (no
|
|
123
|
+
auto-eviction), and you free space by deleting artifacts in the UI.
|
|
123
124
|
|
|
124
125
|
## Use the shims (transparent `curl` / `wget`)
|
|
125
126
|
|
|
@@ -245,10 +246,10 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
245
246
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
246
247
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
247
248
|
- **Cached** (landing): URL, size, **hits** (times served) and **misses** (times requested before it was cached), SHA-256, fetched-at, each with **Delete** to free space.
|
|
248
|
-
- **
|
|
249
|
-
- **
|
|
250
|
-
- **
|
|
251
|
-
- **
|
|
249
|
+
- **Downloads**: live progress bars, `queued/running/completed/cancelled/failed`, **Cancel**, and **Clear finished**. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on [bty]'s job managers.
|
|
250
|
+
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
251
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configurable via `$WITHCACHE_CATALOG_URL` or the subnav Set&fetch input); pre-seed by URL via the "Add image from oras" or "Add HTTPS entry" forms, then **Download** each entry.
|
|
252
|
+
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
252
253
|
|
|
253
254
|
## Auth
|
|
254
255
|
|
|
@@ -288,10 +289,10 @@ url = client.serve_url("http://cache:8081", origin) or origin
|
|
|
288
289
|
```
|
|
289
290
|
|
|
290
291
|
`is_cached()` is a graceful `HEAD` (a miss, timeout, or unreachable cache all
|
|
291
|
-
return `False`, so you fall back to the origin)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
so consumers stay in lockstep with the cache-host.
|
|
292
|
+
return `False`, so you fall back to the origin). Since v0.10.0 a miss is
|
|
293
|
+
recorded on the cache-host's `/ui/misses` page but no background fetch fires
|
|
294
|
+
-- the operator explicitly chooses what to Download. The encoding is shared
|
|
295
|
+
with the shims and server, so consumers stay in lockstep with the cache-host.
|
|
295
296
|
|
|
296
297
|
### Pull an `oras://` artifact (oras + client together)
|
|
297
298
|
|
|
@@ -23,21 +23,23 @@ Artifacts are cached **by their origin URL as a key**; the shim opts in by
|
|
|
23
23
|
re-pointing the URL at the cache. No transparent proxy, no TLS interception, no
|
|
24
24
|
client CA. The URL is a lookup key, not a connection target.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
only box that needs internet egress
|
|
32
|
-
never write to it.
|
|
26
|
+
A miss falls through to origin (the caller gets its file straight away) and
|
|
27
|
+
withcache records the miss on the operator's **`/ui/misses`** page. The operator
|
|
28
|
+
reviews the miss list, picks what's worth caching, and one click **Fetch**s a URL
|
|
29
|
+
into the catalog + downloads it. Every entry in the catalog is a byte-perfect
|
|
30
|
+
copy of the origin at the time the operator hit Download; subsequent requests
|
|
31
|
+
hit the local cache. The cache-host is the only box that needs internet egress
|
|
32
|
+
(and any vendor credentials); clients never write to it.
|
|
33
33
|
|
|
34
34
|
## Why not just curl + a caching proxy?
|
|
35
35
|
|
|
36
36
|
For `https://` (i.e. every vendor download) a forward proxy can't cache without
|
|
37
37
|
**SSL-bump / MITM**: curl tunnels TLS end-to-end via `CONNECT`, so the proxy
|
|
38
38
|
only sees ciphertext. The shim sidesteps that entirely by *re-pointing the URL*
|
|
39
|
-
to the cache instead of intercepting the connection. And
|
|
40
|
-
|
|
39
|
+
to the cache instead of intercepting the connection. And a proxy that
|
|
40
|
+
auto-fetches everything a client asks for isn't what you want in a lab; the
|
|
41
|
+
**operator-curated** model here means only bytes the operator chose live on
|
|
42
|
+
disk.
|
|
41
43
|
|
|
42
44
|
## Components
|
|
43
45
|
|
|
@@ -94,9 +96,8 @@ WITHCACHE_ADMIN_PASSWORD=change-me withcache-server --data-dir ./data --port 808
|
|
|
94
96
|
Data (blobs + `cache.db` + `session-secret`) lives in the `/data` volume (or
|
|
95
97
|
`--data-dir`). Artifacts are immutable per version, so there's no cache
|
|
96
98
|
invalidation. `--workers N` sets the number of concurrent download workers,
|
|
97
|
-
`--
|
|
98
|
-
|
|
99
|
-
and you free space by deleting artifacts in the UI.
|
|
99
|
+
`--max-bytes` (e.g. `50G`) caps the cache: when full it refuses new fills (no
|
|
100
|
+
auto-eviction), and you free space by deleting artifacts in the UI.
|
|
100
101
|
|
|
101
102
|
## Use the shims (transparent `curl` / `wget`)
|
|
102
103
|
|
|
@@ -222,10 +223,10 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
222
223
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
223
224
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
224
225
|
- **Cached** (landing): URL, size, **hits** (times served) and **misses** (times requested before it was cached), SHA-256, fetched-at, each with **Delete** to free space.
|
|
225
|
-
- **
|
|
226
|
-
- **
|
|
227
|
-
- **
|
|
228
|
-
- **
|
|
226
|
+
- **Downloads**: live progress bars, `queued/running/completed/cancelled/failed`, **Cancel**, and **Clear finished**. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on [bty]'s job managers.
|
|
227
|
+
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
228
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configurable via `$WITHCACHE_CATALOG_URL` or the subnav Set&fetch input); pre-seed by URL via the "Add image from oras" or "Add HTTPS entry" forms, then **Download** each entry.
|
|
229
|
+
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
229
230
|
|
|
230
231
|
## Auth
|
|
231
232
|
|
|
@@ -265,10 +266,10 @@ url = client.serve_url("http://cache:8081", origin) or origin
|
|
|
265
266
|
```
|
|
266
267
|
|
|
267
268
|
`is_cached()` is a graceful `HEAD` (a miss, timeout, or unreachable cache all
|
|
268
|
-
return `False`, so you fall back to the origin)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
so consumers stay in lockstep with the cache-host.
|
|
269
|
+
return `False`, so you fall back to the origin). Since v0.10.0 a miss is
|
|
270
|
+
recorded on the cache-host's `/ui/misses` page but no background fetch fires
|
|
271
|
+
-- the operator explicitly chooses what to Download. The encoding is shared
|
|
272
|
+
with the shims and server, so consumers stay in lockstep with the cache-host.
|
|
272
273
|
|
|
273
274
|
### Pull an `oras://` artifact (oras + client together)
|
|
274
275
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.name = .withcache_shim,
|
|
3
3
|
// Zig requires a literal here; keep it in lockstep with the project's
|
|
4
4
|
// single source (src/withcache/__init__.py) via `make bump` / `make version-check`.
|
|
5
|
-
.version = "0.
|
|
5
|
+
.version = "0.12.0",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
a cache-host (build serve URLs, probe what's cached) without re-implementing
|
|
8
8
|
the ``/b/`` URL scheme.
|
|
9
9
|
- ``withcache.oras``: OCI registry adapter. Parses ``oras://...`` references
|
|
10
|
-
and resolves them to a plain HTTPS blob URL + bearer headers. The
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
and resolves them to a plain HTTPS blob URL + bearer headers. The
|
|
11
|
+
cache-host's DownloadManager invokes it when an operator hits Download
|
|
12
|
+
on an ``oras://`` catalog entry; library consumers (e.g. ``bty``) import
|
|
13
|
+
it to validate catalog entries and pre-resolve content digests.
|
|
13
14
|
|
|
14
15
|
Since v0.9.0 the daemon runs on FastAPI + Jinja + Bootstrap 5 + htmx
|
|
15
16
|
(matching bty-web + nbdmux, the eventual ``trio-common`` extraction
|
|
@@ -21,6 +22,6 @@ inherit the framework floor.
|
|
|
21
22
|
from . import oras
|
|
22
23
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
23
24
|
|
|
24
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.12.0"
|
|
25
26
|
|
|
26
27
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -11,15 +11,15 @@ stay byte-identical:
|
|
|
11
11
|
decorative; the b64 token is the source of truth.
|
|
12
12
|
- Cache hit -> 200 + ``Content-Type`` + ``Content-Length`` +
|
|
13
13
|
``X-Withcache-Sha256`` + streamed body (64 KiB chunks).
|
|
14
|
-
- Cache miss -> 404 with
|
|
15
|
-
side-effect
|
|
16
|
-
``
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
- Cache miss -> 404 with a message pointing at ``/ui/catalog``'s
|
|
15
|
+
Download button, plus a side-effect ``record_miss`` write so the
|
|
16
|
+
``/ui/misses`` page shows the operator what clients tried to
|
|
17
|
+
fetch that isn't in the catalog yet.
|
|
18
|
+
|
|
19
|
+
Since v0.10.0 there is no auto-fetch on miss: the operator
|
|
20
|
+
explicitly Downloads. The Miss page's Fetch button (retargeted in
|
|
21
|
+
v0.11.0) promotes a missed URL to a catalog entry + enqueues the
|
|
22
|
+
Download in one click.
|
|
23
23
|
|
|
24
24
|
Streaming shape: FastAPI ``StreamingResponse`` with a generator
|
|
25
25
|
that reads chunks from the on-disk blob at 64 KiB reads;
|
|
@@ -37,7 +37,7 @@ from typing import Any
|
|
|
37
37
|
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
38
38
|
from fastapi.responses import JSONResponse, PlainTextResponse, Response, StreamingResponse
|
|
39
39
|
|
|
40
|
-
from .server import CHUNK, CatalogState,
|
|
40
|
+
from .server import CHUNK, CatalogState, _oras_tag_moved, _serialise_catalog
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _decode_blob_origin(path: str, query: str) -> str:
|
|
@@ -78,7 +78,6 @@ def _serve_blob(
|
|
|
78
78
|
return PlainTextResponse("missing url\n", status_code=400)
|
|
79
79
|
|
|
80
80
|
store = request.app.state.store
|
|
81
|
-
streams: StreamRegistry = request.app.state.streams
|
|
82
81
|
|
|
83
82
|
row = store.get_blob(url)
|
|
84
83
|
if row is not None and _oras_tag_moved(url, row["sha256"]):
|
|
@@ -105,35 +104,20 @@ def _serve_blob(
|
|
|
105
104
|
|
|
106
105
|
if head_only:
|
|
107
106
|
# No body; the shim's HEAD probe doesn't count as a served
|
|
108
|
-
# download so record_hit
|
|
107
|
+
# download so record_hit is skipped.
|
|
109
108
|
return Response(status_code=200, media_type=media_type, headers=headers)
|
|
110
109
|
|
|
111
110
|
store.record_hit(row["key"])
|
|
112
|
-
client = f"{request.client.host}:{request.client.port}" if request.client else "unknown"
|
|
113
|
-
stream = streams.start(url=url, client=client, total=row["size"])
|
|
114
111
|
|
|
115
112
|
def _chunks() -> Iterator[bytes]:
|
|
116
113
|
"""Read the blob in 64 KiB chunks. StreamingResponse iterates
|
|
117
|
-
this + writes each chunk to the client.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
with open(path, "rb") as f:
|
|
125
|
-
while True:
|
|
126
|
-
chunk = f.read(CHUNK)
|
|
127
|
-
if not chunk:
|
|
128
|
-
break
|
|
129
|
-
yield chunk
|
|
130
|
-
sent += len(chunk)
|
|
131
|
-
ticks += 1
|
|
132
|
-
if ticks % StreamRegistry.PROGRESS_STRIDE == 0:
|
|
133
|
-
streams.bump(stream.id, sent)
|
|
134
|
-
streams.bump(stream.id, sent)
|
|
135
|
-
finally:
|
|
136
|
-
streams.finish(stream.id)
|
|
114
|
+
this + writes each chunk to the client."""
|
|
115
|
+
with open(path, "rb") as f:
|
|
116
|
+
while True:
|
|
117
|
+
chunk = f.read(CHUNK)
|
|
118
|
+
if not chunk:
|
|
119
|
+
break
|
|
120
|
+
yield chunk
|
|
137
121
|
|
|
138
122
|
return StreamingResponse(_chunks(), media_type=media_type, headers=headers)
|
|
139
123
|
|
|
@@ -141,10 +125,9 @@ def _serve_blob(
|
|
|
141
125
|
def register_api_routes(app: FastAPI) -> None:
|
|
142
126
|
"""Attach the byte-serving routes to ``app``.
|
|
143
127
|
|
|
144
|
-
Runtime objects (``store``, ``mgr``, ``streams
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
the app.
|
|
128
|
+
Runtime objects (``store``, ``mgr``, ``streams``) are read from
|
|
129
|
+
``app.state`` at request time so tests + the lifespan hook can
|
|
130
|
+
inject fresh instances without rebuilding the app.
|
|
148
131
|
"""
|
|
149
132
|
|
|
150
133
|
@app.get("/blob", response_class=Response)
|
|
@@ -212,25 +195,25 @@ def register_api_routes(app: FastAPI) -> None:
|
|
|
212
195
|
|
|
213
196
|
Open route: bty polls this from a sibling container without a
|
|
214
197
|
session (mirrors the ``/blob`` byte-serving surface's trust
|
|
215
|
-
model -- LAN-only, no auth on reads). Returns
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
198
|
+
model -- LAN-only, no auth on reads). Returns only entries
|
|
199
|
+
whose bytes are on disk in withcache's cache. Since v0.11.0
|
|
200
|
+
"presence in this list" IS the readiness signal: staged
|
|
201
|
+
entries (added but not yet Downloaded) are invisible to trio
|
|
202
|
+
consumers so bty + nbdmux can trust every entry they see is
|
|
203
|
+
flashable / exportable. Operators see the staged view on
|
|
204
|
+
withcache's own ``/ui/catalog``.
|
|
205
|
+
|
|
206
|
+
No pagination -- the catalog is small (dozens of entries at
|
|
222
207
|
most).
|
|
223
208
|
"""
|
|
224
209
|
cs: CatalogState = request.app.state.catalog
|
|
225
210
|
store = request.app.state.store
|
|
226
211
|
entries_out: list[dict[str, Any]] = []
|
|
227
212
|
for e in cs.entries:
|
|
228
|
-
enriched = dict(e)
|
|
229
213
|
fetch_url = e.get("resolved_src") or e.get("src") or ""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
entries_out.append(enriched)
|
|
214
|
+
if not fetch_url or store.get_blob(fetch_url) is None:
|
|
215
|
+
continue
|
|
216
|
+
entries_out.append(dict(e))
|
|
234
217
|
return JSONResponse(
|
|
235
218
|
{
|
|
236
219
|
"url": cs.url,
|