fix: prevent duplicate uploads and group asset commits

This commit is contained in:
Tom Boullay
2026-04-24 16:58:49 +02:00
parent fe8a6f0f54
commit 53c4c0ed60
15 changed files with 329 additions and 152 deletions
+23
View File
@@ -0,0 +1,23 @@
export type AssetCategory = 'color' | 'roughness' | 'normal' | 'metalness' | 'assets'
export function classifyAssetCategory(filename: string): AssetCategory {
const name = filename.toLowerCase().replace(/\.[^.]+$/, '')
if (name.includes('base_color') || name.includes('_color') || name === 'color') {
return 'color'
}
if (name.includes('roughness')) {
return 'roughness'
}
if (name.includes('normal')) {
return 'normal'
}
if (name.includes('metallic') || name.includes('metalness')) {
return 'metalness'
}
return 'assets'
}
+36 -15
View File
@@ -1,4 +1,6 @@
import type { AssetCategory } from './asset-classification'
import type { FileChange } from './types'
import type { PreparedAssetSummary } from './types'
/**
* Build a formatted commit message based on the upload context.
@@ -12,8 +14,7 @@ import type { FileChange } from './types'
export function buildCommitMessage(
folderName: string,
modelFilename: string,
textureNames: string[],
compressed: boolean,
assetSummaries: PreparedAssetSummary[],
isReplace: boolean,
fileChanges: Map<string, FileChange>,
deletedFileNames: string[],
@@ -25,36 +26,56 @@ export function buildCommitMessage(
const lines: string[] = [title, '']
// Model section — show status for new, changed, or unchanged
const modelSummary = assetSummaries.find((asset) => asset.kind === 'model')
const modelChange = fileChanges.get(modelFilename.toLowerCase())
if (modelChange === 'new') {
lines.push('📦 Model')
lines.push(`${modelFilename}${compressed ? ' (compressed)' : ''}`)
lines.push(`${modelFilename}${modelSummary?.compressed ? ' (compressed)' : ''}`)
} else if (modelChange === 'changed') {
lines.push('📦 Model')
lines.push(` 🔄 ${modelFilename}${compressed ? ' (compressed)' : ''}`)
lines.push(` 🔄 ${modelFilename}${modelSummary?.compressed ? ' (compressed)' : ''}`)
} else if (modelChange === 'unchanged') {
lines.push('📦 Model')
lines.push(` ↔️ ${modelFilename} (inchange)`)
lines.push(` ↔️ ${modelFilename}${modelSummary?.compressed ? ' (compressed)' : ' (inchange)'}`)
}
const textureLines: string[] = []
const grouped = new Map<AssetCategory, string[]>()
for (const textureName of textureNames) {
const change = fileChanges.get(textureName.toLowerCase())
for (const asset of assetSummaries) {
if (asset.kind === 'model' || !asset.category) continue
const change = fileChanges.get(asset.filename.toLowerCase())
if (change === 'new') {
textureLines.push(`${textureName}`)
const current = grouped.get(asset.category) || []
current.push(`${asset.filename}${asset.compressed ? ' (compressed)' : ''}`)
grouped.set(asset.category, current)
} else if (change === 'changed') {
textureLines.push(` 🔄 ${textureName}`)
const current = grouped.get(asset.category) || []
current.push(` 🔄 ${asset.filename}${asset.compressed ? ' (compressed)' : ''}`)
grouped.set(asset.category, current)
}
}
for (const name of deletedFileNames) {
textureLines.push(`${name} (supprime)`)
const sectionTitles: Record<AssetCategory, string> = {
color: '🎨 Textures (color)',
roughness: '🪶 Textures (roughness)',
normal: '🧭 Textures (normal)',
metalness: '🔩 Textures (metalness)',
assets: '🧩 Assets',
}
if (textureLines.length > 0) {
lines.push('🎨 Textures')
lines.push(...textureLines)
for (const category of ['color', 'roughness', 'normal', 'metalness', 'assets'] as const) {
const entries = grouped.get(category)
if (!entries || entries.length === 0) continue
lines.push('')
lines.push(sectionTitles[category])
lines.push(...entries)
}
if (deletedFileNames.length > 0) {
lines.push('')
lines.push('🗑 Deleted')
lines.push(...deletedFileNames.map((name) => `${name}`))
}
return lines.join('\n')
+21 -5
View File
@@ -4,7 +4,8 @@ import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises'
import { TMP_DIR } from '@/lib/constants'
import { compressWithBlender } from '@/lib/blender'
import { compressTextureBuffer } from '@/lib/texture-compression'
import type { ParsedFile } from '@/lib/types'
import { classifyAssetCategory } from '@/lib/asset-classification'
import type { ParsedFile, PreparedAssetSummary } from '@/lib/types'
interface PushFile {
path: string
@@ -19,7 +20,7 @@ interface PrepareGitAssetsParams {
interface PrepareGitAssetsResult {
filesToPush: PushFile[]
modelFilename: string
textureNames: string[]
assetSummaries: PreparedAssetSummary[]
compressed: boolean
compressionError?: string
}
@@ -29,7 +30,7 @@ export async function prepareGitAssets({
parsedFiles,
}: PrepareGitAssetsParams): Promise<PrepareGitAssetsResult> {
const filesToPush: PushFile[] = []
const textureNames: string[] = []
const assetSummaries: PreparedAssetSummary[] = []
let modelFilename = ''
let compressed = false
let compressionError: string | undefined
@@ -39,6 +40,7 @@ export async function prepareGitAssets({
if (pf.isModel) {
modelFilename = pf.filename
let modelCompressed = false
const tmpFolder = join(TMP_DIR, folderName)
await mkdir(tmpFolder, { recursive: true })
@@ -54,6 +56,7 @@ export async function prepareGitAssets({
if (result.success && existsSync(compressedPath)) {
content = await readFile(compressedPath)
compressed = true
modelCompressed = true
await unlink(compressedPath).catch(() => {})
} else {
compressionError = result.error
@@ -62,8 +65,14 @@ export async function prepareGitAssets({
await unlink(tmpFilePath).catch(() => {})
await rm(tmpFolder, { recursive: true, force: true }).catch(() => {})
}
assetSummaries.push({
filename: pf.filename,
kind: 'model',
compressed: modelCompressed,
})
} else {
textureNames.push(pf.filename)
const category = classifyAssetCategory(pf.filename)
const textureResult = await compressTextureBuffer(pf.filename, pf.buffer)
content = textureResult.buffer
@@ -71,6 +80,13 @@ export async function prepareGitAssets({
if (textureResult.error && !compressionError) {
compressionError = textureResult.error
}
assetSummaries.push({
filename: pf.filename,
kind: category === 'assets' ? 'asset' : 'texture',
category,
compressed: textureResult.compressed,
})
}
filesToPush.push({
@@ -82,7 +98,7 @@ export async function prepareGitAssets({
return {
filesToPush,
modelFilename,
textureNames,
assetSummaries,
compressed,
compressionError,
}
+9
View File
@@ -2,6 +2,8 @@
// Shared types
// ---------------------------------------------------------------------------
import type { AssetCategory } from './asset-classification'
export interface ParsedFile {
filename: string
buffer: Buffer
@@ -19,3 +21,10 @@ export interface RemoteFile {
name: string
size: number
}
export interface PreparedAssetSummary {
filename: string
kind: 'model' | 'texture' | 'asset'
category?: AssetCategory
compressed: boolean
}
+16
View File
@@ -0,0 +1,16 @@
const activeUploads = new Set<string>()
function buildKey(folderName: string) {
return folderName.toLowerCase()
}
export function acquireUploadLock(folderName: string): boolean {
const key = buildKey(folderName)
if (activeUploads.has(key)) return false
activeUploads.add(key)
return true
}
export function releaseUploadLock(folderName: string) {
activeUploads.delete(buildKey(folderName))
}