اضافه شدن قابلیت مقایسه شماتیک
This commit is contained in:
+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)
|
||||
|
||||
مرجع در شماره جدید
مسدود کردن کاربر