$ cat node-template.py

D

Drive 3D Model Loader

// Reads a 3D file from Drive (GLB, glTF, OBJ, STL, FBX, PLY, USDZ or .blend) and outputs it as a File for 3D nodes like Retopology.

Input
Storage
#drive#storage#3d#model#blend
template.py
1"""drive-3d-model-loader — read a 3D file from Drive, output it as a File.23Sibling of drive-image-reader. Privileged Drive IO is delegated to the4gais.drive SDK (USER_TOKEN + Drive API live there); this node imports no5HTTP client and uses no os/shutil (both are lint-blocked). Validation is by6EXTENSION, not MIME: Drive stores whatever the browser reported, and7browsers report an empty or generic MIME for .blend/.glb.8"""910from __future__ import annotations1112import json13import re14import sys15import traceback16from typing import Any1718from gais import Gais1920OUTPUT_DIR = "/data/output"2122# Keep in sync with EXTENSIONS_BY_CATEGORY.model in packages/file-utils.23MODEL_EXTENSIONS = (".glb", ".gltf", ".blend", ".obj", ".stl", ".fbx", ".ply", ".usdz")2425_UUID_RE = re.compile(26    r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",27    re.IGNORECASE,28)293031def _normalize_drive_item_id(value: Any) -> str:32    """Accept a bare UUID or a mention://drive_{file,folder}/<uuid> URI."""33    if not isinstance(value, str):34        raise ValueError(f"Drive item id must be a string, got {type(value).__name__}")35    s = value.strip()36    for prefix in ("mention://drive_file/", "mention://drive_folder/"):37        if s.startswith(prefix):38            s = s[len(prefix):]39            break40    if not _UUID_RE.match(s):41        raise ValueError(f"Drive item id is not a valid UUID: {value!r}")42    return s434445def _safe_output_name(name: Any, default: str) -> str:46    """Reduce a Drive-supplied name to a bare filename so it cannot escape47    OUTPUT_DIR (path traversal)."""48    base = str(name or "").replace("\\", "/").rsplit("/", 1)[-1].strip()49    if not base or base in (".", ".."):50        return default51    return base525354def _copy_to_output(src_path: str, name: str) -> None:55    """Stream-copy the downloaded file into /data/output in 1 MiB chunks."""56    with open(src_path, "rb") as src, open(f"{OUTPUT_DIR}/{name}", "wb") as dst:57        while True:58            chunk = src.read(1024 * 1024)59            if not chunk:60                break61            dst.write(chunk)626364def process(inputs: dict[str, Any]) -> dict[str, Any]:65    """Validate the input, download via gais.drive, stage the File output."""66    raw = inputs.get("driveItemId")67    if not raw:68        raise ValueError("Required input 'driveItemId' not provided")6970    if isinstance(raw, list):71        if len(raw) == 1:72            raw = raw[0]73        else:74            raise ValueError(75                f"Drive 3D Model Loader accepts a single file, got {len(raw)} items. "76                "Use one Drive 3D Model Loader per file."77            )78    elif not isinstance(raw, str):79        raise ValueError(f"driveItemId must be a string, got {type(raw).__name__}")8081    item_id = _normalize_drive_item_id(raw)8283    result = Gais.drive.download(item_id=item_id)84    file_name = _safe_output_name(result.metadata.get("name", "model.glb"), "model.glb")8586    lowered = file_name.lower()87    if not lowered.endswith(MODEL_EXTENSIONS):88        raise ValueError(89            f"Selected Drive item is not a supported 3D file ({file_name}). "90            f"Supported extensions: {', '.join(MODEL_EXTENSIONS)}."91        )9293    _copy_to_output(str(result.file("file")), file_name)94    return {"file": file_name}959697def main() -> None:98    try:99        envelope = json.loads(sys.stdin.read() or "{}")100        if not isinstance(envelope, dict):101            envelope = {}102        inputs = envelope.get("inputs", {}) or {}103        json.dump(process(inputs), sys.stdout)104    except Exception as e:105        print(106            json.dumps({107                "error": str(e),108                "errorType": type(e).__name__,109                "traceback": traceback.format_exc(),110            }),111            file=sys.stderr,112        )113        sys.exit(1)114115116if __name__ == "__main__":117    main()

$ git log --oneline

v1.1.0
HEAD
2026-08-18