Spaces:
Running
Running
File size: 5,930 Bytes
86adb5f 9d5755d f721a91 86adb5f f721a91 9d5755d f721a91 86adb5f 9d5755d f721a91 c48ce72 f721a91 c48ce72 f721a91 9d5755d 86adb5f 9d5755d f721a91 9d5755d f721a91 9d5755d 86adb5f 9d5755d | 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 | import os
import pathlib
import uuid
from urllib.parse import urlparse
import httpx
from fastapi import FastAPI, File, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
DATA_DIR = pathlib.Path(os.environ.get("DATA_DIR", "/tmp/uploads"))
DATA_DIR.mkdir(parents=True, exist_ok=True)
MAX_BYTES = int(os.environ.get("MAX_BYTES", str(30 * 1024 * 1024)))
# /mirror streams somebody else's object storage back with CORS headers. It stores nothing.
MIRROR_MAX_BYTES = int(os.environ.get("MIRROR_MAX_BYTES", str(300 * 1024 * 1024)))
DEFAULT_MIRROR_HOSTS = (
# object storage the generated videos actually live on
"aliyuncs.com,r2.cloudflarestorage.com,amazonaws.com,myqcloud.com,bcebos.com,"
"volces.com,byteimg.com,volccdn.com,googleapis.com,cloudfront.net,oaiusercontent.com,"
# relay gateways whose own /v1/videos/.../content links may need mirroring
"diwdiw.cn,hjmie.cc.cd"
)
MIRROR_ALLOW_HOSTS = tuple(host.strip().lower() for host in os.environ.get("MIRROR_ALLOW_HOSTS", DEFAULT_MIRROR_HOSTS).split(",") if host.strip())
# Optional shared upload token. If UPLOAD_TOKEN is unset, uploads are open (temp use).
UPLOAD_TOKEN = os.environ.get("UPLOAD_TOKEN", "").strip()
# Optional public base override, e.g. https://looknicemm1-tmp-files.hf.space
PUBLIC_BASE = os.environ.get("PUBLIC_BASE", "").strip().rstrip("/")
EXT_BY_TYPE = {
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/png": "png",
"image/webp": "webp",
"image/gif": "gif",
"video/mp4": "mp4",
"video/quicktime": "mov",
"video/webm": "webm",
"audio/mpeg": "mp3",
"audio/mp4": "m4a",
"audio/wav": "wav",
"audio/x-wav": "wav",
"audio/aac": "aac",
"audio/ogg": "ogg",
}
app = FastAPI(title="tmp-file-service")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def public_base(request: Request) -> str:
if PUBLIC_BASE:
return PUBLIC_BASE
host = request.headers.get("x-forwarded-host") or request.headers.get("host") or "localhost"
proto = request.headers.get("x-forwarded-proto", "https")
return f"{proto}://{host}"
def check_auth(request: Request) -> None:
if not UPLOAD_TOKEN:
return
header = request.headers.get("authorization", "")
token = header[7:].strip() if header.lower().startswith("bearer ") else request.query_params.get("token", "")
if token != UPLOAD_TOKEN:
raise HTTPException(401, "invalid upload token")
def pick_ext(file: UploadFile) -> str:
ext = EXT_BY_TYPE.get((file.content_type or "").lower())
if ext:
return ext
suffix = pathlib.Path(file.filename or "").suffix.lstrip(".").lower()
return suffix or "bin"
def check_mirror_url(raw: str) -> str:
url = (raw or "").strip()
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise HTTPException(400, "url must be http(s)")
host = (parsed.hostname or "").lower()
if not host:
raise HTTPException(400, "url has no host")
if not any(host == allowed or host.endswith(f".{allowed}") for allowed in MIRROR_ALLOW_HOSTS):
raise HTTPException(403, f"host not allowed: {host}")
return url
@app.get("/")
def health():
return {"ok": True, "service": "tmp-file-service", "auth": bool(UPLOAD_TOKEN), "mirror_hosts": list(MIRROR_ALLOW_HOSTS)}
@app.get("/mirror")
async def mirror(request: Request, url: str = ""):
"""Fetch a signed object-storage URL server-side and stream it back, so browsers can read
bytes that the origin serves without CORS headers. Nothing is written to disk."""
target = check_mirror_url(url)
client = httpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(connect=15.0, read=120.0, write=60.0, pool=15.0))
try:
upstream = await client.send(client.build_request("GET", target), stream=True)
except httpx.HTTPError as error:
await client.aclose()
raise HTTPException(502, f"fetch failed: {error}") from error
if upstream.status_code >= 400:
await upstream.aclose()
await client.aclose()
raise HTTPException(502, f"upstream returned {upstream.status_code}")
declared = upstream.headers.get("content-length")
if declared and declared.isdigit() and int(declared) > MIRROR_MAX_BYTES:
await upstream.aclose()
await client.aclose()
raise HTTPException(413, f"object too large (> {MIRROR_MAX_BYTES} bytes)")
async def body():
sent = 0
try:
async for chunk in upstream.aiter_bytes(chunk_size=256 * 1024):
sent += len(chunk)
if sent > MIRROR_MAX_BYTES:
break
yield chunk
finally:
await upstream.aclose()
await client.aclose()
headers = {"Cache-Control": "no-store"}
if declared:
headers["Content-Length"] = declared
return StreamingResponse(body(), media_type=upstream.headers.get("content-type", "application/octet-stream"), headers=headers)
@app.post("/upload")
async def upload(request: Request, file: UploadFile = File(...)):
check_auth(request)
data = await file.read()
if not data:
raise HTTPException(400, "empty file")
if len(data) > MAX_BYTES:
raise HTTPException(413, f"file too large (> {MAX_BYTES} bytes)")
name = f"{uuid.uuid4().hex}.{pick_ext(file)}"
(DATA_DIR / name).write_bytes(data)
url = f"{public_base(request)}/files/{name}"
return {"success": True, "url": url, "filename": name, "size": len(data)}
@app.get("/files/{name}")
def get_file(name: str):
if "/" in name or ".." in name:
raise HTTPException(400, "bad name")
path = DATA_DIR / name
if not path.is_file():
raise HTTPException(404, "not found")
return FileResponse(path)
|