Build and Push Docker Container / build-and-push (push) Successful in 2m37s
File cache: - Add my_modules/filecache with metadata, blob, validator and store layers - Cache convex.get_file via AsyncCache, TTL clamped to the file expiry - Skip caching for missing, incomplete or already expired records - Add a process-local, size-bounded LRU for immutable storage blobs - Build the cache stack from FILE_CACHE_* env vars in build_file_cache - Attach the cache to the app in my_modules/app/setup.py - Expose app-bound helpers in filecache/access.py for routes Serving: - Send strong ETag, Last-Modified and Cache-Control on served files - Answer If-None-Match and If-Modified-Since with 304 from metadata only - Avoid pulling the blob from Convex on a revalidation hit - Add a short Cloudflare-CDN-Cache-Control edge window for deleted files - Record cache_status in the wide event on a 304 - Invalidate cached metadata after file update and delete in both routes Text viewer: - Add my_modules/textview to render text/plain and text/markdown as HTML - Linkify http(s) URLs and trim trailing sentence punctuation - Resolve links pointing at this instance to the referenced file - Inline image previews, mark missing and expired targets - Cap resolution at MAX_LOOKUPS backend lookups per document - Serve the viewer only for real browser navigations, never for curl or CLI - Keep raw bytes for ?raw=1, downloads and Accept headers without text/html - Add templates/side/views/webpage/files/text_view.htm with a raw link Other: - Add tests for cache layers, serving, text rendering and the route - Bump multidict to 7.2.1 in uv.lock - Update the my_helpers submodule pointer
43 lines
1.4 KiB
Python
43 lines
1.4 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from .render import build_segments
|
|
|
|
# Text types that get the rendered viewer; anything else is served untouched.
|
|
VIEWABLE_CONTENT_TYPES = {'text/plain', 'text/markdown'}
|
|
|
|
_TRUTHY = {'1', 'true', 'yes'}
|
|
|
|
def _base_content_type(content_type: str) -> str:
|
|
return (content_type or '').split(';')[0].strip().lower()
|
|
|
|
def wants_text_view(content_type: str, accept_header: str, raw_param: str | None, force_download: bool) -> bool:
|
|
"""Only real browser navigations get HTML; curl, wget and the CLI keep the raw bytes."""
|
|
if force_download or (raw_param or '').lower() in _TRUTHY:
|
|
return False
|
|
if _base_content_type(content_type) not in VIEWABLE_CONTENT_TYPES:
|
|
return False
|
|
# Accept: */* (curl, wget, CLI clients) must not trigger the viewer.
|
|
return 'text/html' in (accept_header or '').lower()
|
|
|
|
def decode_text(raw: bytes) -> str:
|
|
"""Decode the full document; invalid bytes become replacement characters."""
|
|
return raw.decode('utf-8', errors='replace')
|
|
|
|
async def build_view_context(
|
|
raw: bytes,
|
|
file_name: str,
|
|
host: str,
|
|
root_path: str,
|
|
raw_url: str,
|
|
lookup: Callable[[str], Awaitable[dict | None]] | None,
|
|
) -> dict:
|
|
text = decode_text(raw)
|
|
segments = await build_segments(text, host=host, root_path=root_path, lookup=lookup)
|
|
return {
|
|
'file_name': file_name,
|
|
'segments': segments,
|
|
'raw_url': raw_url,
|
|
}
|