fix: prevent duplicate uploads and group asset commits
This commit is contained in:
@@ -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
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user