Spaces:
Sleeping
Sleeping
File size: 7,612 Bytes
b1c8f17 ff80fbe 4d0ec3e ff80fbe 4d0ec3e ff80fbe 4d0ec3e 26e0ddc ff80fbe e7c157d ff80fbe 1ed50ec ff80fbe ecd288b ff80fbe 01f7187 ff80fbe 01f7187 ff80fbe 01f7187 ff80fbe 01f7187 ff80fbe 0f044f0 54210e7 ff80fbe |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
import os
import shutil
import zipfile
import logging
import tempfile
import magic
from pathlib import Path
from typing import Set, Optional
from fastapi import FastAPI, File, UploadFile, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Initialize FastAPI app
app = FastAPI(title="Static Site Server")
# Add security middlewares
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure as needed
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["*"] # Configure as needed
)
# Constants
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
ALLOWED_EXTENSIONS = {'.html', '.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'}
class SiteManager:
def __init__(self):
self.sites_dir = Path("/app/sites")
self.temp_dir = Path("/app/temp")
self.active_sites: Set[str] = set()
# Ensure directories exist
self.sites_dir.mkdir(parents=True, exist_ok=True)
self.temp_dir.mkdir(parents=True, exist_ok=True)
# Load existing sites
self._load_existing_sites()
def _load_existing_sites(self):
"""Load existing sites from disk"""
logger.info("Loading existing sites...")
for site_dir in self.sites_dir.iterdir():
if site_dir.is_dir() and (site_dir / 'index.html').exists():
self.active_sites.add(site_dir.name)
logger.info(f"Loaded site: {site_dir.name}")
def _validate_file_types(self, zip_path: Path) -> bool:
"""Validate file types in ZIP archive"""
mime = magic.Magic(mime=True)
with zipfile.ZipFile(zip_path) as zip_ref:
for file_info in zip_ref.filelist:
if file_info.filename.endswith('/'): # Skip directories
continue
suffix = Path(file_info.filename).suffix.lower()
if suffix not in ALLOWED_EXTENSIONS:
return False
# Extract file to check MIME type
with tempfile.NamedTemporaryFile() as tmp:
with zip_ref.open(file_info) as source:
shutil.copyfileobj(source, tmp)
tmp.flush()
mime_type = mime.from_file(tmp.name)
if mime_type.startswith('application/x-'):
return False
return True
async def deploy_site(self, unique_id: str, zip_file: UploadFile) -> dict:
"""Deploy a new site from a ZIP file"""
if await zip_file.read(1) == b'':
raise HTTPException(status_code=400, detail="Empty file")
await zip_file.seek(0)
# Create temporary file
temp_file = self.temp_dir / f"{unique_id}.zip"
try:
# Save uploaded file
content = await zip_file.read()
if len(content) > MAX_UPLOAD_SIZE:
raise HTTPException(status_code=400, detail="File too large")
temp_file.write_bytes(content)
# Validate ZIP file
if not zipfile.is_zipfile(temp_file):
raise HTTPException(status_code=400, detail="Invalid ZIP file")
# Validate file types
if not self._validate_file_types(temp_file):
raise HTTPException(status_code=400, detail="Invalid file types in ZIP")
# Process the ZIP file
site_path = self.sites_dir / unique_id
with zipfile.ZipFile(temp_file) as zip_ref:
# Verify index.html exists
if not any(name.endswith('/index.html') or name == 'index.html'
for name in zip_ref.namelist()):
raise HTTPException(
status_code=400,
detail="ZIP file must contain index.html in root directory"
)
# Clear existing site if present
if site_path.exists():
shutil.rmtree(site_path)
# Extract files
zip_ref.extractall(self.temp_dir / unique_id)
# Move to final location
extraction_path = self.temp_dir / unique_id
root_dir = next(
(p for p in extraction_path.iterdir() if p.is_dir()
and (p / 'index.html').exists()),
extraction_path
)
shutil.move(str(root_dir), str(site_path))
self.active_sites.add(unique_id)
return {
"status": "success",
"message": f"Site deployed at /{unique_id}",
"url": f"/{unique_id}"
}
except Exception as e:
logger.error(f"Error deploying site {unique_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
finally:
# Cleanup
if temp_file.exists():
temp_file.unlink()
cleanup_path = self.temp_dir / unique_id
if cleanup_path.exists():
shutil.rmtree(cleanup_path)
def remove_site(self, unique_id: str) -> bool:
"""Remove a deployed site"""
if unique_id in self.active_sites:
site_path = self.sites_dir / unique_id
if site_path.exists():
shutil.rmtree(site_path)
self.active_sites.remove(unique_id)
return True
return False
# Initialize site manager
site_manager = SiteManager()
@app.post("/deploy/{unique_id}")
async def deploy_site(unique_id: str, file: UploadFile = File(...)):
"""Deploy a new site from a ZIP file"""
if not file.filename.endswith('.zip'):
raise HTTPException(status_code=400, detail="File must be a ZIP archive")
result = await site_manager.deploy_site(unique_id, file)
return JSONResponse(content=result)
@app.delete("/site/{unique_id}")
async def remove_site(unique_id: str):
"""Remove a deployed site"""
if site_manager.remove_site(unique_id):
return {"status": "success", "message": f"Site {unique_id} removed"}
raise HTTPException(status_code=404, detail="Site not found")
@app.get("/sites")
async def list_sites():
"""List all deployed sites"""
return {"sites": list(site_manager.active_sites)}
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "sites_count": len(site_manager.active_sites)}
# Mount static file handlers for each site
@app.on_event("startup")
async def startup_event():
"""Configure static file handlers for existing sites"""
logger.info("Starting up server...")
for site_id in site_manager.active_sites:
site_path = site_manager.sites_dir / site_id
app.mount(f"/{site_id}", StaticFiles(directory=str(site_path), html=True), name=site_id)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) |