| |
| FROM ubuntu:22.04 |
|
|
| ENV DEBIAN_FRONTEND=noninteractive |
| ENV HOSTNAME=Ubuntu |
|
|
| |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| ca-certificates \ |
| curl \ |
| wget \ |
| git \ |
| sudo \ |
| docker.io \ |
| htop \ |
| btop \ |
| neovim \ |
| lsof \ |
| qemu-system \ |
| cloud-image-utils \ |
| python3 \ |
| python3-pip \ |
| nginx \ |
| supervisor \ |
| && rm -rf /var/lib/apt/lists/* |
|
|
| |
| RUN curl -fsSL https://code-server.dev/install.sh | sh |
|
|
| |
| RUN pip3 install fastapi uvicorn pydantic huggingface_hub |
|
|
| |
| RUN mkdir -p /opt/api /workspace /var/log/supervisor |
|
|
| |
| COPY <<"EOF" /opt/api/api.py |
| from fastapi import FastAPI, HTTPException, BackgroundTasks |
| from pydantic import BaseModel |
| import os, subprocess, shutil, logging |
| from huggingface_hub import HfApi, snapshot_download |
|
|
| |
| logging.basicConfig(level=logging.INFO, format="%(message)s") |
| logger = logging.getLogger("workspace_api") |
|
|
| app = FastAPI() |
| BASE_DIR = "/workspace" |
|
|
| |
| class WriteRequest(BaseModel): |
| session_id: str = "default" |
| path: str |
| content: str |
|
|
| class ExecRequest(BaseModel): |
| session_id: str = "default" |
| command: str |
|
|
| class DeleteRequest(BaseModel): |
| session_id: str = "default" |
| path: str |
|
|
| |
| def get_session_dir(session_id: str): |
| session_dir = os.path.abspath(os.path.join(BASE_DIR, session_id)) |
| if not session_dir.startswith(BASE_DIR): |
| raise HTTPException(status_code=403, detail="Invalid session ID") |
| os.makedirs(session_dir, exist_ok=True) |
| return session_dir |
|
|
| def resolve_path(session_id: str, relative_path: str): |
| session_dir = get_session_dir(session_id) |
| if relative_path in [".", "/", ""]: |
| return session_dir |
| clean_path = relative_path.lstrip("/") |
| full_path = os.path.abspath(os.path.join(session_dir, clean_path)) |
| if not full_path.startswith(session_dir): |
| raise HTTPException(status_code=403, detail="Path traversal detected") |
| return full_path |
|
|
| |
| @app.on_event("startup") |
| def startup_sync(): |
| token = os.getenv("HF_TOKEN") |
| dataset = os.getenv("HF_DATASET") |
| if token and dataset: |
| logger.info(f"Pulling dataset {dataset} on startup...") |
| try: |
| snapshot_download(repo_id=dataset, repo_type="dataset", local_dir=BASE_DIR, token=token) |
| logger.info("Successfully restored sessions from Hugging Face Dataset!") |
| except Exception as e: |
| logger.warning(f"Startup sync failed (If dataset is empty, this is normal): {e}") |
|
|
| |
| @app.get('/api/list') |
| def list_dir(session_id: str = "default", path: str = ""): |
| target = resolve_path(session_id, path) |
| try: |
| return {'files': os.listdir(target)} |
| except Exception as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| @app.get('/api/read') |
| def read_file(session_id: str = "default", path: str = ""): |
| target = resolve_path(session_id, path) |
| try: |
| with open(target, 'r', encoding='utf-8') as f: |
| return {'content': f.read()} |
| except Exception as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| @app.post('/api/write') |
| def write_file(req: WriteRequest): |
| target = resolve_path(req.session_id, req.path) |
| try: |
| os.makedirs(os.path.dirname(target), exist_ok=True) |
| with open(target, 'w', encoding='utf-8') as f: |
| f.write(req.content) |
| return {'status': 'success', 'saved_to': target} |
| except Exception as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| @app.post('/api/exec') |
| def exec_cmd(req: ExecRequest): |
| session_dir = get_session_dir(req.session_id) |
| try: |
| |
| logger.info(f"[Session {req.session_id} terminal input]: {req.command}") |
| |
| result = subprocess.run(req.command, shell=True, cwd=session_dir, capture_output=True, text=True) |
| |
| |
| if result.stdout: |
| logger.info(f"[Session {req.session_id} output]:\n{result.stdout.strip()}") |
| if result.stderr: |
| logger.error(f"[Session {req.session_id} error]:\n{result.stderr.strip()}") |
| |
| return {'stdout': result.stdout, 'stderr': result.stderr, 'returncode': result.returncode} |
| except Exception as e: |
| logger.error(f"[Session {req.session_id} critical error]: {str(e)}") |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| @app.post('/api/delete') |
| def delete_item(req: DeleteRequest): |
| target = resolve_path(req.session_id, req.path) |
| try: |
| if os.path.isdir(target): |
| shutil.rmtree(target) |
| else: |
| os.remove(target) |
| return {'status': 'success'} |
| except Exception as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| |
| @app.post('/api/sync/push') |
| def push_to_dataset(background_tasks: BackgroundTasks): |
| token = os.getenv("HF_TOKEN") |
| dataset = os.getenv("HF_DATASET") |
| if not token or not dataset: |
| raise HTTPException(status_code=400, detail="HF_TOKEN or HF_DATASET env missing") |
| |
| def push_job(): |
| try: |
| api = HfApi(token=token) |
| api.upload_folder(folder_path=BASE_DIR, repo_id=dataset, repo_type="dataset", commit_message="API Auto-sync") |
| logger.info("Successfully backed up to dataset.") |
| except Exception as e: |
| logger.error(f"Push failed: {e}") |
|
|
| background_tasks.add_task(push_job) |
| return {"status": "sync started in background"} |
| EOF |
|
|
| |
| |
| COPY <<"EOF" /etc/nginx/nginx.conf |
| events {} |
| http { |
| server { |
| listen 7860; |
|
|
| |
| location /api/ { |
| proxy_pass http://127.0.0.1:8000; |
| proxy_set_header Host $host; |
| proxy_set_header X-Real-IP $remote_addr; |
| } |
|
|
| |
| location ~ ^/proxy/(?<app_port>[0-9]+)(?<app_uri>.*)$ { |
| proxy_pass http://127.0.0.1:$app_port$app_uri; |
| proxy_http_version 1.1; |
| proxy_set_header Upgrade $http_upgrade; |
| proxy_set_header Connection "upgrade"; |
| proxy_set_header Host $host; |
| proxy_set_header X-Real-IP $remote_addr; |
| } |
|
|
| |
| location / { |
| proxy_pass http://127.0.0.1:8080; |
| proxy_http_version 1.1; |
| proxy_set_header Host $host; |
| proxy_set_header Upgrade $http_upgrade; |
| proxy_set_header Connection "upgrade"; |
| proxy_set_header Accept-Encoding gzip; |
| } |
| } |
| } |
| EOF |
|
|
| |
| |
| COPY <<"EOF" /etc/supervisor/conf.d/supervisord.conf |
| [supervisord] |
| nodaemon=true |
| user=root |
| logfile=/dev/null |
| logfile_maxbytes=0 |
|
|
| [program:codeserver] |
| command=code-server --bind-addr 127.0.0.1:8080 --auth none |
| directory=/workspace |
| autorestart=true |
| stdout_logfile=/dev/stdout |
| stdout_logfile_maxbytes=0 |
| stderr_logfile=/dev/stderr |
| stderr_logfile_maxbytes=0 |
|
|
| [program:api] |
| command=uvicorn api:app --host 127.0.0.1 --port 8000 |
| directory=/opt/api |
| autorestart=true |
| stdout_logfile=/dev/stdout |
| stdout_logfile_maxbytes=0 |
| stderr_logfile=/dev/stderr |
| stderr_logfile_maxbytes=0 |
|
|
| [program:nginx] |
| command=nginx -g "daemon off;" |
| autorestart=true |
| stdout_logfile=/dev/stdout |
| stdout_logfile_maxbytes=0 |
| stderr_logfile=/dev/stderr |
| stderr_logfile_maxbytes=0 |
| EOF |
|
|
| |
| WORKDIR /workspace |
| EXPOSE 7860 |
| CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] |
|
|