from pathlib import Path from typing import Any from urllib.parse import quote, urlparse from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, Response from . import runtime, source_files from .doc_store import get_doc, random_doc, variants_for_title from .document_identity import content_hash from .storage_lock import serving_lock router = APIRouter() @router.get("/api/preview/{doc_id}") async def preview(doc_id: str, request: Request): if runtime.index_status() == "restoring": return JSONResponse({"error": "index restore in progress"}, status_code=503) with serving_lock(): stored = get_doc(doc_id) if stored: payload = await preview_payload(stored) content_hash = str(payload["content_hash"]) etag = f'"{content_hash}"' if request.headers.get("if-none-match") == etag: return Response(status_code=304, headers={"ETag": etag, "Cache-Control": "private, max-age=0, must-revalidate"}) first_page = next((file.get("initial_page") or file.get("pages", [None])[0] for file in payload.get("source_files", []) if file.get("initial_page") or file.get("pages")), None) headers = {"ETag": etag, "Cache-Control": "private, max-age=0, must-revalidate"} if first_page: headers["Link"] = f"<{first_page}>; rel=preload; as=image" return JSONResponse(payload, headers=headers) return JSONResponse({"error": "not found"}, status_code=404) @router.get("/api/random") async def random_preview(): if runtime.index_status() == "restoring": return JSONResponse({"error": "index restore in progress"}, status_code=503) with serving_lock(): stored = random_doc() if stored: return await preview_payload(stored) return JSONResponse({"error": "not found"}, status_code=404) async def preview_payload(source: dict[str, Any]) -> dict[str, Any]: doc_id = str(source.get("doc_id") or "") title = source.get("title") variants: list[dict[str, Any]] = [] if title: with serving_lock(): variants = variants_for_title(str(title), doc_id, source.get("publication_name")) source_items = [] for index, url in enumerate(source.get("source_files") or []): source_url = str(url) hostname = (urlparse(source_url).hostname or "").lower() entry = await source_files.source_mirror_entry(source_url) if hostname in {"raw.githubusercontent.com", "github.com"} else None page_paths = entry.get("page_previews", {}).get("paths", []) if entry else [] pages = [ f"{source_files.SOURCE_MIRROR_BASE_URL}/{quote(path, safe='/')}" for path in page_paths if isinstance(path, str) and path.startswith("archives") and ".." not in Path(path).parts ] item = {"index": index, "name": Path(source_url.split("?", 1)[0]).name or f"文件 {index + 1}", "url": source_url} mirror_path = entry.get("path") if entry else None if (not source_url.lower().split("?", 1)[0].endswith(".pdf") and isinstance(mirror_path, str) and mirror_path.startswith("archives") and ".." not in Path(mirror_path).parts): item["preview_url"] = f"{source_files.SOURCE_MIRROR_BASE_URL}/{quote(mirror_path, safe='/')}" if pages: item["page_count"] = len(page_paths) article_page = int((source.get("article") or {}).get("page_start") or 1) requested_numbers = [number for number in (article_page, article_page + 1) if 1 <= number <= len(page_paths)] item["pages"] = [ f"{source_files.SOURCE_MIRROR_BASE_URL}/{quote(page_paths[number - 1], safe='/')}" for number in requested_numbers if isinstance(page_paths[number - 1], str) and page_paths[number - 1].startswith("archives") and ".." not in Path(page_paths[number - 1]).parts ] item["page_numbers"] = requested_numbers[:len(item["pages"])] if item["pages"]: item["initial_page"] = item["pages"][0] item["initial_page_number"] = item["page_numbers"][0] source_items.append(item) return { "doc_id": doc_id, "content_hash": content_hash(source), "title": title, "publication_name": source.get("publication_name"), "publication_type": source.get("publication_type"), "source_file_count": int(source.get("source_file_count") or len(source.get("source_files") or [])), "date_display": source.get("date_display") or [], "tag_names": source.get("tag_names") or [], "authors": source.get("authors") or [], "content": source.get("content") or "", "article_id": source.get("article_id"), "publication_id": source.get("publication_id"), "archive_id": source.get("archive_id"), "article": source.get("article") or {}, "source_files": source_items, "variants": variants, }