fix: prevent duplicate uploads and group asset commits
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
uploadFile,
|
||||
findNextVersion,
|
||||
} from '@/lib/nextcloud'
|
||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -56,6 +57,13 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!acquireUploadLock(folderName)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' },
|
||||
{ status: 409 },
|
||||
)
|
||||
}
|
||||
|
||||
const basePath = process.env.NEXTCLOUD_BASE_PATH || 'Models'
|
||||
const vfFolderPath = `${basePath}/VF/${folderName}`
|
||||
|
||||
@@ -95,5 +103,7 @@ export async function POST(req: NextRequest) {
|
||||
{ success: false, error: `Drive echoue: ${message}` },
|
||||
{ status: 500 },
|
||||
)
|
||||
} finally {
|
||||
releaseUploadLock(folderName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getRemoteFolder, pushAllToGitHub } from '@/lib/github'
|
||||
import { buildCommitMessage } from '@/lib/commit-message'
|
||||
import { classifyFileChanges } from '@/lib/diff-files'
|
||||
import { prepareGitAssets } from '@/lib/prepare-git-assets'
|
||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -31,13 +32,21 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!acquireUploadLock(folderName)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' },
|
||||
{ status: 409 },
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// --- Process files (compress model + textures for Git) ---
|
||||
const {
|
||||
filesToPush,
|
||||
modelFilename,
|
||||
compressed,
|
||||
compressionError,
|
||||
textureNames,
|
||||
assetSummaries,
|
||||
} = await prepareGitAssets({ folderName, parsedFiles })
|
||||
|
||||
// --- Detect existing files and classify changes ---
|
||||
@@ -70,8 +79,12 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// --- Build commit message ---
|
||||
const commitMessage = buildCommitMessage(
|
||||
folderName, modelFilename, textureNames,
|
||||
compressed, isReplace, fileChanges, deletedFileNames,
|
||||
folderName,
|
||||
modelFilename,
|
||||
assetSummaries,
|
||||
isReplace,
|
||||
fileChanges,
|
||||
deletedFileNames,
|
||||
)
|
||||
|
||||
// --- Push all in one commit ---
|
||||
@@ -94,4 +107,7 @@ export async function POST(req: NextRequest) {
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
releaseUploadLock(folderName)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export default function Home() {
|
||||
|
||||
<UploadZone />
|
||||
|
||||
<footer className="text-gray-500 text-xs text-center">
|
||||
<footer className="mt-3 text-gray-500 text-xs text-center">
|
||||
Modeles : <span className="font-mono text-gray-400">.glb</span>
|
||||
<span className="mx-2">·</span>
|
||||
Textures : <span className="font-mono text-gray-400">.png · .jpg · .webp</span>
|
||||
|
||||
@@ -35,6 +35,8 @@ export default function UploadZone() {
|
||||
|
||||
const {
|
||||
isUploading,
|
||||
isChecking,
|
||||
isResolvingDriveError,
|
||||
globalError,
|
||||
setGlobalError,
|
||||
overwriteConfirm,
|
||||
@@ -43,6 +45,7 @@ export default function UploadZone() {
|
||||
setNoChangesFolder,
|
||||
driveError,
|
||||
handleUpload,
|
||||
handleOverwriteCancel,
|
||||
handleDriveContinue,
|
||||
handleDriveCancel,
|
||||
handleCancel,
|
||||
@@ -93,7 +96,7 @@ export default function UploadZone() {
|
||||
secret={secret}
|
||||
secretVisible={secretVisible}
|
||||
secretError={secretError}
|
||||
disabled={isUploading}
|
||||
disabled={isUploading || isChecking}
|
||||
onChange={handleSecretChange}
|
||||
onToggleVisible={toggleSecretVisible}
|
||||
/>
|
||||
@@ -101,7 +104,7 @@ export default function UploadZone() {
|
||||
{entries.length === 0 && (
|
||||
<div>
|
||||
<FolderDropzone
|
||||
isUploading={isUploading}
|
||||
isUploading={isUploading || isChecking}
|
||||
onFolderSelected={handleFolderSelected}
|
||||
onError={setGlobalError}
|
||||
/>
|
||||
@@ -128,6 +131,7 @@ export default function UploadZone() {
|
||||
|
||||
<ActionButtons
|
||||
isUploading={isUploading}
|
||||
isChecking={isChecking}
|
||||
isSecretEmpty={isSecretEmpty}
|
||||
hasPendingOrErrors={hasPendingOrErrors}
|
||||
allDone={allDone}
|
||||
@@ -141,8 +145,9 @@ export default function UploadZone() {
|
||||
<OverwriteConfirmModal
|
||||
folderName={overwriteConfirm.folderName}
|
||||
diffs={overwriteConfirm.diffs}
|
||||
onCancel={() => setOverwriteConfirm(null)}
|
||||
onCancel={handleOverwriteCancel}
|
||||
onConfirm={proceedUpload}
|
||||
disabled={isUploading || isChecking || isResolvingDriveError}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -162,6 +167,7 @@ export default function UploadZone() {
|
||||
error={driveError.error}
|
||||
onCancel={handleDriveCancel}
|
||||
onContinue={handleDriveContinue}
|
||||
disabled={isResolvingDriveError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ interface ModalActionsProps {
|
||||
onConfirm: () => void
|
||||
/** Tailwind classes for the confirm button (default: white bg) */
|
||||
confirmClassName?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function ModalActions({
|
||||
@@ -43,20 +44,23 @@ export function ModalActions({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmClassName = 'bg-white text-[#000000] hover:bg-gray-200',
|
||||
disabled = false,
|
||||
}: ModalActionsProps) {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={disabled}
|
||||
className="flex-1 bg-black-700 text-gray-300 font-medium text-sm
|
||||
py-2.5 px-4 rounded-xl border border-white/10 transition-colors duration-150
|
||||
py-2.5 px-4 rounded-xl border border-white/10 transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
hover:bg-black-600"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`flex-1 font-medium text-sm py-2.5 px-4 rounded-xl transition-colors duration-150 ${confirmClassName}`}
|
||||
disabled={disabled}
|
||||
className={`flex-1 font-medium text-sm py-2.5 px-4 rounded-xl transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed ${confirmClassName}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
interface ActionButtonsProps {
|
||||
isUploading: boolean
|
||||
isChecking: boolean
|
||||
isSecretEmpty: boolean
|
||||
hasPendingOrErrors: boolean
|
||||
allDone: boolean
|
||||
@@ -11,6 +12,7 @@ interface ActionButtonsProps {
|
||||
|
||||
export default function ActionButtons({
|
||||
isUploading,
|
||||
isChecking,
|
||||
isSecretEmpty,
|
||||
hasPendingOrErrors,
|
||||
allDone,
|
||||
@@ -19,11 +21,12 @@ export default function ActionButtons({
|
||||
onCancel,
|
||||
onReset,
|
||||
}: ActionButtonsProps) {
|
||||
const cantUpload = isSecretEmpty
|
||||
const cantUpload = isSecretEmpty || isChecking
|
||||
const isBusy = isUploading || isChecking
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
{!isUploading && hasPendingOrErrors && (
|
||||
{!isBusy && hasPendingOrErrors && (
|
||||
<button
|
||||
onClick={onUpload}
|
||||
disabled={cantUpload}
|
||||
@@ -38,18 +41,19 @@ export default function ActionButtons({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
{isBusy && (
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={!isUploading}
|
||||
className="flex-1 bg-black-700 text-gray-300 font-medium text-sm
|
||||
py-2.5 px-6 rounded-xl border border-black-600 transition-colors duration-150
|
||||
py-2.5 px-6 rounded-xl border border-black-600 transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
hover:bg-black-600"
|
||||
>
|
||||
Annuler
|
||||
{isChecking ? 'Verification...' : 'Annuler'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(allDone || hasErrors) && !isUploading && (
|
||||
{(allDone || hasErrors) && !isBusy && (
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="flex-1 bg-black-700 text-gray-300 font-medium text-sm
|
||||
|
||||
@@ -5,12 +5,14 @@ interface DriveErrorModalProps {
|
||||
error: string
|
||||
onCancel: () => void
|
||||
onContinue: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function DriveErrorModal({
|
||||
error,
|
||||
onCancel,
|
||||
onContinue,
|
||||
disabled = false,
|
||||
}: DriveErrorModalProps) {
|
||||
return (
|
||||
<Modal ariaLabelledBy="drive-error-title">
|
||||
@@ -41,6 +43,7 @@ export default function DriveErrorModal({
|
||||
confirmLabel="Envoyer sur Git seulement"
|
||||
onCancel={onCancel}
|
||||
onConfirm={onContinue}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Drive/Git status sub-line for FolderCard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { SpinnerIcon, XIcon, InfoIcon } from '@/components/ui/icons'
|
||||
import { SpinnerIcon, XIcon, WarningIcon } from '@/components/ui/icons'
|
||||
import type { FolderEntry } from '@/lib/client-types'
|
||||
|
||||
interface DriveStatusLineProps {
|
||||
@@ -35,7 +35,7 @@ export default function DriveStatusLine({ driveStatus, driveError }: DriveStatus
|
||||
)}
|
||||
{driveStatus === 'skipped' && (
|
||||
<>
|
||||
<InfoIcon className="w-3 h-3 text-yellow-400" />
|
||||
<WarningIcon className="w-3 h-3 text-yellow-400" />
|
||||
<span className="text-xs text-yellow-400">Drive ignore</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ interface OverwriteConfirmModalProps {
|
||||
diffs: FileDiff[]
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function OverwriteConfirmModal({
|
||||
@@ -14,6 +15,7 @@ export default function OverwriteConfirmModal({
|
||||
diffs,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
disabled = false,
|
||||
}: OverwriteConfirmModalProps) {
|
||||
return (
|
||||
<Modal ariaLabelledBy="overwrite-title">
|
||||
@@ -68,6 +70,7 @@ export default function OverwriteConfirmModal({
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
confirmClassName="bg-yellow-600 text-[#000000] hover:bg-yellow-500"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -26,6 +26,8 @@ export function useUploadOrchestrator({
|
||||
resetEntries,
|
||||
}: UseUploadOrchestratorParams) {
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
const [isResolvingDriveError, setIsResolvingDriveError] = useState(false)
|
||||
const [globalError, setGlobalError] = useState<string | null>(null)
|
||||
const [overwriteConfirm, setOverwriteConfirm] = useState<{
|
||||
folderName: string
|
||||
@@ -39,6 +41,7 @@ export function useUploadOrchestrator({
|
||||
|
||||
const abortRef = useRef<AbortController | null>(null)
|
||||
const checkResultRef = useRef<CheckResult>({ exists: false, diffs: [] })
|
||||
const uploadActionRef = useRef(false)
|
||||
|
||||
// Refs for values used inside callbacks to avoid stale closures
|
||||
const secretRef = useRef(secret)
|
||||
@@ -67,13 +70,17 @@ export function useUploadOrchestrator({
|
||||
|
||||
// ---- Main upload flow: Drive first, then Git ----
|
||||
const proceedUpload = useCallback(async () => {
|
||||
if (uploadActionRef.current) return
|
||||
uploadActionRef.current = true
|
||||
setOverwriteConfirm(null)
|
||||
setIsChecking(false)
|
||||
setIsUploading(true)
|
||||
setGlobalError(null)
|
||||
|
||||
const controller = new AbortController()
|
||||
abortRef.current = controller
|
||||
|
||||
try {
|
||||
const currentEntries = entriesRef.current
|
||||
|
||||
for (let i = 0; i < currentEntries.length; i++) {
|
||||
@@ -110,20 +117,26 @@ export function useUploadOrchestrator({
|
||||
// ---- Step 2: Git upload ----
|
||||
await pushGit(i, controller.signal)
|
||||
}
|
||||
|
||||
} finally {
|
||||
abortRef.current = null
|
||||
setIsUploading(false)
|
||||
uploadActionRef.current = false
|
||||
}
|
||||
}, [updateEntry, pushGit])
|
||||
|
||||
// ---- Handlers ----
|
||||
|
||||
const handleUpload = useCallback(async () => {
|
||||
if (uploadActionRef.current || isChecking || isUploading) return
|
||||
|
||||
if (!secretRef.current.trim()) {
|
||||
setSecretError("La cle d'acces est requise")
|
||||
return
|
||||
}
|
||||
if (entriesRef.current.length === 0) return
|
||||
|
||||
uploadActionRef.current = true
|
||||
setIsChecking(true)
|
||||
setSecretError(null)
|
||||
setGlobalError(null)
|
||||
|
||||
@@ -140,25 +153,35 @@ export function useUploadOrchestrator({
|
||||
if (check.exists) {
|
||||
if (check.diffs.length === 0) {
|
||||
setNoChangesFolder(folder.folderName)
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
return
|
||||
}
|
||||
setOverwriteConfirm({ folderName: folder.folderName, diffs: check.diffs })
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||
setGlobalError(message)
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
return
|
||||
}
|
||||
|
||||
uploadActionRef.current = false
|
||||
await proceedUpload()
|
||||
}, [setSecretError, proceedUpload])
|
||||
}, [setSecretError, proceedUpload, isChecking, isUploading])
|
||||
|
||||
const handleDriveContinue = useCallback(async () => {
|
||||
if (!driveError) return
|
||||
if (!driveError || uploadActionRef.current) return
|
||||
uploadActionRef.current = true
|
||||
setIsResolvingDriveError(true)
|
||||
const idx = driveError.folderIndex
|
||||
setDriveError(null)
|
||||
|
||||
try {
|
||||
updateEntry(idx, { driveStatus: 'skipped' })
|
||||
|
||||
const signal = abortRef.current?.signal
|
||||
@@ -178,9 +201,12 @@ export function useUploadOrchestrator({
|
||||
|
||||
await pushGit(i, abortRef.current?.signal)
|
||||
}
|
||||
|
||||
} finally {
|
||||
abortRef.current = null
|
||||
setIsUploading(false)
|
||||
setIsResolvingDriveError(false)
|
||||
uploadActionRef.current = false
|
||||
}
|
||||
}, [driveError, updateEntry, pushGit])
|
||||
|
||||
const handleDriveCancel = useCallback(() => {
|
||||
@@ -197,25 +223,40 @@ export function useUploadOrchestrator({
|
||||
|
||||
abortRef.current?.abort()
|
||||
abortRef.current = null
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
setIsResolvingDriveError(false)
|
||||
setIsUploading(false)
|
||||
}, [driveError, updateEntry])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isChecking) {
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
return
|
||||
}
|
||||
abortRef.current?.abort()
|
||||
abortRef.current = null
|
||||
uploadActionRef.current = false
|
||||
setIsResolvingDriveError(false)
|
||||
setIsUploading(false)
|
||||
}, [])
|
||||
}, [isChecking])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
resetEntries()
|
||||
setGlobalError(null)
|
||||
setIsChecking(false)
|
||||
setIsUploading(false)
|
||||
setIsResolvingDriveError(false)
|
||||
setDriveError(null)
|
||||
checkResultRef.current = { exists: false, diffs: [] }
|
||||
uploadActionRef.current = false
|
||||
}, [resetEntries])
|
||||
|
||||
return {
|
||||
isUploading,
|
||||
isChecking,
|
||||
isResolvingDriveError,
|
||||
globalError,
|
||||
setGlobalError,
|
||||
overwriteConfirm,
|
||||
@@ -224,6 +265,11 @@ export function useUploadOrchestrator({
|
||||
setNoChangesFolder,
|
||||
driveError,
|
||||
handleUpload,
|
||||
handleOverwriteCancel: () => {
|
||||
setOverwriteConfirm(null)
|
||||
uploadActionRef.current = false
|
||||
setIsChecking(false)
|
||||
},
|
||||
handleDriveContinue,
|
||||
handleDriveCancel,
|
||||
handleCancel,
|
||||
|
||||
@@ -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