Download server/main.py from devmandan/qr-scan-difficulty-check: direct link, hf CLI and curl.
- Browser
- Download file 6.33 kB
-
https://huggingface.co/spaces/devmandan/qr-scan-difficulty-check/resolve/main/server/main.py
- Command line
-
hf download hf://spaces/devmandan/qr-scan-difficulty-check/server/main.py
-
curl -L -o main.py https://huggingface.co/spaces/devmandan/qr-scan-difficulty-check/resolve/main/server/main.py
6.33 kB
| """QR Scan Difficulty Check: the measured bell, and a place on it for your own pictures. | |
| GET / the page (web/) | |
| POST /api/measure one picture (JPEG, PNG or WebP, at most 25 MB) as the request body; returns its reads by | |
| the five scanners at the seven frame sizes and its place on the scale | |
| GET /api/health what is running | |
| Pictures are read from a temporary file that is deleted when the reading ends; nothing is stored, and no access log is | |
| kept. | |
| """ | |
| import asyncio | |
| import os | |
| import tempfile | |
| from contextlib import asynccontextmanager | |
| from importlib.metadata import version | |
| from pathlib import Path | |
| from starlette.applications import Starlette | |
| from starlette.concurrency import run_in_threadpool | |
| from starlette.requests import ClientDisconnect, Request | |
| from starlette.responses import JSONResponse | |
| from starlette.routing import Mount, Route | |
| from starlette.staticfiles import StaticFiles | |
| from .place import place | |
| from .reading import FRAMES, SCANNERS, PictureError, Reader | |
| WEB = Path(__file__).resolve().parent.parent / "web" | |
| MAX_BYTES = 25 * 1024 * 1024 | |
| SLOTS = int(os.environ.get("QSD_SLOTS", "2")) # pictures read at once: one decoder worker each | |
| MAX_WAITING = 16 # pictures waiting beyond those (arriving, or waiting for a slot); more are turned away | |
| SLOT_WAIT_S = 10 # the longest a picture waits for a slot, so that a request ends within a minute | |
| BODY_S = 60 # the longest a picture may take to arrive; a stalled upload doesn't keep its place | |
| BUSY = "Many pictures are being read right now. Try again in a minute." | |
| CSP = "; ".join([ | |
| "default-src 'self'", "img-src 'self' blob: data:", "style-src 'self' 'unsafe-inline'", "script-src 'self'", | |
| "connect-src 'self'", "object-src 'none'", "base-uri 'none'", "form-action 'none'", | |
| "frame-ancestors 'self' https://huggingface.co", | |
| ]) | |
| STATE: dict = {} | |
| def sniff(data: bytes) -> str | None: | |
| """The picture's format from its first bytes: only JPEG, PNG and WebP are opened here (the page converts others).""" | |
| if data[:3] == b"\xff\xd8\xff": | |
| return ".jpg" | |
| if data[:8] == b"\x89PNG\r\n\x1a\n": | |
| return ".png" | |
| if data[:4] == b"RIFF" and data[8:12] == b"WEBP": | |
| return ".webp" | |
| return None | |
| def problem(status: int, message: str) -> JSONResponse: | |
| return JSONResponse({"error": message}, status_code=status, headers={"Cache-Control": "no-store"}) | |
| async def body(request: Request) -> bytes | None: | |
| """The request body, or None past MAX_BYTES (read no further).""" | |
| n = request.headers.get("content-length", "") | |
| if n.isdigit() and int(n) > MAX_BYTES: | |
| return None | |
| buf = bytearray() # one buffer, so a picture costs its own size in memory however small its chunks | |
| async for chunk in request.stream(): | |
| buf += chunk | |
| if len(buf) > MAX_BYTES: | |
| return None | |
| return bytes(buf) | |
| def read_picture(data: bytes, suffix: str) -> dict: | |
| with tempfile.NamedTemporaryFile(prefix="qsd-", suffix=suffix) as f: | |
| f.write(data) | |
| f.flush() | |
| result = STATE["reader"].read(f.name) | |
| result["order"] = FRAMES # the frame size behind each position of "reads" and "frames" | |
| result["place"] = place(result["reads"], FRAMES) | |
| result["count1280"] = sum(result["reads"][k][FRAMES.index(1280)] for k in SCANNERS) | |
| return result | |
| async def measure(request: Request) -> JSONResponse: | |
| if STATE["busy"] >= SLOTS + MAX_WAITING: | |
| return problem(503, BUSY) | |
| STATE["busy"] += 1 # counted from the first byte, so at most SLOTS + MAX_WAITING pictures are held in memory | |
| try: | |
| try: | |
| async with asyncio.timeout(BODY_S): | |
| data = await body(request) | |
| except TimeoutError: | |
| return problem(408, "This picture took too long to arrive. Try again, or send a smaller copy.") | |
| except ClientDisconnect: # the visitor left mid-upload: nobody will read this answer | |
| return problem(400, "The picture did not arrive.") | |
| if data is None: | |
| return problem(413, "This picture is over 25 MB.") | |
| suffix = sniff(data) | |
| if suffix is None: | |
| return problem(415, "Send a JPEG, PNG or WebP picture.") | |
| try: | |
| await asyncio.wait_for(STATE["gate"].acquire(), SLOT_WAIT_S) | |
| except TimeoutError: | |
| return problem(503, BUSY) | |
| try: | |
| result = await run_in_threadpool(read_picture, data, suffix) | |
| finally: | |
| STATE["gate"].release() | |
| except PictureError as e: | |
| return problem(422, str(e)) | |
| finally: | |
| STATE["busy"] -= 1 | |
| return JSONResponse(result, headers={"Cache-Control": "no-store"}) | |
| async def health(request: Request) -> JSONResponse: | |
| return JSONResponse({ | |
| "ok": True, "scanners": SCANNERS, "frames": FRAMES, "slots": SLOTS, | |
| "versions": {"opencv-contrib": version("opencv-contrib-python-headless"), "zxing-cpp": version("zxing-cpp"), | |
| "zxing": "3.5.2"}, | |
| }, headers={"Cache-Control": "no-store"}) | |
| async def lifespan(app): | |
| STATE["reader"] = await run_in_threadpool(Reader, SLOTS) | |
| STATE["gate"] = asyncio.Semaphore(SLOTS) | |
| STATE["busy"] = 0 | |
| yield | |
| STATE["reader"].close() | |
| class SecurityHeaders: | |
| """Plain ASGI middleware: the same headers on every response.""" | |
| def __init__(self, app): | |
| self.app = app | |
| async def __call__(self, scope, receive, send): | |
| if scope["type"] != "http": | |
| return await self.app(scope, receive, send) | |
| async def send_with_headers(message): | |
| if message["type"] == "http.response.start": | |
| message.setdefault("headers", []) | |
| message["headers"] += [(b"content-security-policy", CSP.encode()), | |
| (b"x-content-type-options", b"nosniff"), | |
| (b"referrer-policy", b"strict-origin-when-cross-origin")] | |
| await send(message) | |
| return await self.app(scope, receive, send_with_headers) | |
| app = SecurityHeaders(Starlette(lifespan=lifespan, routes=[ | |
| Route("/api/measure", measure, methods=["POST"]), | |
| Route("/api/health", health, methods=["GET"]), | |
| Mount("/", StaticFiles(directory=WEB, html=True), name="web"), | |
| ])) | |