'''ServiceLink mesh endpoint for the picoshare/NanoShare node. Lets other nodes (browser-cli, website) push files in and read file metadata over the shared servicelink envelope at POST /rpc, alongside the existing web UI and /api routes. Every call needs a bearer token carrying the `files` or `mesh` scope; the endpoint is rate limited and body-size capped. Keep /rpc on the internal node network. ''' from __future__ import annotations import base64 import os from pathlib import PurePosixPath from quart import current_app from my_modules.app.setup import LIMITER from my_modules.expiry import ensure_utc, parse_expires from my_modules.file_meta import format_size, iso_stamp_filename from servicelink import Forbidden, InvalidParams, NotFound, Router, Unauthorized, any_verifier, bearer_verifier, create_link_blueprint, shared_secret_verifier MAX_RPC_BODY = 16 * 1024 * 1024 RPC_SCOPES = ('files', 'mesh') router = Router('picoshare') def _safe_file_path(value: str) -> str: raw = (value or '').replace('\\', '/') parts = [ part for part in PurePosixPath(raw).parts if part not in ('', '.', '..', '/') ] return '/'.join(parts) def _user_id(ctx): if ctx.principal is None: raise Unauthorized('authentication required') if not any(ctx.principal.has_scope(scope) for scope in RPC_SCOPES): raise Forbidden('missing required scope: files') return ctx.principal.subject @router.method('files.upload') async def files_upload(params, ctx): user_id = _user_id(ctx) text = params.get('text') content_b64 = params.get('content_b64') if content_b64: data = base64.b64decode(content_b64) content_type = params.get('content_type') or 'application/octet-stream' default_ext = 'bin' elif text is not None: data = text.encode('utf-8') content_type = 'text/plain' default_ext = 'txt' else: raise InvalidParams('provide text or content_b64') file_name = PurePosixPath(str(params.get('file_name') or iso_stamp_filename('mesh', default_ext)).replace('\\', '/')).name file_path = _safe_file_path(str(params.get('file_path') or '')) storage_id = await current_app.convex.send_to_storage(data=data, content_type=content_type) file_record = await current_app.convex.add_file( file_name=file_name, file_path=file_path, file_size=format_size(len(data)), note=params.get('note', ''), content_type=content_type, expires_at=ensure_utc(parse_expires(params.get('expires', ''))), storage_id=storage_id, user_id=user_id, ) file_id = file_record.get('file_id') if isinstance(file_record, dict) else None return {'file_id': file_id, 'file_name': file_name, 'file_path': file_path, 'size': len(data), 'content_type': content_type} @router.method('files.list') async def files_list(params, ctx): return {'files': await current_app.convex.get_files(user_id=_user_id(ctx))} @router.method('files.get') async def files_get(params, ctx): file_id = params.get('file_id') if not file_id: raise InvalidParams('file_id is required') meta = await current_app.convex.get_file(file_id) if not meta: raise NotFound('no such file', data={'file_id': file_id}) return meta @router.method('files.info') async def files_info(params, ctx): file_id = params.get('file_id') if not file_id: raise InvalidParams('file_id is required') return await current_app.convex.get_file_informations(file_id, _user_id(ctx)) @router.method('files.update') async def files_update(params, ctx): file_id = params.get('file_id') file_name = PurePosixPath(str(params.get('file_name') or '').replace('\\', '/')).name if not file_id or not file_name: raise InvalidParams('file_id and file_name are required') await current_app.convex.update_file( file_id=file_id, file_name=file_name, file_path=_safe_file_path(str(params.get('file_path') or '')), note=params.get('note', ''), expires_at=ensure_utc(parse_expires(params.get('expires', ''))), user_id=_user_id(ctx), ) return {'updated': True} @router.method('files.delete') async def files_delete(params, ctx): file_id = params.get('file_id') if not file_id: raise InvalidParams('file_id is required') await current_app.convex.delete_file(file_id, _user_id(ctx)) return {'deleted': True} async def _decode_access_token(token): payload = await current_app.convex.decode_access_token_payload(access_token=token) if not payload or payload.get('error') or not payload.get('sub'): raise ValueError((payload or {}).get('error', 'invalid token')) return payload def _build_verify(): # Accept a JWT access token with a file/RPC scope (public path) OR, on the # trusted Docker network, a static shared secret from SERVICELINK_MESH_SECRET. jwt = bearer_verifier(_decode_access_token) secret = os.getenv('SERVICELINK_MESH_SECRET') return any_verifier(shared_secret_verifier(secret, scopes=('mesh',)), jwt) if secret else jwt verify = _build_verify() link_bp = create_link_blueprint(router, verify=verify, limiter=LIMITER.limit('30 per minute'), max_body=MAX_RPC_BODY)