From 3c9f4a10241df9ef9197ba708adb69557b22dda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Tue, 12 May 2026 01:59:50 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20AP?= =?UTF-8?q?I=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=20=D1=81=D0=BA=D0=B0=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20APK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/backend/main.py b/backend/main.py index 6449528..e6bbaca 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,4 +1,5 @@ from pathlib import Path +from urllib.parse import unquote from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -66,6 +67,34 @@ class WorkspaceRequest(BaseModel): device: str | None = None +def _safe_workspace_file(workspace: str, file_path: str) -> Path: + root = Path(workspace).resolve() + target = Path(unquote(file_path)).resolve() + if not root.exists(): + raise HTTPException(status_code=400, detail='Workspace path does not exist') + if not target.exists() or not target.is_file(): + raise HTTPException(status_code=404, detail='File not found') + if root not in target.parents and target != root: + raise HTTPException(status_code=403, detail='File is outside workspace') + if target.suffix.lower() != '.apk': + raise HTTPException(status_code=403, detail='Only APK files are allowed') + return target + + +def _apk_meta(path: str, workspace: str) -> dict: + apk = Path(path).resolve() + stat = apk.stat() + return { + 'name': apk.name, + 'path': apk.as_posix(), + 'relative_path': apk.relative_to(Path(workspace).resolve()).as_posix(), + 'size_bytes': stat.st_size, + 'mtime': stat.st_mtime, + 'open_url': f'/api/android/apk/open?workspace={workspace}&path={apk.as_posix()}', + 'download_url': f'/api/android/apk/download?workspace={workspace}&path={apk.as_posix()}', + } + + @app.get('/') async def root(): return FileResponse(FRONTEND_DIR / 'index.html') @@ -220,6 +249,34 @@ async def android_build(payload: WorkspaceRequest): } +@app.get('/api/android/apks') +async def android_apks(workspace: str): + root = Path(workspace).resolve() + if not root.exists(): + raise HTTPException(status_code=400, detail='Workspace path does not exist') + flutter_apk_dir = root / 'build' / 'app' / 'outputs' / 'flutter-apk' + apks = [] + if flutter_apk_dir.exists(): + apks = sorted(flutter_apk_dir.glob('*.apk'), key=lambda p: p.stat().st_mtime, reverse=True) + return { + 'success': True, + 'directory': flutter_apk_dir.as_posix(), + 'apks': [_apk_meta(apk.as_posix(), root.as_posix()) for apk in apks], + } + + +@app.get('/api/android/apk/open') +async def android_open_apk(workspace: str, path: str): + apk = _safe_workspace_file(workspace, path) + return FileResponse(apk, media_type='application/vnd.android.package-archive', filename=apk.name) + + +@app.get('/api/android/apk/download') +async def android_download_apk(workspace: str, path: str): + apk = _safe_workspace_file(workspace, path) + return FileResponse(apk, media_type='application/vnd.android.package-archive', filename=apk.name, headers={'Content-Disposition': f'attachment; filename="{apk.name}"'}) + + @app.post('/api/android/install-latest') async def android_install_latest(payload: WorkspaceRequest): add_log(f'Installing APK to device: {payload.device}')