upatde: dockerfile init blender
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Asset Bridge 3D — Dockerfile for Coolify
|
# Upload GLTF — Dockerfile for Coolify
|
||||||
# Node 20 Debian · Blender (headless) · Multi-stage build
|
# Node 20 Debian · Blender (headless) · Multi-stage build
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ RUN npm run build
|
|||||||
# --- Stage 3: Production -----------------------------------------------------
|
# --- Stage 3: Production -----------------------------------------------------
|
||||||
FROM node:20-slim AS runner
|
FROM node:20-slim AS runner
|
||||||
|
|
||||||
LABEL maintainer="Asset Bridge 3D"
|
LABEL maintainer="La Fabrik Durable"
|
||||||
LABEL description="Secure 3D asset upload interface with Draco compression and GitHub push"
|
LABEL description="Secure 3D asset upload interface with Draco compression and GitHub push"
|
||||||
|
|
||||||
# Install Blender (headless) + tini
|
# Install Blender (headless) + tini
|
||||||
|
|||||||
@@ -64,23 +64,27 @@ docker run -p 3000:3000 \
|
|||||||
upload-gltf
|
upload-gltf
|
||||||
```
|
```
|
||||||
|
|
||||||
The Dockerfile includes Blender headless for automatic Draco compression.
|
The Docker image includes Blender headless (installed once at build time). On startup, the entrypoint checks if Blender is available and logs its version. No extra configuration is needed in production — `BLENDER_PATH` defaults to `blender` which is in the container's PATH.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. The user enters their access key
|
1. The user enters their access key
|
||||||
2. They pick a **destination** (`farm`, `map`, `powergrid`, `workshop`, `general`, `environment`)
|
2. They pick a **destination** (`farm`, `map`, `powergrid`, `workshop`, `general`, `environment`)
|
||||||
3. They select a folder containing:
|
3. They select a folder containing:
|
||||||
- `model.glb` or `model.gltf` (required)
|
- `model.glb` or `model.gltf` (**required**)
|
||||||
- Textures: `roughness`, `normal`, `metalness`, `color`, `displace` (`.png/.jpg/.webp`, optional)
|
- Textures: `roughness`, `normal`, `metalness`, `color`, `displace` (`.png/.jpg/.webp`, **optional** — missing textures show a warning but don't block the upload)
|
||||||
4. The model is displayed in a 3D preview
|
4. The model is displayed in a 3D preview
|
||||||
5. On clicking "Envoyer sur GitHub":
|
5. On clicking "Envoyer sur GitHub":
|
||||||
- The app checks if the folder already exists on the remote repo
|
- The app computes the git SHA of each local file and compares with the remote repo
|
||||||
- If it exists, a confirmation dialog is shown listing the existing files that will be replaced
|
- If the folder doesn't exist, upload proceeds directly
|
||||||
- On confirmation (or if the folder is new), all files are sent to the `/api/upload` endpoint
|
- If the folder exists and files differ, a confirmation dialog shows **only the actual changes** (modified, new, or deleted files)
|
||||||
|
- If the folder exists and nothing changed, the upload is skipped entirely ("Aucun fichier modifie")
|
||||||
6. For models: the file is written to `/tmp`, compressed with Blender Draco, then the compressed version is pushed
|
6. For models: the file is written to `/tmp`, compressed with Blender Draco, then the compressed version is pushed
|
||||||
7. For textures: pushed directly without compression
|
7. For textures: pushed directly without compression
|
||||||
8. All files are pushed in a **single commit** with a formatted message:
|
8. **Only changed and new files are pushed** — unchanged files are skipped to save bandwidth and API calls
|
||||||
|
9. All changes are pushed in a **single commit** with a formatted message that only lists what changed:
|
||||||
|
|
||||||
|
**New folder:**
|
||||||
```
|
```
|
||||||
update: upload-gltf add a new model -> farm/my-model
|
update: upload-gltf add a new model -> farm/my-model
|
||||||
|
|
||||||
@@ -90,8 +94,19 @@ The Dockerfile includes Blender headless for automatic Draco compression.
|
|||||||
✅ color.jpg
|
✅ color.jpg
|
||||||
❌ metalness (manquant)
|
❌ metalness (manquant)
|
||||||
```
|
```
|
||||||
9. If the folder already existed, orphan files (present in the old version but not in the new upload) are deleted in the same commit
|
|
||||||
10. If Blender is unavailable, the original model is pushed as-is (graceful fallback)
|
**Update (only metalness changed):**
|
||||||
|
```
|
||||||
|
update: upload-gltf update -> general/coffeetest
|
||||||
|
|
||||||
|
🎨 Textures
|
||||||
|
🔄 metalness.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Symbols: `✅` new — `🔄` modified — `❌` missing or deleted
|
||||||
|
|
||||||
|
10. Orphan files (present on remote but not in the new upload) are deleted in the same commit
|
||||||
|
11. If Blender is unavailable, the original model is pushed as-is (graceful fallback)
|
||||||
|
|
||||||
## Destinations
|
## Destinations
|
||||||
|
|
||||||
@@ -110,16 +125,18 @@ Uploaded models are pushed to `public/models/<destination>/<folderName>/` in the
|
|||||||
|
|
||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
├── api/upload/route.ts # API: GET (check existence) + POST (compress + push)
|
├── api/upload/route.ts # API: GET (check + SHA diff) + POST (compress + smart push)
|
||||||
├── globals.css # Tailwind + Google Fonts
|
├── globals.css # Tailwind + Google Fonts
|
||||||
├── layout.tsx # Root layout
|
├── layout.tsx # Root layout
|
||||||
└── page.tsx # Home page
|
└── page.tsx # Home page
|
||||||
components/
|
components/
|
||||||
├── UploadZone.tsx # UI: key input, destination picker, folder picker, validation, overwrite confirmation, upload
|
├── UploadZone.tsx # UI: key input, destination picker, folder picker, SHA diff, overwrite confirmation, upload
|
||||||
├── ModelViewer.tsx # Lazy wrapper for the 3D viewer
|
├── ModelViewer.tsx # Lazy wrapper for the 3D viewer
|
||||||
└── SceneViewer.tsx # Three.js Canvas
|
└── SceneViewer.tsx # Three.js Canvas
|
||||||
scripts/
|
scripts/
|
||||||
└── compress.py # Blender Draco compression script
|
└── compress.py # Blender Draco compression script
|
||||||
|
Dockerfile # Multi-stage build: Node 20 slim + Blender headless + tini
|
||||||
|
docker-entrypoint.sh # Startup: Blender check + launch
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported Formats
|
## Supported Formats
|
||||||
|
|||||||
+118
-27
@@ -5,6 +5,7 @@ import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises'
|
|||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
|
import { createHash } from 'crypto'
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile)
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
@@ -46,6 +47,13 @@ function parseRepoUrl(): { owner: string; repo: string } {
|
|||||||
return { owner: match[1], repo: match[2] }
|
return { owner: match[1], repo: match[2] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compute the SHA that Git would assign to a blob with this content */
|
||||||
|
function computeGitBlobSha(content: Buffer): string {
|
||||||
|
const header = `blob ${content.length}\0`
|
||||||
|
const store = Buffer.concat([Buffer.from(header), content])
|
||||||
|
return createHash('sha1').update(store).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Blender Draco compression
|
// Blender Draco compression
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -94,29 +102,60 @@ function buildCommitMessage(
|
|||||||
modelFilename: string,
|
modelFilename: string,
|
||||||
textureNames: string[],
|
textureNames: string[],
|
||||||
compressed: boolean,
|
compressed: boolean,
|
||||||
|
isReplace: boolean,
|
||||||
|
fileChanges: Map<string, 'new' | 'changed' | 'unchanged'>,
|
||||||
|
deletedFileNames: string[],
|
||||||
): string {
|
): string {
|
||||||
const title = `update: upload-gltf add a new model -> ${destination}/${folderName}`
|
const title = isReplace
|
||||||
|
? `update: upload-gltf update -> ${destination}/${folderName}`
|
||||||
|
: `update: upload-gltf add a new model -> ${destination}/${folderName}`
|
||||||
|
|
||||||
|
const lines: string[] = [title, '']
|
||||||
|
|
||||||
|
// Model section — only show if changed or new
|
||||||
|
const modelChange = fileChanges.get(modelFilename.toLowerCase())
|
||||||
|
if (modelChange === 'new') {
|
||||||
|
lines.push('📦 Model')
|
||||||
|
lines.push(` ✅ ${modelFilename}${compressed ? ' (compressed)' : ''}`)
|
||||||
|
} else if (modelChange === 'changed') {
|
||||||
|
lines.push('📦 Model')
|
||||||
|
lines.push(` 🔄 ${modelFilename}${compressed ? ' (compressed)' : ''}`)
|
||||||
|
}
|
||||||
|
// unchanged → don't show model section at all
|
||||||
|
|
||||||
|
// Textures section — only show lines that have changes
|
||||||
const foundTextures = new Set(
|
const foundTextures = new Set(
|
||||||
textureNames.map(t => t.toLowerCase().replace(/\.[^.]+$/, ''))
|
textureNames.map(t => t.toLowerCase().replace(/\.[^.]+$/, ''))
|
||||||
)
|
)
|
||||||
|
|
||||||
const textureLines = REQUIRED_TEXTURES.map(tex => {
|
const textureLines: string[] = []
|
||||||
if (foundTextures.has(tex)) {
|
|
||||||
const actual = textureNames.find(t => t.toLowerCase().replace(/\.[^.]+$/, '') === tex)
|
|
||||||
return ` ✅ ${actual}`
|
|
||||||
}
|
|
||||||
return ` ❌ ${tex} (manquant)`
|
|
||||||
})
|
|
||||||
|
|
||||||
const lines = [
|
// Changed or new textures
|
||||||
title,
|
for (const tex of REQUIRED_TEXTURES) {
|
||||||
'',
|
if (foundTextures.has(tex)) {
|
||||||
'📦 Model',
|
const actual = textureNames.find(t => t.toLowerCase().replace(/\.[^.]+$/, '') === tex)!
|
||||||
` ✅ ${modelFilename}${compressed ? ' (compressed)' : ''}`,
|
const change = fileChanges.get(actual.toLowerCase())
|
||||||
'🎨 Textures',
|
if (change === 'new') {
|
||||||
...textureLines,
|
textureLines.push(` ✅ ${actual}`)
|
||||||
]
|
} else if (change === 'changed') {
|
||||||
|
textureLines.push(` 🔄 ${actual}`)
|
||||||
|
}
|
||||||
|
// unchanged → skip
|
||||||
|
} else if (!isReplace) {
|
||||||
|
// Only show missing textures for new uploads
|
||||||
|
textureLines.push(` ❌ ${tex} (manquant)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted files (orphans removed from remote)
|
||||||
|
for (const name of deletedFileNames) {
|
||||||
|
textureLines.push(` ❌ ${name} (supprime)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textureLines.length > 0) {
|
||||||
|
lines.push('🎨 Textures')
|
||||||
|
lines.push(...textureLines)
|
||||||
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
@@ -313,7 +352,7 @@ export async function GET(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
const existingFiles = data.map(f => f.name)
|
const existingFiles = data.map(f => ({ name: f.name, sha: f.sha }))
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
exists: true,
|
exists: true,
|
||||||
@@ -412,12 +451,9 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Build commit message ---
|
// --- Detect existing files and compare SHA to classify changes ---
|
||||||
const commitMessage = buildCommitMessage(folderName, destination, modelFilename, textureNames, compressed)
|
|
||||||
|
|
||||||
// --- Detect existing files to clean up orphans ---
|
|
||||||
const folderPath = `public/models/${destination}/${folderName}`
|
const folderPath = `public/models/${destination}/${folderName}`
|
||||||
let existingFilePaths: string[] = []
|
const remoteFileMap = new Map<string, string>() // name -> sha
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const octokit = getOctokit()
|
const octokit = getOctokit()
|
||||||
@@ -432,23 +468,78 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
existingFilePaths = data.map(f => `${folderPath}/${f.name}`)
|
for (const f of data) {
|
||||||
|
remoteFileMap.set(f.name.toLowerCase(), f.sha)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 404 = folder doesn't exist yet, no cleanup needed
|
// 404 = folder doesn't exist yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isReplace = remoteFileMap.size > 0
|
||||||
|
|
||||||
|
// Classify each file: changed, new, or unchanged
|
||||||
|
type FileChange = 'new' | 'changed' | 'unchanged'
|
||||||
|
const fileChanges = new Map<string, FileChange>()
|
||||||
|
const changedFilesToPush: { path: string; contentBase64: string }[] = []
|
||||||
|
|
||||||
|
for (const f of filesToPush) {
|
||||||
|
const filename = f.path.split('/').pop()!
|
||||||
|
const localSha = computeGitBlobSha(Buffer.from(f.contentBase64, 'base64'))
|
||||||
|
const remoteSha = remoteFileMap.get(filename.toLowerCase())
|
||||||
|
|
||||||
|
if (!remoteSha) {
|
||||||
|
fileChanges.set(filename.toLowerCase(), 'new')
|
||||||
|
changedFilesToPush.push(f)
|
||||||
|
} else if (remoteSha !== localSha) {
|
||||||
|
fileChanges.set(filename.toLowerCase(), 'changed')
|
||||||
|
changedFilesToPush.push(f)
|
||||||
|
} else {
|
||||||
|
fileChanges.set(filename.toLowerCase(), 'unchanged')
|
||||||
|
// skip — don't push unchanged files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files on remote that are not in the new upload → will be deleted (orphans)
|
||||||
|
const newFileNames = new Set(filesToPush.map(f => f.path.split('/').pop()!.toLowerCase()))
|
||||||
|
const deletedFileNames: string[] = []
|
||||||
|
const deletePaths: string[] = []
|
||||||
|
for (const [name] of remoteFileMap) {
|
||||||
|
if (!newFileNames.has(name)) {
|
||||||
|
deletedFileNames.push(name)
|
||||||
|
deletePaths.push(`${folderPath}/${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing changed at all, don't create an empty commit
|
||||||
|
if (changedFilesToPush.length === 0 && deletePaths.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
folderName,
|
||||||
|
filesCount: 0,
|
||||||
|
compressed,
|
||||||
|
compressionError: compressionError || undefined,
|
||||||
|
message: 'Aucun fichier modifie — rien a envoyer.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Build commit message ---
|
||||||
|
const commitMessage = buildCommitMessage(
|
||||||
|
folderName, destination, modelFilename, textureNames,
|
||||||
|
compressed, isReplace, fileChanges, deletedFileNames,
|
||||||
|
)
|
||||||
|
|
||||||
// --- Push all in one commit ---
|
// --- Push all in one commit ---
|
||||||
try {
|
try {
|
||||||
const { commitUrl } = await pushAllToGitHub(folderName, filesToPush, existingFilePaths, commitMessage)
|
const { commitUrl } = await pushAllToGitHub(folderName, changedFilesToPush, deletePaths, commitMessage)
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
folderName,
|
folderName,
|
||||||
filesCount: filesToPush.length,
|
filesCount: changedFilesToPush.length,
|
||||||
compressed,
|
compressed,
|
||||||
compressionError: compressionError || undefined,
|
compressionError: compressionError || undefined,
|
||||||
message: `${filesToPush.length} fichier(s) envoye(s) sur GitHub en un seul commit.`,
|
message: `${changedFilesToPush.length} fichier(s) modifie(s) envoye(s) sur GitHub en un seul commit.`,
|
||||||
commitUrl,
|
commitUrl,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+105
-23
@@ -88,23 +88,85 @@ function validateFolder(files: File[]): { model?: File; textures: TextureFile[];
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFolderExists(
|
/** Compute the git blob SHA1 for a file (same as `git hash-object`) */
|
||||||
folderName: string,
|
async function computeGitBlobSha(file: File): Promise<string> {
|
||||||
|
const buffer = await file.arrayBuffer()
|
||||||
|
const content = new Uint8Array(buffer)
|
||||||
|
const header = new TextEncoder().encode(`blob ${content.length}\0`)
|
||||||
|
const store = new Uint8Array(header.length + content.length)
|
||||||
|
store.set(header)
|
||||||
|
store.set(content, header.length)
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-1', store)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileDiff {
|
||||||
|
name: string
|
||||||
|
status: 'changed' | 'new' | 'deleted'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckResult {
|
||||||
|
exists: boolean
|
||||||
|
diffs: FileDiff[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkFolderDiffs(
|
||||||
|
folder: FolderEntry,
|
||||||
destination: string,
|
destination: string,
|
||||||
secret: string,
|
secret: string,
|
||||||
): Promise<{ exists: boolean; files: string[] }> {
|
): Promise<CheckResult> {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ folderName, destination })
|
const params = new URLSearchParams({ folderName: folder.folderName, destination })
|
||||||
const res = await fetch(`/api/upload?${params}`, {
|
const res = await fetch(`/api/upload?${params}`, {
|
||||||
headers: { 'x-upload-secret': secret.trim() },
|
headers: { 'x-upload-secret': secret.trim() },
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.success && data.exists) {
|
if (!data.success || !data.exists) {
|
||||||
return { exists: true, files: data.files || [] }
|
return { exists: false, diffs: [] }
|
||||||
}
|
}
|
||||||
return { exists: false, files: [] }
|
|
||||||
|
const remoteFiles: { name: string; sha: string }[] = data.files || []
|
||||||
|
const remoteMap = new Map(remoteFiles.map(f => [f.name.toLowerCase(), f.sha]))
|
||||||
|
|
||||||
|
// Compute SHA for all local files
|
||||||
|
const localFiles: { name: string; sha: string }[] = []
|
||||||
|
localFiles.push({
|
||||||
|
name: folder.modelFile.name,
|
||||||
|
sha: await computeGitBlobSha(folder.modelFile),
|
||||||
|
})
|
||||||
|
for (const tex of folder.textures) {
|
||||||
|
localFiles.push({
|
||||||
|
name: tex.name,
|
||||||
|
sha: await computeGitBlobSha(tex.file),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffs: FileDiff[] = []
|
||||||
|
const localNames = new Set<string>()
|
||||||
|
|
||||||
|
for (const local of localFiles) {
|
||||||
|
const key = local.name.toLowerCase()
|
||||||
|
localNames.add(key)
|
||||||
|
const remoteSha = remoteMap.get(key)
|
||||||
|
if (!remoteSha) {
|
||||||
|
diffs.push({ name: local.name, status: 'new' })
|
||||||
|
} else if (remoteSha !== local.sha) {
|
||||||
|
diffs.push({ name: local.name, status: 'changed' })
|
||||||
|
}
|
||||||
|
// unchanged → not in diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files on remote but not in local → deleted
|
||||||
|
for (const [name] of remoteMap) {
|
||||||
|
if (!localNames.has(name)) {
|
||||||
|
diffs.push({ name, status: 'deleted' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { exists: true, diffs }
|
||||||
} catch {
|
} catch {
|
||||||
return { exists: false, files: [] }
|
return { exists: false, diffs: [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +225,7 @@ export default function UploadZone() {
|
|||||||
const [secretError, setSecretError] = useState<string | null>(null)
|
const [secretError, setSecretError] = useState<string | null>(null)
|
||||||
const [destination, setDestination] = useState<typeof DESTINATIONS[number]['value']>(DESTINATIONS[0].value)
|
const [destination, setDestination] = useState<typeof DESTINATIONS[number]['value']>(DESTINATIONS[0].value)
|
||||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||||
const [overwriteConfirm, setOverwriteConfirm] = useState<{ folderName: string; files: string[] } | null>(null)
|
const [overwriteConfirm, setOverwriteConfirm] = useState<{ folderName: string; diffs: FileDiff[] } | null>(null)
|
||||||
|
|
||||||
const isSecretEmpty = !secret.trim()
|
const isSecretEmpty = !secret.trim()
|
||||||
|
|
||||||
@@ -181,11 +243,16 @@ export default function UploadZone() {
|
|||||||
setSecretError(null)
|
setSecretError(null)
|
||||||
setGlobalError(null)
|
setGlobalError(null)
|
||||||
|
|
||||||
// Check if folder already exists on remote
|
// Check if folder already exists on remote and compute diffs
|
||||||
const folder = files[0]
|
const folder = files[0]
|
||||||
const check = await checkFolderExists(folder.folderName, destination, secret)
|
const check = await checkFolderDiffs(folder, destination, secret)
|
||||||
if (check.exists) {
|
if (check.exists) {
|
||||||
setOverwriteConfirm({ folderName: folder.folderName, files: check.files })
|
if (check.diffs.length === 0) {
|
||||||
|
// Nothing changed at all
|
||||||
|
setGlobalError('Aucun fichier modifie — le dossier distant est identique.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,26 +599,41 @@ export default function UploadZone() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-100">Dossier deja existant</h3>
|
<h3 className="text-sm font-semibold text-gray-100">Dossier deja existant</h3>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
<span className="font-mono text-yellow-400">{destination}/{overwriteConfirm.folderName}</span> existe deja sur le repo.
|
<span className="font-mono text-yellow-400">{destination}/{overwriteConfirm.folderName}</span> existe deja sur le repo
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{overwriteConfirm.files.length > 0 && (
|
{overwriteConfirm.diffs.length > 0 && (
|
||||||
<div className="bg-black-800 border border-white/10 rounded-xl p-3 max-h-32 overflow-y-auto">
|
<div className="bg-black-800 border border-white/10 rounded-xl p-3 max-h-40 overflow-y-auto">
|
||||||
<p className="text-xs text-gray-500 mb-1.5">Fichiers existants qui seront remplaces :</p>
|
<p className="text-xs text-gray-500 mb-2">Modifications detectees :</p>
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-1">
|
||||||
{overwriteConfirm.files.map((f) => (
|
{overwriteConfirm.diffs.map((d) => (
|
||||||
<li key={f} className="text-xs text-gray-400 font-mono">{f}</li>
|
<li key={d.name} className="flex items-center gap-2 text-xs font-mono">
|
||||||
|
{d.status === 'changed' && (
|
||||||
|
<>
|
||||||
|
<span className="text-yellow-400">🔄</span>
|
||||||
|
<span className="text-gray-300">{d.name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{d.status === 'new' && (
|
||||||
|
<>
|
||||||
|
<span className="text-green-400">✅</span>
|
||||||
|
<span className="text-gray-300">{d.name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{d.status === 'deleted' && (
|
||||||
|
<>
|
||||||
|
<span className="text-red-400">❌</span>
|
||||||
|
<span className="text-gray-500 line-through">{d.name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-400">
|
|
||||||
Les anciens fichiers seront supprimes et remplaces par les nouveaux. Cette action est irreversible.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setOverwriteConfirm(null)}
|
onClick={() => setOverwriteConfirm(null)}
|
||||||
|
|||||||
+11
-2
@@ -1,11 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "[entrypoint] Starting Asset Bridge 3D..."
|
echo "[upload-gltf] Starting Upload GLTF..."
|
||||||
|
|
||||||
# Ensure tmp directory for uploads exists
|
# Ensure tmp directory for uploads exists
|
||||||
mkdir -p /tmp/assets
|
mkdir -p /tmp/assets
|
||||||
|
|
||||||
echo "[entrypoint] Ready. Launching application..."
|
# Check if Blender is available for Draco compression
|
||||||
|
if command -v blender > /dev/null 2>&1; then
|
||||||
|
BLENDER_VERSION=$(blender --version 2>/dev/null | head -n 1)
|
||||||
|
echo "[upload-gltf] Blender found: $BLENDER_VERSION"
|
||||||
|
echo "[upload-gltf] Draco compression is enabled."
|
||||||
|
else
|
||||||
|
echo "[upload-gltf] WARNING: Blender not found. Models will be pushed without compression."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[upload-gltf] Ready. Launching application..."
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user