2026-05-17 14:03:30 +02:00
2026-05-15 01:08:44 +02:00
2026-05-17 14:03:30 +02:00
2026-04-03 10:22:24 +02:00
2026-04-03 10:41:56 +02:00
2026-05-15 00:15:42 +02:00
2026-04-27 23:57:12 +02:00
2026-05-15 00:15:42 +02:00
2026-04-03 10:22:24 +02:00
2026-05-15 01:19:22 +02:00
2026-04-03 11:32:56 +02:00

upload-GLTF

A secure web interface for uploading model.gltf with its associated .bin file and textures with two outputs:

  • Nextcloud Drive — Archives the original files with automatic versioning (VF/V1/V2...), so artists always have a history of past versions
  • Git remote — Delivers Draco-compressed GLB assets by default, with an optional GLTF delivery mode for specific models

Built for La Fabrik Durable's internal use, but open-sourced for anyone looking for a similar solution. The app validates the upload locally, stages it server-side, then compares file diffs to avoid unnecessary uploads and commits. The Drive upload serves as the source of truth and version history, while the Git upload delivers the prepared assets to developers.

Stack

Usage

Development

npm ci
npm run dev

The app runs on http://localhost:3000 with hot reload. The upload API routes are available under http://localhost:3000/api/upload/*

Use npm for this repo. package-lock.json is the source of truth for local installs and Coolify builds; no pnpm/yarn lockfile should be committed here.

Dependency and security policy

The project pins next to 16.2.5 to include the May 2026 WebSocket SSRF fix (CVE-2026-44578 / GHSA-c4j6-fc7j-m34r). Do not loosen this back to ^16.2.4 or any range that can resolve below 16.2.5.

This repo also keeps install-time package scripts disabled by default through .npmrc and Docker:

npm ci --ignore-scripts --no-audit --no-fund

When a dependency update is needed, prefer a lockfile-only update in a clean environment with no .env, no Git token, and no cloud credentials:

mkdir -p /tmp/npm-clean-home /tmp/npm-clean-cache
env -i \
  HOME=/tmp/npm-clean-home \
  PATH="$PATH" \
  npm_config_userconfig=/tmp/npm-clean-home/.npmrc \
  npm_config_cache=/tmp/npm-clean-cache \
  npm_config_ignore_scripts=true \
  npm_config_audit=false \
  npm_config_fund=false \
  npm install <package>@<version> --package-lock-only --ignore-scripts --no-audit --no-fund --save-exact

The May 2026 TanStack incident affected malicious package versions published to npm, not the npm CLI itself. This repo does not depend on @tanstack/*, but other projects should be checked for @tanstack/setup, github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c, router_init.js, tanstack_runner.js, and the fake unscoped package tanstack@2.0.4 through 2.0.7.

How it works

  1. The user enters their access key
  2. They select a folder containing:
    • model.gltf (required)
    • Any associated binary buffer (.bin, for example model.bin)
    • Any associated textures (.png/.jpg/.jpeg/.webp)
  3. The folder is validated locally. .glb files are not accepted.
  4. On clicking "Envoyer" or "Envoyer en GLTF":
    • The app uploads the folder once to a temporary server-side staging area
    • The app prepares the final Git payload from this staging area
    • The app checks the remote Git repo for existing files and computes diffs
    • If the folder doesn't exist, upload proceeds directly
    • If the folder exists and files differ, a confirmation dialog shows only the actual changes
    • If nothing changed, the upload is skipped entirely

Asset naming convention

Texture filenames can either use the internal convention or common GLTF export suffixes. The Git payload is normalized automatically and model.gltf texture URIs are rewritten to match the normalized filenames. Drive archiving keeps the original artist files.

Internal convention: use asset.png to apply a texture to the whole model, or asset_object.png to target a specific object. Allowed families are defined in lib/asset-naming.ts: color, diffuse, roughness, normal, metalness, height, opacity, orm, ao. Valid internal examples: color.png, diffuse_lampe.png, normal_cable1.png, opacity_lampe.png. Accepted export examples: lampe_baseColor.png, lampe_base_color.png, lampe_normal_opengl.png, lampe_metallic.png, lampe_occlusionRoughnessMetallic.png, lampe_mixed_ao.png. Git normalization examples: lampe_baseColor.png becomes color_lampe.png, lampe_metallic.png becomes metalness_lampe.png, lampe_occlusionRoughnessMetallic.png becomes orm_lampe.png, and lampe_mixed_ao.png becomes ao_lampe.png. When several exported textures would normalize to the same filename, all colliding variants keep their original names to avoid data loss. For example, chap_normal.png and chap_normal_opengl.png are both kept as-is. Invalid or unknown asset names still block the upload.

Upload flow: Drive first, then Git

  1. Drive upload (archiving) — Original files from the staging area are uploaded to the Nextcloud Drive with automatic versioning (see below). This serves as the artists' source of truth and version history. If the Drive upload fails, a modal asks the user whether to send to Git only or cancel entirely.
  2. Git upload (delivery to devs) — The prepared Git payload is reused from staging. By default, Blender exports a single model.glb with Draco compression. If Blender cannot process a specific model, the upload falls back to the separate model.gltf + .bin + compressed textures workflow and shows a warning. For specific models, "Envoyer en GLTF" keeps that separate GLTF delivery mode from the start.

Drive versioning (Nextcloud WebDAV)

The Drive uses a VF (version finale) / Vx (archived versions) structure:

Models/
  VF/                    ← latest version
    coffeetest/
      model.gltf
      model.bin
      color.jpg
  V1/                    ← first archive
    coffeetest/
  V2/                    ← second archive
    coffeetest/
  • New folder (doesn't exist in VF/): files are uploaded directly to VF/{folderName}/
  • Replace (folder exists in VF/ with diffs): VF/{folderName} is moved to Vx/{folderName} (next available version), then all files are re-uploaded to VF/{folderName}/
  • No changes: nothing happens on the Drive

All files are uploaded to VF/ (not just diffs), because the move operation empties the previous folder.

Upload safeguards

  • The upload flow prevents duplicate submissions on the client (Envoyer, overwrite confirmation, and "Git only" confirmation are locked while processing)
  • The server applies a lightweight per-folder lock on Drive and Git routes to avoid duplicate commits and concurrent writes
  • The folder is staged server-side so the browser sends the payload only once during the full upload flow
  • Invalid model.gltf JSON or malformed buffers entries block the upload before remote writes
  • Git LFS uploads are batched in groups of 100 objects to stay within the LFS Batch API limit

Commit messages

All changes are pushed in a single commit with a grouped formatted message:

New folder:

update: upload-gltf add a new model -> my-model

📦 Model
  ✅ model.glb (compressed)

Update (only one texture changed):

update: upload-gltf update -> coffeetest

📦 Model
  🔄 model.glb (compressed)

Commit sections:

  • 📦 Model
  • 🎨 Textures (color)
  • 🪶 Textures (roughness)
  • 🧭 Textures (normal)
  • 🔩 Textures (metalness)
  • 🧩 Assets
  • 🗑 Deleted

Symbols: new — 🔄 modified — ↔️ unchanged (model always re-pushed) — deleted

  1. Orphan files (present on remote but not in the new upload) are deleted in the same commit
  2. Default Git delivery pushes model.glb when Blender compression succeeds. If Blender fails, the app falls back to separate model.gltf, .bin, and compressed textures with a warning. "Envoyer en GLTF" always uses separate GLTF delivery.

Uploaded models are pushed to public/models/<folderName>/ in the target repo.

Limitations

  • Large uploads are staged once, but the Drive upload remains sequential.
  • Git LFS batch uploads are sequential by batch.
  • Default GLB Draco delivery reduces Git LFS usage by replacing many support files with one compressed model file.
  • Uploads expect a single model.gltf file plus optional flat support files (.bin, .png, .jpg, .jpeg, .webp).

Project Structure

app/
├── api/upload/
│   ├── stage/route.ts     # POST: upload folder once to temporary staging
│   ├── check/route.ts     # POST: prepare staged Git assets and compare with remote files
│   ├── drive/route.ts     # POST: upload staged originals to Nextcloud Drive (VF/Vx versioning)
│   └── git/route.ts       # POST: push staged prepared assets to the Git remote
├── globals.css            # Tailwind + CSS variable fonts
├── layout.tsx             # Root layout (next/font/google)
└── page.tsx               # Home page
components/
├── ui/
│   ├── icons.tsx              # Shared SVG icon components
│   └── Modal.tsx              # Shared modal wrapper + ModalActions
├── upload/
│   ├── SecretInput.tsx        # Access key input
│   ├── FolderDropzone.tsx     # Folder drag & drop / picker
│   ├── FolderCard.tsx         # Folder status card (Drive + Git)
│   ├── DriveStatusLine.tsx    # Drive/Git status sub-line
│   ├── WarningBanner.tsx      # Missing texture warnings
│   ├── OverwriteConfirmModal.tsx  # Diff confirmation dialog
│   ├── NoChangesModal.tsx     # "No changes detected" dialog
│   ├── DriveErrorModal.tsx    # "Drive failed, continue?" dialog
│   └── ActionButtons.tsx      # Upload / Cancel / Reset buttons
├── UploadZone.tsx         # Main upload page (rendering only)
├── ModelViewer.tsx        # 3D viewer shell, stats panel, and hierarchy panel
└── SceneViewer.tsx        # Three.js Canvas, model stats, and scene hierarchy extraction
hooks/
├── useSecret.ts               # Secret key state management
├── useFolderEntries.ts        # Folder entries state management
└── useUploadOrchestrator.ts   # Upload pipeline orchestration (Drive → Git)
lib/
├── constants.ts           # Shared constants and extensions
├── types.ts               # Server types (ParsedFile, FileDiff, staged asset metadata, etc.)
├── client-types.ts        # Client types (FolderEntry, DriveStatus, viewer contracts, etc.)
├── upload-api.ts          # Client-side API helpers (stage, check, uploadDrive, uploadGit)
├── guards.ts              # Shared runtime guards and error message helpers
├── diff-files.ts          # File diff classification (new/changed/unchanged/deleted)
├── sanitize.ts            # Filename sanitization
├── auth.ts                # Upload secret validation (timing-safe)
├── github.ts              # Octokit helpers for GitHub-compatible remotes
├── nextcloud.ts           # Nextcloud WebDAV client (native fetch, cached config)
├── upload-staging.ts      # Temporary server-side staging and prepared asset reuse
├── upload-lock.ts         # Lightweight in-memory per-folder upload lock
├── asset-classification.ts # Group assets by family for commit messages
├── asset-naming.ts       # Allowed asset families and naming convention helpers
├── blender.ts            # Blender Draco compression helper
├── commit-message.ts      # Commit message builder
├── parse-upload.ts        # FormData parser + validation
├── validate-folder.ts     # Client-side folder validation (discriminated union)
└── format-bytes.ts        # Byte formatting utility
scripts/
└── compress.py            # Blender Draco compression script
Dockerfile                 # Multi-stage build: Node 20 slim + Blender + tini
docker-entrypoint.sh       # Upload temp setup + Blender availability check

Installation

git clone https://github.com/La-Fabrik-Durable/upload-GLTF.git
cd upload-GLTF
npm ci

Configuration

Copy .env.example to .env.local and fill in the values:

UPLOAD_SECRET_KEY=your-secret-key-here
GIT_USERNAME=your-gitea-username
GIT_TOKEN=your-git-provider-token
GIT_BRANCH=main
GIT_REPO_URL=https://git.example.com/your-org/your-repo
# Nextcloud Drive (public share WebDAV)
NEXTCLOUD_URL=https://cloud.example.com
NEXTCLOUD_SHARE_TOKEN=your-public-share-token
NEXTCLOUD_SHARE_PASSWORD=
NEXTCLOUD_BASE_PATH=Models
Variable Description Required
UPLOAD_SECRET_KEY Secret key for upload authentication Yes
GIT_USERNAME Git username for Git LFS Basic auth on Gitea. Required for Gitea when LFS files are uploaded. Gitea LFS
GIT_TOKEN Git provider token with repository read/write access. GITHUB_TOKEN is still accepted for backward compatibility. Yes
GIT_BRANCH Target branch (default: main) No
GIT_REPO_URL Target GitHub or Gitea repository URL (owner/repo, HTTPS, or SSH) Yes
NEXTCLOUD_URL Nextcloud instance URL Yes
NEXTCLOUD_SHARE_TOKEN Public share token (the part after /s/ in the share link) Yes
NEXTCLOUD_SHARE_PASSWORD Public share password (empty if none) No
NEXTCLOUD_BASE_PATH Root folder on the Drive (default: Models) No

GitHub tokens need Contents: Read and write. Gitea tokens need repository read/write access.

To create a Nextcloud public share token: Nextcloud > Files > select folder > Share > Create public share > set permissions (write access required) > copy the share link and extract the token

Production (Coolify / Docker)

Coolify must build this repository with the included Dockerfile. The dependency stage copies .npmrc and runs npm ci --ignore-scripts --no-audit --no-fund, so the deployed dependency tree comes from package-lock.json.

After a security patch:

  1. Push the commit to both remotes.
  2. In Coolify, trigger a rebuild with cache disabled when possible.
  3. Confirm the build logs show npm ci --ignore-scripts --no-audit --no-fund.
  4. Confirm the app starts and the upload flow still reaches staging, Drive, and Git.
  5. Rotate secrets in Coolify, then redeploy once more with the new values.
docker build -t upload-gltf .
docker run -p 3000:3000 \
  -e UPLOAD_SECRET_KEY=your-key \
  -e GIT_USERNAME=your-gitea-username \
  -e GIT_TOKEN=token_xxx \
  -e GIT_REPO_URL=https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik \
  -e NEXTCLOUD_URL=https://cloud.example.com \
  -e NEXTCLOUD_SHARE_TOKEN=your-share-token \
  upload-gltf

The Docker image runs the Next.js app, Blender Draco compression, and server-side asset preparation in a single container. The docker-entrypoint.sh script creates the upload temp directory and reports Blender availability before launching the app.

Secret rotation

Rotate secrets after patching if the previous deployment exposed a vulnerable Next.js version, if a suspicious dependency install happened on a machine with credentials, or if you cannot prove the install host was clean.

Recommended order:

  1. Generate a new GIT_TOKEN limited to the target model repository with read/write access.
  2. Generate a new long random UPLOAD_SECRET_KEY.
  3. Regenerate the Nextcloud public share token or password when possible.
  4. Update the variables in Coolify.
  5. Redeploy the patched image.
  6. Revoke the old Git token and old Nextcloud share credentials after the new deployment is healthy.

Do not commit real .env files. .dockerignore excludes .env and .env.*, while keeping .env.example as documentation.

Publishing to remotes

This repo is mirrored to GitHub and Gitea:

git remote -v
git push origin main
git push gitea main

If your local git config has the pushall alias, it should be equivalent to:

git push origin main && git push gitea main

Supported Formats

Type Extensions
3D Models .gltf
Binary buffers .bin
Textures .png, .jpg, .jpeg, .webp

Git delivery outputs .glb by default, or keeps the source .gltf structure when "Envoyer en GLTF" is selected.

License

See MIT.

Copyright 2026 La Fabrik Durable. All rights reserved.

S
Description
A secure upload interface for 3D assets with automatic deployment to GitHub via Git LFS
Readme MIT 479 KiB
Languages
TypeScript 92%
Python 6.4%
Dockerfile 1%
Shell 0.5%
CSS 0.1%