Spaces:
Sleeping
Sleeping
| """Upload AI-generated challenge files to Supabase Storage. | |
| The backend NEVER writes files to its own disk — every | |
| ``fileToGenerate`` payload is uploaded to the public | |
| ``challenge-files`` Supabase Storage bucket and the resulting URL is | |
| returned in the training payload (``logData.downloadable_url``) so | |
| the frontend can offer a direct download link. | |
| If the upload fails for any reason, the function still returns a | |
| best-effort relative path string and the rest of the challenge still | |
| loads — the file just becomes a no-op for that one challenge. | |
| """ | |
| import time | |
| import httpx | |
| from app.core.config import ( | |
| SUPABASE_URL, | |
| SUPABASE_ANON_KEY, | |
| CHALLENGE_FILES_BUCKET, | |
| ) | |
| def _public_url(storage_path: str) -> str: | |
| """Build the public URL for an object in the bucket.""" | |
| return f"{SUPABASE_URL}/storage/v1/object/public/{CHALLENGE_FILES_BUCKET}/{storage_path}" | |
| async def upload_challenge_file(file_data: dict, module: str, team_role: str) -> str: | |
| """Upload the ``fileToGenerate`` payload to Supabase Storage. | |
| Returns the **public URL** of the uploaded object, or ``""`` if | |
| nothing was saved. | |
| """ | |
| if not file_data or not file_data.get("fileName") or not file_data.get("content"): | |
| return "" | |
| file_name = str(file_data.get("fileName")).replace(" ", "_") | |
| content = file_data.get("content") | |
| # Sanitize the filename: strip path components + traversal sequences. | |
| safe_name = file_name.replace("\\", "/").split("/")[-1].replace("..", "_") | |
| if not safe_name: | |
| return "" | |
| # Object key: <team>/<module>/<timestamp>_<name> so two solves | |
| # with the same filename never overwrite each other. | |
| storage_path = f"{team_role}/{module}/{int(time.time())}_{safe_name}" | |
| upload_url = f"{SUPABASE_URL}/storage/v1/object/{CHALLENGE_FILES_BUCKET}/{storage_path}" | |
| headers = { | |
| "apikey": SUPABASE_ANON_KEY, | |
| "Authorization": f"Bearer {SUPABASE_ANON_KEY}", | |
| "Content-Type": "application/octet-stream", | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.post(upload_url, content=content.encode("utf-8"), headers=headers) | |
| if resp.status_code in (200, 201): | |
| print(f" [+] تم رفع ملف التحدي إلى Storage: {storage_path}") | |
| return _public_url(storage_path) | |
| print(f" [-] فشل رفع الملف إلى Storage: {resp.status_code} {resp.text[:200]}") | |
| except Exception as e: | |
| print(f" [-] استثناء أثناء رفع الملف إلى Storage: {e}") | |
| return "" | |