# 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 - [**Next.js 16.2.5** (App Router)](https://nextjs.org/docs/app/getting-started/installation) + [React 19](https://react.dev/learn/creating-a-react-app) + [TypeScript](https://www.typescriptlang.org/docs/) - [**Three.js**](https://threejs.org/docs/#manual/en/introduction/Creating-a-scene) ([React Three Fiber](https://r3f.docs.pmnd.rs/getting-started/introduction) + [Drei](https://drei.docs.pmnd.rs/getting-started/introduction)) for 3D preview - [**Tailwind CSS**](https://v3.tailwindcss.com/docs/installation) for styling - [**Octokit**](https://github.com/octokit/rest.js/#readme) + provider adapters for GitHub and Gitea uploads - [**Nextcloud WebDAV**](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/index.html) for Drive archiving with automatic versioning - [**Sharp**](https://sharp.pixelplumbing.com/install/) for server-side texture compression - [**npm lockfile + Coolify** (Docker)](https://coolify.io/docs/applications/build-packs/dockerfile) for hosting ## Usage ### Development ```bash 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: ```bash 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: ```bash 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-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 5. **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. 6. **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 7. Orphan files (present on remote but not in the new upload) are deleted in the same commit 8. 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//` 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) ├── git/ # Git provider layer selected by env │ ├── config.ts # GIT_PROVIDER/GIT_REPO_URL/GIT_TOKEN parsing │ ├── content.ts # Shared remote folder/file content helpers │ ├── http.ts # Shared Git API request helpers │ ├── index.ts # Public getRemoteFolder/pushAllToGit facade │ ├── lfs.ts # Shared Git LFS upload helpers │ └── providers/ │ ├── gitea.ts # Gitea Contents API implementation │ └── github.ts # GitHub Git Data API implementation ├── 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 ```bash 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: ```env UPLOAD_SECRET_KEY=your-secret-key-here GIT_PROVIDER=gitea 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_PROVIDER` | Git provider adapter to use: `github` or `gitea`. If omitted, it is inferred from `GIT_REPO_URL` (`github.com` → GitHub, anything else → Gitea). | No | | `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. ### Git provider selection The upload routes call a small provider layer in `lib/git/`: - `lib/git/config.ts` reads `GIT_PROVIDER`, `GIT_REPO_URL`, `GIT_TOKEN`, `GIT_USERNAME`, and `GIT_BRANCH` - `lib/git/providers/github.ts` handles GitHub commits with the Git Data API - `lib/git/providers/gitea.ts` handles Gitea commits with the Contents API - `lib/git/lfs.ts` handles Git LFS upload/auth for both providers For GitHub: ```env GIT_PROVIDER=github GIT_TOKEN=ghp_xxx GIT_REPO_URL=https://github.com/org/repo.git GIT_BRANCH=main ``` For Gitea: ```env GIT_PROVIDER=gitea GIT_USERNAME=your-gitea-username GIT_TOKEN=token_xxx GIT_REPO_URL=https://git.fabrik.mathieu-chavanel.fr/math-pixel/La-Fabrik GIT_BRANCH=main ``` > 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. ```bash docker build -t upload-gltf . docker run -p 3000:3000 \ -e UPLOAD_SECRET_KEY=your-key \ -e GIT_PROVIDER=gitea \ -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: ```bash 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: ```bash 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](LICENSE). Copyright 2026 La Fabrik Durable. All rights reserved.