fix: harden upload resilience and contracts
This commit is contained in:
@@ -4,8 +4,7 @@ import { getRemoteFolder } from '@/lib/github'
|
||||
import { classifyFileChanges } from '@/lib/diff-files'
|
||||
import { getModelFolderPath } from '@/lib/model-paths'
|
||||
import { ensurePreparedStagingAssets } from '@/lib/upload-staging'
|
||||
import { parseStagingRequestBody } from '@/lib/upload-request'
|
||||
import { getErrorMessage } from '@/lib/guards'
|
||||
import { readStagingRequestBody, uploadErrorResponse } from '@/lib/upload-request'
|
||||
import type { FileDiff } from '@/lib/types'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
@@ -22,15 +21,13 @@ export async function POST(req: NextRequest) {
|
||||
let stagingId: string
|
||||
|
||||
try {
|
||||
const body: unknown = await req.json()
|
||||
stagingId = parseStagingRequestBody(body).stagingId
|
||||
stagingId = (await readStagingRequestBody(req)).stagingId
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
return uploadErrorResponse(err, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const { folderName, filesToPush } = await ensurePreparedStagingAssets(stagingId)
|
||||
const { folderName, filesToPush, deliveryMode, compressionError } = await ensurePreparedStagingAssets(stagingId)
|
||||
const folderPath = getModelFolderPath(folderName)
|
||||
const { exists, files } = await getRemoteFolder(folderPath)
|
||||
|
||||
@@ -53,12 +50,18 @@ export async function POST(req: NextRequest) {
|
||||
exists: true,
|
||||
path: folderPath,
|
||||
diffs,
|
||||
deliveryMode,
|
||||
compressionError,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, exists: false })
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
exists: false,
|
||||
deliveryMode,
|
||||
compressionError,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
return uploadErrorResponse(err, 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
findNextVersion,
|
||||
} from '@/lib/nextcloud'
|
||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||
import { parseDriveRequestBody } from '@/lib/upload-request'
|
||||
import {
|
||||
readDriveRequestBody,
|
||||
uploadErrorMessageResponse,
|
||||
uploadErrorResponse,
|
||||
uploadLockConflictResponse,
|
||||
} from '@/lib/upload-request'
|
||||
import { getErrorMessage } from '@/lib/guards'
|
||||
import type { DriveAction } from '@/lib/types'
|
||||
|
||||
@@ -20,9 +25,9 @@ export async function POST(req: NextRequest) {
|
||||
if (authError) return authError
|
||||
|
||||
if (!process.env.NEXTCLOUD_URL || !process.env.NEXTCLOUD_SHARE_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Nextcloud non configure sur le serveur (NEXTCLOUD_URL, NEXTCLOUD_SHARE_TOKEN)' },
|
||||
{ status: 500 },
|
||||
return uploadErrorMessageResponse(
|
||||
'Nextcloud non configure sur le serveur (NEXTCLOUD_URL, NEXTCLOUD_SHARE_TOKEN)',
|
||||
500,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,23 +36,18 @@ export async function POST(req: NextRequest) {
|
||||
let action: DriveAction
|
||||
|
||||
try {
|
||||
const body: unknown = await req.json()
|
||||
const parsedBody = parseDriveRequestBody(body)
|
||||
const parsedBody = await readDriveRequestBody(req)
|
||||
action = parsedBody.action
|
||||
const stagingId = parsedBody.stagingId
|
||||
const staged = await readStagedOriginalFiles(stagingId)
|
||||
folderName = staged.folderName
|
||||
parsedFiles = staged.files
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
return uploadErrorResponse(err, 400)
|
||||
}
|
||||
|
||||
if (!acquireUploadLock(folderName)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' },
|
||||
{ status: 409 },
|
||||
)
|
||||
return uploadLockConflictResponse()
|
||||
}
|
||||
|
||||
const basePath = process.env.NEXTCLOUD_BASE_PATH || 'Models'
|
||||
@@ -79,10 +79,7 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err, 'Erreur Nextcloud inconnue')
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Drive echoue: ${message}` },
|
||||
{ status: 500 },
|
||||
)
|
||||
return uploadErrorMessageResponse(`Drive echoue: ${message}`, 500)
|
||||
} finally {
|
||||
releaseUploadLock(folderName)
|
||||
}
|
||||
|
||||
+36
-29
@@ -6,12 +6,26 @@ import { classifyFileChanges } from '@/lib/diff-files'
|
||||
import { getModelFolderPath } from '@/lib/model-paths'
|
||||
import { cleanupStagingUpload, ensurePreparedStagingAssets, readStagedManifest } from '@/lib/upload-staging'
|
||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||
import { parseStagingRequestBody } from '@/lib/upload-request'
|
||||
import {
|
||||
readStagingRequestBody,
|
||||
uploadErrorMessageResponse,
|
||||
uploadErrorResponse,
|
||||
uploadLockConflictResponse,
|
||||
} from '@/lib/upload-request'
|
||||
import { getErrorMessage } from '@/lib/guards'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
async function cleanupCompletedStagingUpload(stagingId: string) {
|
||||
await cleanupStagingUpload(stagingId).catch((err) => {
|
||||
console.warn('[WARN] Git upload -> staging cleanup failed', {
|
||||
stagingId,
|
||||
error: getErrorMessage(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/upload/git
|
||||
* Upload prepared files and push to GitHub via Octokit.
|
||||
@@ -24,20 +38,15 @@ export async function POST(req: NextRequest) {
|
||||
let stagingId: string
|
||||
|
||||
try {
|
||||
const body: unknown = await req.json()
|
||||
stagingId = parseStagingRequestBody(body).stagingId
|
||||
stagingId = (await readStagingRequestBody(req)).stagingId
|
||||
const manifest = await readStagedManifest(stagingId)
|
||||
folderName = manifest.folderName
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
return uploadErrorResponse(err, 400)
|
||||
}
|
||||
|
||||
if (!acquireUploadLock(folderName)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Un upload est deja en cours pour ce dossier. Patientez quelques secondes.' },
|
||||
{ status: 409 },
|
||||
)
|
||||
return uploadLockConflictResponse()
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -45,6 +54,7 @@ export async function POST(req: NextRequest) {
|
||||
filesToPush,
|
||||
modelFilename,
|
||||
compressed,
|
||||
deliveryMode,
|
||||
compressionError,
|
||||
assetSummaries,
|
||||
} = await ensurePreparedStagingAssets(stagingId)
|
||||
@@ -59,12 +69,13 @@ export async function POST(req: NextRequest) {
|
||||
classifyFileChanges(filesToPush, remoteFileMap, folderPath)
|
||||
|
||||
if (changedFilesToPush.length === 0 && deletePaths.length === 0) {
|
||||
await cleanupStagingUpload(stagingId).catch(() => {})
|
||||
await cleanupCompletedStagingUpload(stagingId)
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
folderName,
|
||||
filesCount: 0,
|
||||
compressed,
|
||||
deliveryMode,
|
||||
compressionError: compressionError || undefined,
|
||||
message: 'Aucun fichier modifie — rien a envoyer.',
|
||||
})
|
||||
@@ -79,26 +90,22 @@ export async function POST(req: NextRequest) {
|
||||
deletedFileNames,
|
||||
)
|
||||
|
||||
try {
|
||||
const { commitUrl } = await pushAllToGitHub(changedFilesToPush, deletePaths, commitMessage)
|
||||
await cleanupStagingUpload(stagingId).catch(() => {})
|
||||
const { commitUrl } = await pushAllToGitHub(changedFilesToPush, deletePaths, commitMessage)
|
||||
await cleanupCompletedStagingUpload(stagingId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
folderName,
|
||||
filesCount: changedFilesToPush.length,
|
||||
compressed,
|
||||
compressionError: compressionError || undefined,
|
||||
message: `${changedFilesToPush.length} fichier(s) modifie(s) envoye(s) sur GitHub en un seul commit.`,
|
||||
commitUrl,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err, 'Erreur GitHub inconnue')
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Push GitHub echoue: ${message}` },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
folderName,
|
||||
filesCount: changedFilesToPush.length,
|
||||
compressed,
|
||||
deliveryMode,
|
||||
compressionError: compressionError || undefined,
|
||||
message: `${changedFilesToPush.length} fichier(s) modifie(s) envoye(s) sur GitHub en un seul commit.`,
|
||||
commitUrl,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err, 'Erreur GitHub inconnue')
|
||||
return uploadErrorMessageResponse(`Upload GitHub echoue: ${message}`, 500)
|
||||
} finally {
|
||||
releaseUploadLock(folderName)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { validateUploadSecret } from '@/lib/auth'
|
||||
import { parseMultiUpload } from '@/lib/parse-upload'
|
||||
import { createStagingUpload } from '@/lib/upload-staging'
|
||||
import { getErrorMessage } from '@/lib/guards'
|
||||
import { uploadErrorResponse } from '@/lib/upload-request'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -16,7 +16,6 @@ export async function POST(req: NextRequest) {
|
||||
const staged = await createStagingUpload(parsed.folderName, parsed.files, parsed.gitModelMode)
|
||||
return NextResponse.json({ success: true, ...staged })
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||
return uploadErrorResponse(err, 400)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user