fix: batch Git LFS uploads

This commit is contained in:
Tom Boullay
2026-04-27 23:17:56 +02:00
parent 799e61c92e
commit b084c0e20e
2 changed files with 60 additions and 14 deletions
+13 -14
View File
@@ -10,27 +10,26 @@ import type { FileDiff } from '@/lib/types'
import { checkFolderDiffs, stageUpload, uploadDrive, uploadGit } from '@/lib/upload-api' import { checkFolderDiffs, stageUpload, uploadDrive, uploadGit } from '@/lib/upload-api'
import type { CheckResult } from '@/lib/upload-api' import type { CheckResult } from '@/lib/upload-api'
const UPLOAD_LOG_PREFIX = '[upload-gltf]'
function formatElapsed(startedAt: number) { function formatElapsed(startedAt: number) {
return `${((performance.now() - startedAt) / 1000).toFixed(1)}s` return `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
} }
function startTimedLog(label: string, details?: Record<string, unknown>) { function logUpload(level: 'INFO' | 'ERROR', step: string, action: string, startedAt: number, details?: Record<string, unknown>) {
const log = level === 'ERROR' ? console.error : console.info
log(`[${level}] ${step} -> ${action} | Timer: ${formatElapsed(startedAt)}`, details || '')
}
function startTimedLog(step: string, action: string, details?: Record<string, unknown>) {
const startedAt = performance.now() const startedAt = performance.now()
console.info(`${UPLOAD_LOG_PREFIX} ${label} started`, details || '') logUpload('INFO', step, `${action} started`, startedAt, details)
const interval = window.setInterval(() => { const interval = window.setInterval(() => {
console.info(`${UPLOAD_LOG_PREFIX} ${label} still running`, { elapsed: formatElapsed(startedAt), ...details }) logUpload('INFO', step, `${action} running`, startedAt, details)
}, 10_000) }, 10_000)
return (status: 'done' | 'failed' | 'cancelled' = 'done', extra?: Record<string, unknown>) => { return (status: 'done' | 'failed' | 'cancelled' = 'done', extra?: Record<string, unknown>) => {
window.clearInterval(interval) window.clearInterval(interval)
console.info(`${UPLOAD_LOG_PREFIX} ${label} ${status}`, { logUpload(status === 'failed' ? 'ERROR' : 'INFO', step, `${action} ${status}`, startedAt, { ...details, ...extra })
elapsed: formatElapsed(startedAt),
...details,
...extra,
})
} }
} }
@@ -83,7 +82,7 @@ export function useUploadOrchestrator({
} }
const folderName = entriesRef.current[index]?.folderName const folderName = entriesRef.current[index]?.folderName
const endGitLog = startTimedLog('Git upload', { folderName, stagingId }) const endGitLog = startTimedLog('Git', 'Upload', { folderName, stagingId })
let gitResult: Awaited<ReturnType<typeof uploadGit>> let gitResult: Awaited<ReturnType<typeof uploadGit>>
try { try {
@@ -145,7 +144,7 @@ export function useUploadOrchestrator({
driveError: undefined, driveError: undefined,
}) })
const endDriveLog = startTimedLog('Drive upload', { const endDriveLog = startTimedLog('Drive', 'Upload', {
folderName: folderEntry.folderName, folderName: folderEntry.folderName,
stagingId, stagingId,
action: driveAction, action: driveAction,
@@ -206,7 +205,7 @@ export function useUploadOrchestrator({
abortRef.current = controller abortRef.current = controller
try { try {
const endStageLog = startTimedLog('Verification: staging upload', { const endStageLog = startTimedLog('Verification', 'Staging', {
folderName: folder.folderName, folderName: folder.folderName,
files: 1 + folder.textures.length, files: 1 + folder.textures.length,
modelSize: folder.modelFile.size, modelSize: folder.modelFile.size,
@@ -225,7 +224,7 @@ export function useUploadOrchestrator({
stagingIdRef.current = staged.stagingId stagingIdRef.current = staged.stagingId
const endCheckLog = startTimedLog('Verification: GitHub diff check', { const endCheckLog = startTimedLog('Verification', 'GitHub diff', {
folderName: folder.folderName, folderName: folder.folderName,
stagingId: staged.stagingId, stagingId: staged.stagingId,
}) })
+47
View File
@@ -3,6 +3,8 @@ import { Octokit } from '@octokit/rest'
import { LFS_EXTENSIONS } from './constants' import { LFS_EXTENSIONS } from './constants'
import type { PushFile, RemoteFile } from './types' import type { PushFile, RemoteFile } from './types'
const LFS_BATCH_SIZE = 100
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Octokit helpers // Octokit helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -55,6 +57,24 @@ function parseLfsPointer(content: string): { oid: string; size: number } | null
return { oid: oidMatch[1], size: parseInt(sizeMatch[1], 10) } return { oid: oidMatch[1], size: parseInt(sizeMatch[1], 10) }
} }
function formatElapsed(startedAt: number) {
return `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
}
function logInfo(step: string, action: string, startedAt: number, details?: Record<string, unknown>) {
console.info(`[INFO] ${step} -> ${action} | Timer: ${formatElapsed(startedAt)}`, details || '')
}
function chunkArray<T>(items: T[], size: number) {
const chunks: T[][] = []
for (let i = 0; i < items.length; i += size) {
chunks.push(items.slice(i, i + size))
}
return chunks
}
interface LfsObject { interface LfsObject {
oid: string oid: string
size: number size: number
@@ -76,6 +96,25 @@ async function uploadToLfs(
): Promise<void> { ): Promise<void> {
if (objects.length === 0) return if (objects.length === 0) return
const batches = chunkArray(objects, LFS_BATCH_SIZE)
for (let i = 0; i < batches.length; i++) {
await uploadToLfsBatch(owner, repo, batches[i], i + 1, batches.length)
}
}
async function uploadToLfsBatch(
owner: string,
repo: string,
objects: LfsObject[],
batchNumber: number,
totalBatches: number,
): Promise<void> {
const startedAt = performance.now()
logInfo('Git LFS', `Batch ${batchNumber}/${totalBatches} started`, startedAt, {
objects: objects.length,
})
const token = process.env.GITHUB_TOKEN! const token = process.env.GITHUB_TOKEN!
const lfsUrl = `https://github.com/${owner}/${repo}.git/info/lfs/objects/batch` const lfsUrl = `https://github.com/${owner}/${repo}.git/info/lfs/objects/batch`
@@ -96,6 +135,10 @@ async function uploadToLfs(
if (!batchRes.ok) { if (!batchRes.ok) {
const text = await batchRes.text() const text = await batchRes.text()
console.error(`[ERROR] Git LFS -> Batch ${batchNumber}/${totalBatches} failed | Timer: ${formatElapsed(startedAt)}`, {
objects: objects.length,
status: batchRes.status,
})
throw new Error(`LFS batch request failed (${batchRes.status}): ${text}`) throw new Error(`LFS batch request failed (${batchRes.status}): ${text}`)
} }
@@ -165,6 +208,10 @@ async function uploadToLfs(
} }
} }
} }
logInfo('Git LFS', `Batch ${batchNumber}/${totalBatches} done`, startedAt, {
objects: objects.length,
})
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------