v1.1: harden recovery and control boundaries
WP-D and WP-E complete the planned v1.1 implementation packages. WP-D — recovery honesty: - backups verify the completed copy with PRAGMA integrity_check - corruption missed by quick_check is detected by the full check - existing good backups remain protected - oversized exports are still delivered but declare whether this version can import them, while the 20 MB import limit remains unchanged - backup was exercised through the real browser UI on both the normal campaign database and a campaign-shaped database over 100 MB WP-E — control-boundary contrast: - interactive control boundaries meet the WCAG 1.4.11 3:1 target - the contrast audit is now a failing gate rather than an advisory - rendered browser measurements pass for the composer, controls, tabs and nav - text contrast and focus visibility remain intact - owner reviewed and approved the before/after screenshots Reports: - planning/reports/v1.1/V1.1-WP-D-REPORT.md - planning/reports/v1.1/V1.1-WP-E-REPORT.md All planned v1.1 work packages A-E are now complete. Release validation has not yet begun.
This commit is contained in:
@@ -28,7 +28,9 @@ repair. Refusing a whole campaign because a search index would not build would
|
||||
trade the valuable thing for the cheap one.
|
||||
"""
|
||||
|
||||
from fastapi import Body, Depends, Request
|
||||
import json
|
||||
|
||||
from fastapi import Body, Depends, Request, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ... import bundle, head, limits, models, schemas
|
||||
@@ -46,8 +48,38 @@ def export_adventure(
|
||||
|
||||
`app/bundle.py` owns the format, in all three of its versions. A backup
|
||||
outlives the schema, so no call site decides anything about its shape.
|
||||
|
||||
**v1.1 WP-D: the export also says whether this version could import it back.**
|
||||
A campaign large enough to pass `limits.MAX_IMPORT_BODY_BYTES` still exports —
|
||||
the file is complete and not damaged, and refusing to write it would destroy
|
||||
the only copy the reader was trying to make. What it cannot do is come back
|
||||
in here, and the reader is told that at the moment they take it rather than
|
||||
at the moment they need it.
|
||||
|
||||
It travels in headers, not in the body. The body is the bundle, the browser
|
||||
saves exactly those bytes as the file, and a warning inside it would become
|
||||
part of a portable story file and of every checksum taken over one.
|
||||
|
||||
The size measured is the compact serialisation, because that is both what
|
||||
this response sends and what the browser POSTs back on import, which is what
|
||||
`BodySizeLimitMiddleware` weighs. The pretty-printed file the reader
|
||||
downloads is larger, and is not what import reads.
|
||||
"""
|
||||
return bundle.export(db, adv)
|
||||
payload = bundle.export(db, adv)
|
||||
# Serialised exactly as Starlette's JSONResponse would, so the bytes counted
|
||||
# are the bytes sent.
|
||||
body = json.dumps(payload, ensure_ascii=False, allow_nan=False,
|
||||
separators=(",", ":")).encode("utf-8")
|
||||
limit = limits.MAX_IMPORT_BODY_BYTES
|
||||
importable = len(body) <= limit
|
||||
headers = {
|
||||
"X-Export-Bytes": str(len(body)),
|
||||
"X-Import-Limit-Bytes": str(limit),
|
||||
"X-Importable-By-This-Version": "true" if importable else "false",
|
||||
}
|
||||
if not importable:
|
||||
headers["X-Export-Warning"] = limits.oversized_export_warning(len(body), limit)
|
||||
return Response(content=body, media_type="application/json", headers=headers)
|
||||
|
||||
|
||||
@router.post("/import", response_model=schemas.ImportedAdventureOut, status_code=201)
|
||||
|
||||
Reference in New Issue
Block a user