uploads

git clone https://github.com/notamitgamer/uploads.git
· rebuilt hourlyOpen on GitHub
commit 938d0cc8b14246687c78e6d7f80c9d1b16afc36d
parent 125176a486b7d5aa7ae17dc1597010ea9bd7d325
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Tue, 21 Jul 2026 08:16:20 +0530

Refactor file upload handling and add metadata support
Diffstat:
Mapi/index.py | 252++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 213 insertions(+), 39 deletions(-)

diff --git a/api/index.py b/api/index.py @@ -1,63 +1,237 @@ import os +import re +import json import random import string -from datetime import datetime +from datetime import datetime, timedelta, timezone +from typing import List, Optional + +import httpx from fastapi import FastAPI, File, UploadFile, Form, HTTPException -from huggingface_hub import HfApi +from fastapi.responses import RedirectResponse, JSONResponse +from huggingface_hub import HfApi, hf_hub_download +from huggingface_hub.utils import EntryNotFoundError # Fix for Vercel's read-only file system (os error 30) os.environ["HF_HOME"] = "/tmp" os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "0" -# Create FastAPI app for Vercel app = FastAPI(docs_url=None, redoc_url=None) -# Settings -DATASET_REPO = "notamitgamer/uploads" +DATASET_REPO = "notamitgamer/uploads" HF_TOKEN = os.getenv("HF_TOKEN") +MAX_UPLOAD_BYTES = int(4.4 * 1024 * 1024) # stay under Vercel's 4.5MB body limit -@app.post("/api/upload") -async def upload_file(file: UploadFile = File(...), custom_id: str = Form(None)): +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_SAFE_ID_RE = re.compile(r"[^a-zA-Z0-9._-]+") + + +def sanitize_id(raw: str, ext: str) -> str: + """Turn a user-supplied custom_id into a safe repo path segment. + + - strips anything that isn't alnum, dot, dash, underscore + - removes leading dots/dashes (no path traversal, no dotfiles) + - enforces a sane max length + - re-appends the original extension if missing + """ + raw = raw.strip() + raw = raw.replace("/", "-").replace("\\", "-") + raw = _SAFE_ID_RE.sub("", raw) + raw = raw.lstrip(".-") + if not raw: + raise HTTPException(status_code=400, detail="custom_id is invalid after sanitization") + raw = raw[:80] + if ext and not raw.endswith(ext): + raw += ext + return raw + + +def random_id(ext: str) -> str: + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + random_str = "".join(random.choices(string.ascii_letters + string.digits, k=6)) + return f"{timestamp}-{random_str}{ext}" + + +def get_api() -> HfApi: if not HF_TOKEN: raise HTTPException(status_code=500, detail="HF_TOKEN environment variable is not set") + return HfApi() + + +def ensure_repo(api: HfApi): + api.create_repo( + repo_id=DATASET_REPO, + repo_type="dataset", + exist_ok=True, + token=HF_TOKEN, + private=False, + ) + + +def upload_bytes(api: HfApi, content: bytes, path_in_repo: str): + api.upload_file( + path_or_fileobj=content, + path_in_repo=path_in_repo, + repo_id=DATASET_REPO, + repo_type="dataset", + token=HF_TOKEN, + ) + + +def write_meta(api: HfApi, item_id: str, expires_at: Optional[str]): + if not expires_at: + return + meta = {"expires_at": expires_at} + upload_bytes(api, json.dumps(meta).encode(), f"{item_id}.meta.json") + + +def raw_url(item_id: str) -> str: + return f"https://huggingface.co/datasets/{DATASET_REPO}/resolve/main/{item_id}" + + +# --------------------------------------------------------------------------- +# Upload a single file (used by /api/upload for each item) +# --------------------------------------------------------------------------- + +def handle_one_upload( + api: HfApi, + filename: str, + content: bytes, + custom_id: Optional[str], + expires_in_minutes: Optional[int], +) -> dict: + ext = os.path.splitext(filename)[1] + item_id = sanitize_id(custom_id, ext) if custom_id else random_id(ext) + + expires_at = None + if expires_in_minutes and expires_in_minutes > 0: + expires_at = (datetime.now(timezone.utc) + timedelta(minutes=expires_in_minutes)).isoformat() + + upload_bytes(api, content, item_id) + write_meta(api, item_id, expires_at) + + link_path = f"/api/file/{item_id}" if expires_at else f"/{item_id}" + return { + "item_id": item_id, + "link_path": link_path, + "expires_at": expires_at, + "size": len(content), + } + + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.post("/api/upload") +async def upload_files( + files: List[UploadFile] = File(...), + custom_id: Optional[str] = Form(None), + expires_in_minutes: Optional[int] = Form(None), +): + api = get_api() + ensure_repo(api) + + # custom_id only makes sense for a single file + if custom_id and len(files) > 1: + raise HTTPException(status_code=400, detail="custom_id can only be used when uploading a single file") + + results = [] + for f in files: + content = await f.read() + if len(content) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=413, + detail=f"'{f.filename}' exceeds the {MAX_UPLOAD_BYTES // (1024 * 1024)}MB request limit", + ) + try: + result = handle_one_upload(api, f.filename or "file", content, custom_id, expires_in_minutes) + results.append(result) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to upload '{f.filename}': {e}") + + return {"files": results} + + +@app.post("/api/upload-url") +async def upload_from_url( + url: str = Form(...), + custom_id: Optional[str] = Form(None), + expires_in_minutes: Optional[int] = Form(None), +): + if not url.startswith(("http://", "https://")): + raise HTTPException(status_code=400, detail="Only http/https URLs are supported") + + api = get_api() + ensure_repo(api) try: - ext = os.path.splitext(file.filename)[1] - - # Generate unique code if not provided - if not custom_id: - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") - random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=6)) - item_id = f"{timestamp}-{random_str}{ext}" - else: - item_id = custom_id - if ext and not item_id.endswith(ext): - item_id += ext - - # Read the file directly into server RAM (bytes) - content = await file.read() - - api = HfApi() - - # NEW: Automatically create the dataset on Hugging Face if it doesn't exist - api.create_repo( - repo_id=DATASET_REPO, - repo_type="dataset", - exist_ok=True, - token=HF_TOKEN, - private=False # Ensure it is public so your links work instantly + async with httpx.AsyncClient(follow_redirects=True, timeout=20.0) as client: + resp = await client.get(url) + resp.raise_for_status() + content = resp.content + except httpx.HTTPError as e: + raise HTTPException(status_code=400, detail=f"Could not fetch URL: {e}") + + if len(content) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=413, + detail=f"Remote file exceeds the {MAX_UPLOAD_BYTES // (1024 * 1024)}MB limit", ) - # Push directly from RAM to Hugging Face Dataset (Bypassing disk entirely) - api.upload_file( - path_or_fileobj=content, - path_in_repo=item_id, + filename = url.split("/")[-1].split("?")[0] or "file" + + try: + result = handle_one_upload(api, filename, content, custom_id, expires_in_minutes) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + return {"files": [result]} + + +@app.get("/api/file/{item_id:path}") +async def serve_expiring_file(item_id: str): + """Dynamic redirect for links that were uploaded with an expiry.""" + try: + meta_path = hf_hub_download( repo_id=DATASET_REPO, repo_type="dataset", - token=HF_TOKEN + filename=f"{item_id}.meta.json", + token=HF_TOKEN, ) - - return {"item_id": item_id} - + with open(meta_path) as fh: + meta = json.load(fh) + expires_at = meta.get("expires_at") + if expires_at and datetime.fromisoformat(expires_at) < datetime.now(timezone.utc): + return JSONResponse(status_code=410, content={"detail": "This link has expired."}) + except EntryNotFoundError: + pass # no meta file -> treat as non-expiring + except Exception: + pass # if metadata can't be checked, fail open rather than block a valid file + + return RedirectResponse(url=raw_url(item_id), status_code=302) + + +@app.get("/api/files") +async def list_files(): + api = get_api() + try: + entries = api.list_repo_files(repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + files = [] + for name in entries: + if name.endswith(".meta.json") or name == ".gitattributes": + continue + files.append({"item_id": name, "url": raw_url(name)}) + + files.sort(key=lambda x: x["item_id"], reverse=True) + return {"files": files}