339 lines
17 KiB
Markdown
339 lines
17 KiB
Markdown
# 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) for pushing via a GitHub-compatible API (GitHub or Gitea)
|
|
- [**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>@<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
|
|
|
|
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/<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
|
|
|
|
```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_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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```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.
|