اضافه شدن قابلیت مقایسه شماتیک

This commit is contained in:
2026-11-07 15:42:53 +03:30
والد f5da7ee41b
کامیت d2b8ae96c9
28فایلهای تغییر یافته به همراه1012 افزوده شده و 33 حذف شده
+13 -17
مشاهده پرونده
@@ -17,11 +17,11 @@ from sqlalchemy.exc import SQLAlchemyError
from starlette.concurrency import run_in_threadpool
from .config import settings
from .db import Job, initialize, sessions
from .parser.record_decoder import ParseError, detect_format
from .storage import storage
from .fonts import fonts, stylesheet
from .models.api import UploadResponse, RenderStatus, ErrorEnvelope
from .models.schematic import Document
from .uploads import read_schematic
FRONTEND = Path(__file__).resolve().parents[2] / 'frontend'
ID = re.compile(r'^[a-f0-9]{48}$')
@@ -54,7 +54,8 @@ class UploadLimit:
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)
limit = settings().max_upload_bytes + 1024 * 1024
file_count = 2 if scope['path'] in ('/api/compare','/api/v1/comparisons') else 1
limit = file_count * settings().max_upload_bytes + 1024 * 1024
received = 0
async def bounded_receive():
nonlocal received
@@ -70,7 +71,7 @@ app.add_middleware(UploadLimit)
@app.middleware('http')
async def security(request, call_next):
# Reject unauthorized POST bodies before the multipart parser reads them.
if request.method == 'POST' and request.url.path == '/api/v1/schematics/render':
if request.method == 'POST' and request.url.path in ('/api/v1/schematics/render','/api/compare','/api/v1/comparisons'):
if settings().api_key and not authenticated(request.headers.get('x-api-key')):
return JSONResponse({'success': False, 'error': {'code': 'unauthorized', 'message': 'Provide a valid X-API-Key header'}}, 401)
response = await call_next(request)
@@ -108,10 +109,11 @@ def health():
with sessions()() as db: db.execute(text('SELECT 1'))
return {'status': 'ok'}
def authorize(identifier, key, token):
def authorize(identifier, key, token, *, comparison=False):
if not ID.fullmatch(identifier): error('not_found', 'Schematic not found', 404)
with sessions()() as db: job = db.get(Job, identifier)
if job is None: error('not_found', 'Schematic not found', 404)
if job is None or job.input_key.endswith('/comparison.json') != comparison:
error('not_found', 'Schematic not found', 404)
# Private existence and expiry are not disclosed to unauthenticated callers.
if job.access == 'private' and not (authenticated(key) or (token and hmac.compare_digest(job.token_hash, digest(token)))):
error('not_found', 'Schematic not found', 404)
@@ -135,19 +137,9 @@ async def upload(file: Annotated[UploadFile, File(description='Native binary OLE
expires_in: Annotated[int | None, Form(gt=0, description='Link lifetime in seconds, up to configured maximum')] = None,
x_api_key: Annotated[str | None, Header()] = None):
require_api(x_api_key)
name = (file.filename or '').replace('\\', '/').rsplit('/', 1)[-1]
name = ''.join(c for c in name if c.isprintable())[:255]
if not name.lower().endswith('.schdoc'): error('unsupported_file_type', 'Upload a .SchDoc file', 415)
ttl = expires_in or settings().link_ttl
if ttl > settings().link_ttl: error('invalid_expiry', 'expires_in exceeds the configured link lifetime', 422)
body = bytearray()
try:
while chunk := await file.read(1024 * 1024):
body.extend(chunk)
if len(body) > settings().max_upload_bytes: error('upload_too_large', 'Schematic exceeds configured upload size', 413)
finally: await file.close()
try: detect_format(bytes(body))
except ParseError as exc: error('invalid_schematic', str(exc), 422)
name, body = await read_schematic(file)
identifier = secrets.token_hex(24)
token = secrets.token_urlsafe(32)
now = time.time()
@@ -209,7 +201,7 @@ def index(): return FileResponse(FRONTEND / 'index.html')
@app.get('/assets/{name}', include_in_schema=False)
def asset(name: str):
if name not in {'viewer.js','viewer.css','upload.js','upload.css','camera.js'}: error('not_found', 'Asset not found', 404)
if name not in {'viewer.js','viewer.css','upload.js','upload.css','camera.js','compare.js','compare.css','compare-upload.js','interactions.js'}: error('not_found', 'Asset not found', 404)
return FileResponse(FRONTEND / name)
@app.get('/docs', include_in_schema=False)
@@ -228,3 +220,7 @@ def font_file(name: str):
async def database_error(request, exc):
logging.getLogger(__name__).error('Database request failed', exc_info=exc)
return JSONResponse({'success':False,'error':{'code':'database_unavailable','message':'Database service unavailable'}},status_code=503,headers={'Cache-Control':'no-store'})
# Comparison jobs share the existing queue, access policy and error envelope.
from .compare_api import register_comparison_routes
register_comparison_routes(app, authorize=authorize, require_api=require_api, error=error)