|
from fastapi import APIRouter, HTTPException, UploadFile, Form |
|
from fastapi.responses import JSONResponse |
|
from docker_client import docker_client |
|
import uuid |
|
import zipfile |
|
import os |
|
import time |
|
from pyngrok import ngrok |
|
|
|
router = APIRouter() |
|
|
|
@router.post("/project") |
|
async def deploy_code(file: UploadFile, app_name: str = Form(...)): |
|
if not docker_client: |
|
raise HTTPException(status_code=503, detail="Docker is not available") |
|
if not file.filename.endswith(".zip"): |
|
raise HTTPException(status_code=400, detail="Only .zip files are supported") |
|
|
|
|
|
project_id = str(uuid.uuid4()) |
|
base_dir = os.path.dirname(os.path.abspath(__file__)) |
|
projects_dir = os.path.abspath(os.path.join(base_dir, "..", "projects")) |
|
os.makedirs(projects_dir, exist_ok=True) |
|
|
|
project_path = os.path.join(projects_dir, project_id) |
|
os.makedirs(project_path, exist_ok=True) |
|
|
|
|
|
zip_path = os.path.join(project_path, file.filename) |
|
with open(zip_path, "wb") as f: |
|
f.write(await file.read()) |
|
|
|
|
|
try: |
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref: |
|
zip_ref.extractall(project_path) |
|
except Exception as e: |
|
raise HTTPException(status_code=400, detail=f"Invalid zip file: {str(e)}") |
|
|
|
|
|
print(f"[DEBUG] Extracted files in: {project_path}") |
|
for dirpath, _, files in os.walk(project_path): |
|
for fname in files: |
|
print(" -", os.path.join(dirpath, fname)) |
|
|
|
|
|
def find_file(filename, root): |
|
filename = filename.lower() |
|
for dirpath, _, files in os.walk(root): |
|
for file in files: |
|
if file.lower() == filename: |
|
return os.path.join(dirpath, file) |
|
return None |
|
|
|
|
|
main_py = find_file("main.py", project_path) |
|
requirements_txt = find_file("requirements.txt", project_path) |
|
dockerfile = find_file("Dockerfile", project_path) |
|
|
|
missing_files = [] |
|
if not main_py: |
|
missing_files.append("main.py") |
|
if not requirements_txt: |
|
missing_files.append("requirements.txt") |
|
if not dockerfile: |
|
missing_files.append("Dockerfile") |
|
|
|
if missing_files: |
|
raise HTTPException( |
|
status_code=400, |
|
detail=f"Zip is missing required file(s): {', '.join(missing_files)} (case-insensitive)" |
|
) |
|
|
|
|
|
if os.path.dirname(dockerfile) != project_path: |
|
print(f"[DEBUG] Moving Dockerfile from {dockerfile} to project root") |
|
target_path = os.path.join(project_path, "Dockerfile") |
|
os.replace(dockerfile, target_path) |
|
dockerfile = target_path |
|
|
|
|
|
image_name = f"{app_name.lower()}_{project_id[:8]}" |
|
container_name = image_name |
|
|
|
try: |
|
for c in docker_client.containers.list(all=True): |
|
if c.status in ["created", "exited"]: |
|
print(f"Removing leftover container {c.name} ({c.id})") |
|
c.remove(force=True) |
|
|
|
image, _ = docker_client.images.build(path=project_path, tag=image_name) |
|
|
|
container = docker_client.containers.run( |
|
image=image_name, |
|
ports={"8080/tcp": None}, |
|
name=container_name, |
|
detach=True, |
|
mem_limit="512m", |
|
nano_cpus=1_000_000_000, |
|
read_only=True, |
|
tmpfs={"/tmp": ""}, |
|
user="1001:1001" |
|
) |
|
|
|
time.sleep(2) |
|
|
|
port_info = docker_client.api.port(container.id, 8080) |
|
if not port_info: |
|
raise Exception("Port 8080 not exposed by container") |
|
port = port_info[0]['HostPort'] |
|
|
|
tunnel = ngrok.connect(port, bind_tls=True) |
|
public_url = tunnel.public_url |
|
|
|
return JSONResponse({ |
|
"project_id": project_id, |
|
"container_name": container_name, |
|
"preview_url": public_url |
|
}) |
|
|
|
except Exception as e: |
|
return JSONResponse({"error": str(e)}, status_code=500) |
|
|
|
@router.get("/project/{project_id}") |
|
def get_project_status(project_id: str): |
|
|
|
return JSONResponse({"project_id": project_id, "status": "running"}) |
|
|