fasthost / routers /deploy.py
chipling's picture
Upload 25 files
f8d6c3f verified
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")
# Create unique project directory
project_id = str(uuid.uuid4())
base_dir = os.path.dirname(os.path.abspath(__file__)) # /router/
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)
# Save uploaded zip
zip_path = os.path.join(project_path, file.filename)
with open(zip_path, "wb") as f:
f.write(await file.read())
# Extract zip contents
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)}")
# Debug: print all extracted files
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))
# Case-insensitive recursive search for file
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) # lowercase f
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)"
)
# Move Dockerfile to root if needed
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
# Build and run Docker container
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):
# Placeholder for project status logic
return JSONResponse({"project_id": project_id, "status": "running"})