$ 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