Spaces:
Paused
Paused
| # ============================================================ | |
| # HF-VPS β One-shot project creator | |
| # Run this script and the entire project is created ready to push | |
| # Usage: bash setup.sh | |
| # ============================================================ | |
| set -e | |
| PROJECT="hf-vps" | |
| echo "============================================" | |
| echo " Creating $PROJECT..." | |
| echo "============================================" | |
| # --- Create structure --- | |
| mkdir -p $PROJECT/{config,app/node,scripts,data,app/public} | |
| # ============================================================ | |
| # Dockerfile | |
| # ============================================================ | |
| cat > $PROJECT/Dockerfile << 'EOF' | |
| FROM python:3.11-slim | |
| LABEL maintainer="hf-vps" | |
| LABEL description="Universal VPS-like base environment for Hugging Face Spaces" | |
| ENV DEBIAN_FRONTEND=noninteractive \ | |
| PYTHONUNBUFFERED=1 \ | |
| PYTHONDONTWRITEBYTECODE=1 \ | |
| NODE_VERSION=20 \ | |
| APP_HOME=/app \ | |
| DATA_DIR=/app/data \ | |
| LOG_DIR=/app/logs \ | |
| PORT=7860 | |
| WORKDIR $APP_HOME | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| curl wget git ca-certificates gnupg \ | |
| build-essential gcc g++ make \ | |
| supervisor cron \ | |
| nginx \ | |
| sqlite3 \ | |
| redis-server \ | |
| htop procps net-tools vim nano \ | |
| zip unzip \ | |
| && apt-get clean \ | |
| && rm -rf /var/lib/apt/lists/* | |
| RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ | |
| && apt-get install -y nodejs \ | |
| && apt-get clean \ | |
| && rm -rf /var/lib/apt/lists/* \ | |
| && npm install -g pm2 npm@latest | |
| COPY requirements.txt . | |
| RUN pip install --no-cache-dir --upgrade pip \ | |
| && pip install --no-cache-dir -r requirements.txt | |
| RUN npm install -g axios dotenv | |
| RUN mkdir -p \ | |
| $DATA_DIR \ | |
| $LOG_DIR \ | |
| /app/app \ | |
| /app/scripts \ | |
| /var/log/supervisor \ | |
| /var/log/nginx \ | |
| /run/nginx | |
| COPY config/nginx.conf /etc/nginx/nginx.conf | |
| COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf | |
| COPY config/redis.conf /etc/redis/redis.conf | |
| COPY app/ /app/app/ | |
| COPY scripts/ /app/scripts/ | |
| COPY scripts/entrypoint.sh /entrypoint.sh | |
| RUN chmod +x /entrypoint.sh \ | |
| && chmod +x /app/scripts/*.sh 2>/dev/null || true | |
| EXPOSE 7860 | |
| ENTRYPOINT ["/entrypoint.sh"] | |
| EOF | |
| # ============================================================ | |
| # requirements.txt | |
| # ============================================================ | |
| cat > $PROJECT/requirements.txt << 'EOF' | |
| fastapi>=0.111.0 | |
| uvicorn[standard]>=0.30.0 | |
| starlette>=0.37.0 | |
| httpx>=0.27.0 | |
| requests>=2.32.0 | |
| aiohttp>=3.9.0 | |
| apscheduler>=3.10.0 | |
| sqlalchemy>=2.0.0 | |
| aiosqlite>=0.20.0 | |
| redis>=5.0.0 | |
| python-dotenv>=1.0.0 | |
| pydantic>=2.7.0 | |
| pydantic-settings>=2.2.0 | |
| python-jose[cryptography]>=3.3.0 | |
| passlib[bcrypt]>=1.7.4 | |
| python-multipart>=0.0.9 | |
| loguru>=0.7.0 | |
| tenacity>=8.3.0 | |
| python-dateutil>=2.9.0 | |
| EOF | |
| # ============================================================ | |
| # config/nginx.conf | |
| # ============================================================ | |
| cat > $PROJECT/config/nginx.conf << 'EOF' | |
| worker_processes auto; | |
| pid /tmp/nginx.pid; | |
| error_log /app/logs/nginx_error.log warn; | |
| events { | |
| worker_connections 512; | |
| multi_accept on; | |
| } | |
| http { | |
| include /etc/nginx/mime.types; | |
| default_type application/octet-stream; | |
| access_log /app/logs/nginx_access.log; | |
| sendfile on; | |
| tcp_nopush on; | |
| tcp_nodelay on; | |
| keepalive_timeout 65; | |
| gzip on; | |
| gzip_types text/plain application/json application/javascript text/css; | |
| proxy_connect_timeout 120s; | |
| proxy_send_timeout 120s; | |
| proxy_read_timeout 120s; | |
| server { | |
| listen 7860; | |
| server_name _; | |
| client_max_body_size 50M; | |
| location /health { | |
| proxy_pass http://127.0.0.1:8000/health; | |
| proxy_set_header Host $host; | |
| } | |
| location /api/ { | |
| proxy_pass http://127.0.0.1:8000/; | |
| proxy_set_header Host $host; | |
| proxy_set_header X-Real-IP $remote_addr; | |
| proxy_http_version 1.1; | |
| proxy_set_header Upgrade $http_upgrade; | |
| proxy_set_header Connection "upgrade"; | |
| } | |
| location /node/ { | |
| proxy_pass http://127.0.0.1:3000/; | |
| proxy_set_header Host $host; | |
| proxy_http_version 1.1; | |
| proxy_set_header Upgrade $http_upgrade; | |
| proxy_set_header Connection "upgrade"; | |
| } | |
| location /static/ { | |
| alias /app/public/; | |
| expires 1d; | |
| add_header Cache-Control "public"; | |
| } | |
| location / { | |
| proxy_pass http://127.0.0.1:8000; | |
| proxy_set_header Host $host; | |
| proxy_set_header X-Real-IP $remote_addr; | |
| proxy_http_version 1.1; | |
| proxy_set_header Upgrade $http_upgrade; | |
| proxy_set_header Connection "upgrade"; | |
| } | |
| } | |
| } | |
| EOF | |
| # ============================================================ | |
| # config/supervisord.conf | |
| # ============================================================ | |
| cat > $PROJECT/config/supervisord.conf << 'EOF' | |
| [supervisord] | |
| nodaemon=true | |
| logfile=/app/logs/supervisord.log | |
| logfile_maxbytes=10MB | |
| logfile_backups=3 | |
| loglevel=info | |
| pidfile=/tmp/supervisord.pid | |
| user=root | |
| [unix_http_server] | |
| file=/tmp/supervisor.sock | |
| [rpcinterface:supervisor] | |
| supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface | |
| [supervisorctl] | |
| serverurl=unix:///tmp/supervisor.sock | |
| [program:redis] | |
| command=redis-server /etc/redis/redis.conf | |
| autostart=true | |
| autorestart=true | |
| priority=10 | |
| stdout_logfile=/app/logs/redis.log | |
| stderr_logfile=/app/logs/redis_error.log | |
| [program:fastapi] | |
| command=uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 --loop asyncio | |
| directory=/app | |
| autostart=true | |
| autorestart=true | |
| priority=20 | |
| stdout_logfile=/app/logs/fastapi.log | |
| stderr_logfile=/app/logs/fastapi_error.log | |
| environment=PYTHONPATH="/app" | |
| [program:node] | |
| command=node /app/app/node/server.js | |
| directory=/app/app/node | |
| autostart=false | |
| autorestart=true | |
| priority=30 | |
| stdout_logfile=/app/logs/node.log | |
| stderr_logfile=/app/logs/node_error.log | |
| [program:scheduler] | |
| command=python /app/app/scheduler.py | |
| directory=/app | |
| autostart=true | |
| autorestart=true | |
| priority=25 | |
| stdout_logfile=/app/logs/scheduler.log | |
| stderr_logfile=/app/logs/scheduler_error.log | |
| environment=PYTHONPATH="/app" | |
| [program:nginx] | |
| command=nginx -g "daemon off;" | |
| autostart=true | |
| autorestart=true | |
| priority=40 | |
| stdout_logfile=/app/logs/nginx.log | |
| stderr_logfile=/app/logs/nginx_error.log | |
| EOF | |
| # ============================================================ | |
| # config/redis.conf | |
| # ============================================================ | |
| cat > $PROJECT/config/redis.conf << 'EOF' | |
| bind 127.0.0.1 | |
| port 6379 | |
| protected-mode yes | |
| maxmemory 256mb | |
| maxmemory-policy allkeys-lru | |
| save 900 1 | |
| save 300 10 | |
| dbfilename dump.rdb | |
| dir /app/data | |
| loglevel notice | |
| logfile /app/logs/redis.log | |
| tcp-keepalive 300 | |
| timeout 0 | |
| tcp-backlog 128 | |
| EOF | |
| # ============================================================ | |
| # app/main.py | |
| # ============================================================ | |
| cat > $PROJECT/app/main.py << 'EOF' | |
| # ============================================================ | |
| # HF-VPS β Main FastAPI Application | |
| # Uvicorn: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 | |
| # PYTHONPATH=/app β "app.main" resolves to /app/app/main.py | |
| # ============================================================ | |
| import os | |
| import time | |
| import platform | |
| import socket | |
| import json | |
| from fastapi import FastAPI | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from loguru import logger | |
| from pathlib import Path | |
| import psutil | |
| import redis as redis_lib | |
| app = FastAPI( | |
| title="HF-VPS", | |
| description="Universal VPS-like environment on Hugging Face Spaces", | |
| version="1.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| public_dir = Path("/app/public") | |
| public_dir.mkdir(exist_ok=True) | |
| app.mount("/static", StaticFiles(directory=str(public_dir)), name="static") | |
| START_TIME = time.time() | |
| # Add your routers here: | |
| # from app.routers import myrouter | |
| # app.include_router(myrouter.router, prefix="/api/v1") | |
| @app.get("/", response_class=HTMLResponse, include_in_schema=False) | |
| async def root(): | |
| index = public_dir / "index.html" | |
| if index.exists(): | |
| return HTMLResponse(content=index.read_text()) | |
| return HTMLResponse(content="<h1>HF-VPS running</h1>") | |
| @app.get("/health", tags=["system"]) | |
| async def health(): | |
| uptime = int(time.time() - START_TIME) | |
| return JSONResponse({ | |
| "status": "ok", | |
| "uptime_seconds": uptime, | |
| "uptime_human": _format_uptime(uptime), | |
| "python": platform.python_version(), | |
| "platform": platform.system(), | |
| }) | |
| @app.get("/info", tags=["system"]) | |
| async def info(): | |
| return JSONResponse({ | |
| "name": os.getenv("APP_NAME", "HF-VPS"), | |
| "version": "1.0.0", | |
| "port": 7860, | |
| "environment": os.getenv("ENV", "production"), | |
| }) | |
| @app.get("/status", tags=["system"]) | |
| async def status(): | |
| uptime = int(time.time() - START_TIME) | |
| return JSONResponse({ | |
| "app": { | |
| "name": os.getenv("APP_NAME", "HF-VPS"), | |
| "version": "1.0.0", | |
| "environment": os.getenv("ENV", "production"), | |
| }, | |
| "uptime_seconds": uptime, | |
| "uptime_human": _format_uptime(uptime), | |
| "python": platform.python_version(), | |
| "platform": platform.system(), | |
| "services": { | |
| "fastapi": {"status": "ok"}, | |
| "redis": {"status": "ok" if _check_redis() else "error"}, | |
| "nginx": {"status": "ok" if _check_port(7860) else "error"}, | |
| "scheduler": {"status": "ok" if _check_scheduler() else "error"}, | |
| }, | |
| "keep_alive": _get_scheduler_state(), | |
| "system": { | |
| "memory": _memory_stats(), | |
| "disk": _disk_stats(), | |
| }, | |
| }) | |
| def _format_uptime(seconds: int) -> str: | |
| days = seconds // 86400 | |
| hours = (seconds % 86400) // 3600 | |
| minutes = (seconds % 3600) // 60 | |
| return f"{days}d {hours}h {minutes}m" | |
| def _check_redis() -> bool: | |
| try: | |
| r = redis_lib.Redis(host="127.0.0.1", port=6379, socket_timeout=1) | |
| return r.ping() | |
| except Exception: | |
| return False | |
| def _check_port(port: int) -> bool: | |
| try: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
| s.settimeout(1) | |
| result = s.connect_ex(("127.0.0.1", port)) | |
| s.close() | |
| return result == 0 | |
| except Exception: | |
| return False | |
| def _check_scheduler() -> bool: | |
| try: | |
| r = redis_lib.Redis(host="127.0.0.1", port=6379, socket_timeout=1) | |
| return r.exists("hfvps:scheduler_state") == 1 | |
| except Exception: | |
| return False | |
| def _get_scheduler_state() -> dict: | |
| try: | |
| r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True, socket_timeout=1) | |
| raw = r.get("hfvps:scheduler_state") | |
| if raw: | |
| return json.loads(raw) | |
| except Exception: | |
| pass | |
| return { | |
| "last_ping_time": None, | |
| "last_ping_status": None, | |
| "next_ping_time": None, | |
| "ping_count": 0, | |
| "keep_alive_enabled": os.getenv("ENABLE_KEEP_ALIVE", "true").lower() == "true", | |
| "keep_alive_interval_seconds": int(os.getenv("KEEP_ALIVE_INTERVAL", "1800")), | |
| } | |
| def _memory_stats() -> dict: | |
| mem = psutil.virtual_memory() | |
| return { | |
| "used_mb": round(mem.used / 1024 / 1024), | |
| "total_mb": round(mem.total / 1024 / 1024), | |
| "percent": round(mem.percent, 1), | |
| } | |
| def _disk_stats() -> dict: | |
| disk = psutil.disk_usage("/app") | |
| return { | |
| "used_gb": round(disk.used / 1024 / 1024 / 1024, 2), | |
| "total_gb": round(disk.total / 1024 / 1024 / 1024, 2), | |
| "percent": round(disk.percent, 1), | |
| } | |
| EOF | |
| # ============================================================ | |
| # app/scheduler.py | |
| # ============================================================ | |
| cat > $PROJECT/app/scheduler.py << 'EOF' | |
| # ============================================================ | |
| # HF-VPS β Background Scheduler | |
| # State is persisted to Redis (key: hfvps:scheduler_state) | |
| # so the /status endpoint can read it for the dashboard. | |
| # ============================================================ | |
| import asyncio | |
| import json | |
| import os | |
| from datetime import datetime, timezone, timedelta | |
| import httpx | |
| import redis as redis_lib | |
| from apscheduler.schedulers.asyncio import AsyncIOScheduler | |
| from apscheduler.triggers.interval import IntervalTrigger | |
| from loguru import logger | |
| SPACE_URL: str = os.getenv("SPACE_URL", "http://127.0.0.1:8000") | |
| KEEP_ALIVE_INTERVAL: int = int(os.getenv("KEEP_ALIVE_INTERVAL", "1800")) | |
| ENABLE_KEEP_ALIVE: bool = os.getenv("ENABLE_KEEP_ALIVE", "true").lower() == "true" | |
| REDIS_STATE_KEY: str = "hfvps:scheduler_state" | |
| _ping_count: int = 0 | |
| def _load_state_from_redis() -> int: | |
| try: | |
| r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True) | |
| raw = r.get(REDIS_STATE_KEY) | |
| if raw: | |
| return int(json.loads(raw).get("ping_count", 0)) | |
| except Exception as e: | |
| logger.warning(f"[scheduler] could not load state from Redis: {e}") | |
| return 0 | |
| def _save_state(last_ping_time, last_ping_status, next_ping_time, ping_count) -> None: | |
| state = { | |
| "last_ping_time": last_ping_time, | |
| "last_ping_status": last_ping_status, | |
| "next_ping_time": next_ping_time, | |
| "ping_count": ping_count, | |
| "keep_alive_enabled": ENABLE_KEEP_ALIVE, | |
| "keep_alive_interval_seconds": KEEP_ALIVE_INTERVAL, | |
| } | |
| try: | |
| r = redis_lib.Redis(host="127.0.0.1", port=6379, decode_responses=True) | |
| r.set(REDIS_STATE_KEY, json.dumps(state)) | |
| except Exception as e: | |
| logger.error(f"[scheduler] failed to save state to Redis: {e}") | |
| async def keep_alive() -> None: | |
| global _ping_count | |
| now = datetime.now(timezone.utc) | |
| next_ping = (now + timedelta(seconds=KEEP_ALIVE_INTERVAL)).isoformat() | |
| try: | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| r = await client.get(f"{SPACE_URL}/health") | |
| if r.status_code == 200: | |
| _ping_count += 1 | |
| logger.info(f"[keep-alive] OK β uptime: {r.json().get('uptime_human','?')} β count: {_ping_count}") | |
| _save_state(now.isoformat(), "ok", next_ping, _ping_count) | |
| else: | |
| logger.warning(f"[keep-alive] unexpected status: {r.status_code}") | |
| _save_state(now.isoformat(), "failed", next_ping, _ping_count) | |
| except Exception as e: | |
| logger.error(f"[keep-alive] failed: {e}") | |
| _save_state(now.isoformat(), "failed", next_ping, _ping_count) | |
| async def rotate_tokens() -> None: | |
| """Hook for token rotation β add your refresh logic here.""" | |
| logger.info("[tokens] rotation tick β add your refresh logic here") | |
| async def main() -> None: | |
| global _ping_count | |
| _ping_count = _load_state_from_redis() | |
| _save_state(None, None, None, _ping_count) | |
| scheduler = AsyncIOScheduler() | |
| if ENABLE_KEEP_ALIVE: | |
| scheduler.add_job(keep_alive, IntervalTrigger(seconds=KEEP_ALIVE_INTERVAL), id="keep_alive", next_run_time=None) | |
| logger.info(f"[scheduler] keep-alive registered β interval: {KEEP_ALIVE_INTERVAL}s") | |
| else: | |
| logger.info("[scheduler] keep-alive disabled") | |
| scheduler.add_job(rotate_tokens, IntervalTrigger(minutes=40), id="token_rotation") | |
| scheduler.start() | |
| logger.info("[scheduler] all jobs started") | |
| try: | |
| while True: | |
| await asyncio.sleep(60) | |
| except (KeyboardInterrupt, SystemExit): | |
| scheduler.shutdown() | |
| logger.info("[scheduler] stopped cleanly") | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |
| EOF | |
| # ============================================================ | |
| # app/node/server.js | |
| # ============================================================ | |
| cat > $PROJECT/app/node/server.js << 'EOF' | |
| const http = require("http"); | |
| const PORT = process.env.NODE_PORT || 3000; | |
| const server = http.createServer((req, res) => { | |
| if (req.url === "/health") { | |
| res.writeHead(200, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify({ status: "ok", runtime: "node", uptime: process.uptime() })); | |
| return; | |
| } | |
| res.writeHead(200, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify({ message: "Node.js layer ready β customize server.js" })); | |
| }); | |
| server.listen(PORT, "0.0.0.0", () => console.log(`[node] running on port ${PORT}`)); | |
| process.on("SIGTERM", () => server.close(() => process.exit(0))); | |
| EOF | |
| # ============================================================ | |
| # scripts/entrypoint.sh | |
| # ============================================================ | |
| cat > $PROJECT/scripts/entrypoint.sh << 'EOF' | |
| #!/bin/bash | |
| set -e | |
| echo "============================================" | |
| echo " HF-VPS Starting..." | |
| echo "============================================" | |
| mkdir -p /app/logs /app/data /app/public | |
| [ ! -f /app/.env ] && touch /app/.env | |
| echo "[init] Python: $(python --version)" | |
| echo "[init] Node: $(node --version)" | |
| echo "[init] Redis: $(redis-server --version | head -1)" | |
| nginx -t 2>/dev/null && echo "[init] Nginx config OK" | |
| echo "[init] Starting all services..." | |
| exec supervisord -c /etc/supervisor/conf.d/supervisord.conf | |
| EOF | |
| chmod +x $PROJECT/scripts/entrypoint.sh | |
| # ============================================================ | |
| # .env.example | |
| # ============================================================ | |
| cat > $PROJECT/.env.example << 'EOF' | |
| APP_NAME=HF-VPS | |
| ENV=production | |
| SPACE_URL=https://your-username-your-space.hf.space | |
| ENABLE_KEEP_ALIVE=true | |
| KEEP_ALIVE_INTERVAL=1800 | |
| # API_KEY= | |
| # DATABASE_URL=sqlite:///./data/app.db | |
| # TOKEN_1= | |
| # TOKEN_2= | |
| EOF | |
| # ============================================================ | |
| # .gitignore | |
| # ============================================================ | |
| cat > $PROJECT/.gitignore << 'EOF' | |
| .env | |
| *.env | |
| __pycache__/ | |
| *.pyc | |
| .pytest_cache/ | |
| node_modules/ | |
| data/*.db | |
| data/*.rdb | |
| logs/ | |
| .DS_Store | |
| EOF | |
| # ============================================================ | |
| # README.md (HF Space header included) | |
| # ============================================================ | |
| cat > $PROJECT/README.md << 'EOF' | |
| --- | |
| title: HF-VPS | |
| emoji: π | |
| colorFrom: blue | |
| colorTo: purple | |
| sdk: docker | |
| pinned: false | |
| --- | |
| # π HF-VPS β Universal Base Environment | |
| A solid, flexible Docker environment for Hugging Face Spaces. | |
| Built to support the widest variety of projects on free hardware. | |
| **2 vCPU Β· 16GB RAM Β· 50GB Disk Β· Free** | |
| ## Stack | |
| - **Nginx** β reverse proxy, port 7860 | |
| - **FastAPI** β Python backend | |
| - **Node.js 20** β optional JS layer | |
| - **Redis** β cache & queue | |
| - **SQLite** β lightweight DB | |
| - **APScheduler** β cron + keep-alive | |
| - **Supervisord** β keeps everything running | |
| ## Quick deploy | |
| ```bash | |
| git clone https://github.com/your-username/hf-vps | |
| cd hf-vps | |
| git remote add hf https://huggingface.co/spaces/YOUR_HF_USERNAME/YOUR_SPACE_NAME | |
| git push hf main | |
| ``` | |
| ## Endpoints | |
| | URL | Description | | |
| |---|---| | |
| | `/` | Your app | | |
| | `/health` | Health + uptime | | |
| | `/docs` | FastAPI docs | | |
| | `/static/` | Static files | | |
| | `/api/` | API routes | | |
| | `/node/` | Node.js (if enabled) | | |
| ## Set in HF Secrets | |
| ``` | |
| SPACE_URL = https://your-username-your-space.hf.space | |
| APP_NAME = your-app-name | |
| ``` | |
| MIT License | |
| EOF | |
| # ============================================================ | |
| # Done | |
| # ============================================================ | |
| echo "" | |
| echo "============================================" | |
| echo " β $PROJECT created successfully!" | |
| echo "============================================" | |
| echo "" | |
| echo "Next steps:" | |
| echo "" | |
| echo " 1. cd $PROJECT" | |
| echo " 2. git init" | |
| echo " 3. git add ." | |
| echo " 4. git commit -m 'initial hf-vps base'" | |
| echo " 5. git remote add origin https://github.com/YOUR_USERNAME/hf-vps" | |
| echo " 6. git push origin main" | |
| echo " 7. git remote add hf https://huggingface.co/spaces/HF_USERNAME/SPACE_NAME" | |
| echo " 8. git push hf main" | |
| echo "" | |
| echo " Then set SPACE_URL in your HF Space Secrets and you're live!" | |
| echo "" | |