refactor: strengthen upload boundary types
This commit is contained in:
@@ -4,6 +4,7 @@ import { getRemoteFolder } from '@/lib/github'
|
|||||||
import { classifyFileChanges } from '@/lib/diff-files'
|
import { classifyFileChanges } from '@/lib/diff-files'
|
||||||
import { getModelFolderPath } from '@/lib/model-paths'
|
import { getModelFolderPath } from '@/lib/model-paths'
|
||||||
import { ensurePreparedStagingAssets } from '@/lib/upload-staging'
|
import { ensurePreparedStagingAssets } from '@/lib/upload-staging'
|
||||||
|
import { parseStagingRequestBody } from '@/lib/upload-request'
|
||||||
|
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,11 +20,8 @@ export async function POST(req: NextRequest) {
|
|||||||
let stagingId: string
|
let stagingId: string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body: unknown = await req.json()
|
||||||
stagingId = body.stagingId
|
stagingId = parseStagingRequestBody(body).stagingId
|
||||||
if (!stagingId || typeof stagingId !== 'string') {
|
|
||||||
throw new Error('stagingId manquant')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
const message = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||||
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
return NextResponse.json({ success: false, error: message }, { status: 400 })
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
findNextVersion,
|
findNextVersion,
|
||||||
} from '@/lib/nextcloud'
|
} from '@/lib/nextcloud'
|
||||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||||
|
import { parseDriveRequestBody } from '@/lib/upload-request'
|
||||||
|
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -45,15 +46,13 @@ export async function POST(req: NextRequest) {
|
|||||||
// --- Parse staging request ---
|
// --- Parse staging request ---
|
||||||
let folderName: string
|
let folderName: string
|
||||||
let parsedFiles: Awaited<ReturnType<typeof readStagedOriginalFiles>>['files']
|
let parsedFiles: Awaited<ReturnType<typeof readStagedOriginalFiles>>['files']
|
||||||
let action: string
|
let action: 'new' | 'replace'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body: unknown = await req.json()
|
||||||
const stagingId = body.stagingId
|
const parsedBody = parseDriveRequestBody(body)
|
||||||
if (!stagingId || typeof stagingId !== 'string') {
|
action = parsedBody.action
|
||||||
throw new Error('stagingId manquant')
|
const stagingId = parsedBody.stagingId
|
||||||
}
|
|
||||||
action = typeof body.action === 'string' ? body.action.trim() || 'new' : 'new'
|
|
||||||
const staged = await readStagedOriginalFiles(stagingId)
|
const staged = await readStagedOriginalFiles(stagingId)
|
||||||
folderName = staged.folderName
|
folderName = staged.folderName
|
||||||
parsedFiles = staged.files
|
parsedFiles = staged.files
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { classifyFileChanges } from '@/lib/diff-files'
|
|||||||
import { getModelFolderPath } from '@/lib/model-paths'
|
import { getModelFolderPath } from '@/lib/model-paths'
|
||||||
import { cleanupStagingUpload, ensurePreparedStagingAssets, readStagedManifest } from '@/lib/upload-staging'
|
import { cleanupStagingUpload, ensurePreparedStagingAssets, readStagedManifest } from '@/lib/upload-staging'
|
||||||
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
import { acquireUploadLock, releaseUploadLock } from '@/lib/upload-lock'
|
||||||
|
import { parseStagingRequestBody } from '@/lib/upload-request'
|
||||||
|
|
||||||
export const runtime = 'nodejs'
|
export const runtime = 'nodejs'
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -23,11 +24,8 @@ export async function POST(req: NextRequest) {
|
|||||||
let stagingId: string
|
let stagingId: string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await req.json()
|
const body: unknown = await req.json()
|
||||||
stagingId = body.stagingId
|
stagingId = parseStagingRequestBody(body).stagingId
|
||||||
if (!stagingId || typeof stagingId !== 'string') {
|
|
||||||
throw new Error('stagingId manquant')
|
|
||||||
}
|
|
||||||
const manifest = await readStagedManifest(stagingId)
|
const manifest = await readStagedManifest(stagingId)
|
||||||
folderName = manifest.folderName
|
folderName = manifest.folderName
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -28,12 +28,22 @@ function readDirectoryEntries(reader: FileSystemDirectoryReader) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFileEntry(entry: FileSystemEntry): entry is FileSystemFileEntry {
|
||||||
|
return entry.isFile
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectoryEntry(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
|
||||||
|
return entry.isDirectory
|
||||||
|
}
|
||||||
|
|
||||||
async function collectEntryFiles(entry: FileSystemEntry): Promise<File[]> {
|
async function collectEntryFiles(entry: FileSystemEntry): Promise<File[]> {
|
||||||
if (entry.isFile) {
|
if (isFileEntry(entry)) {
|
||||||
return [await readDroppedFile(entry as FileSystemFileEntry)]
|
return [await readDroppedFile(entry)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = (entry as FileSystemDirectoryEntry).createReader()
|
if (!isDirectoryEntry(entry)) return []
|
||||||
|
|
||||||
|
const reader = entry.createReader()
|
||||||
const files: File[] = []
|
const files: File[] = []
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -109,7 +119,8 @@ export default function FolderDropzone({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
|
const relatedTarget = e.relatedTarget
|
||||||
|
if (!(relatedTarget instanceof Node) || !e.currentTarget.contains(relatedTarget)) {
|
||||||
setIsDragActive(false)
|
setIsDragActive(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +138,7 @@ export default function FolderDropzone({
|
|||||||
.map((item) => item.webkitGetAsEntry?.())
|
.map((item) => item.webkitGetAsEntry?.())
|
||||||
.filter((entry): entry is FileSystemEntry => entry !== null)
|
.filter((entry): entry is FileSystemEntry => entry !== null)
|
||||||
|
|
||||||
const directoryEntries = rootEntries.filter((entry) => entry.isDirectory)
|
const directoryEntries = rootEntries.filter(isDirectoryEntry)
|
||||||
|
|
||||||
if (directoryEntries.length > 0) {
|
if (directoryEntries.length > 0) {
|
||||||
if (directoryEntries.length > 1) {
|
if (directoryEntries.length > 1) {
|
||||||
@@ -135,7 +146,7 @@ export default function FolderDropzone({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootEntry = directoryEntries[0] as FileSystemDirectoryEntry
|
const rootEntry = directoryEntries[0]
|
||||||
const files = await collectEntryFiles(rootEntry)
|
const files = await collectEntryFiles(rootEntry)
|
||||||
processFiles(files, rootEntry.name)
|
processFiles(files, rootEntry.name)
|
||||||
return
|
return
|
||||||
|
|||||||
+4
-3
@@ -19,12 +19,13 @@ export interface ParsedUpload {
|
|||||||
*/
|
*/
|
||||||
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
|
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
|
||||||
const formData = await req.formData()
|
const formData = await req.formData()
|
||||||
const folderName = (formData.get('folderName') as string | null)?.trim() || 'assets'
|
const folderValue = formData.get('folderName')
|
||||||
|
const folderName = typeof folderValue === 'string' ? folderValue.trim() || 'assets' : 'assets'
|
||||||
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
const safeFolderName = sanitizeFilename(folderName).replace(/[^a-zA-Z0-9-_]/g, '-')
|
||||||
|
|
||||||
const rawFiles = formData.getAll('files')
|
const rawFiles = formData.getAll('files')
|
||||||
const fileTypes = formData.getAll('fileTypes') as string[]
|
const fileTypes = formData.getAll('fileTypes').filter((value): value is string => typeof value === 'string')
|
||||||
const textureNames = formData.getAll('textureNames') as string[]
|
const textureNames = formData.getAll('textureNames').filter((value): value is string => typeof value === 'string')
|
||||||
|
|
||||||
// Collect extra string fields
|
// Collect extra string fields
|
||||||
const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames'])
|
const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames'])
|
||||||
|
|||||||
+35
-13
@@ -16,6 +16,20 @@ export interface StageResult {
|
|||||||
filesCount: number
|
filesCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiError(data: unknown, fallback: string) {
|
||||||
|
return isRecord(data) && typeof data.error === 'string' ? data.error : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileDiff(value: unknown): value is FileDiff {
|
||||||
|
return isRecord(value)
|
||||||
|
&& typeof value.name === 'string'
|
||||||
|
&& (value.status === 'new' || value.status === 'changed' || value.status === 'deleted')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shared FormData builder
|
// Shared FormData builder
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -69,18 +83,20 @@ export async function checkFolderDiffs(
|
|||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
const data: unknown = await res.json()
|
||||||
|
|
||||||
// Surface auth/server errors to the caller
|
// Surface auth/server errors to the caller
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(data.error || `Erreur serveur (${res.status})`)
|
throw new Error(getApiError(data, `Erreur serveur (${res.status})`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.success || !data.exists) {
|
if (!isRecord(data) || data.success !== true || data.exists !== true) {
|
||||||
return { exists: false, diffs: [] }
|
return { exists: false, diffs: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { exists: true, diffs: (data.diffs || []) as FileDiff[] }
|
const diffs = Array.isArray(data.diffs) ? data.diffs.filter(isFileDiff) : []
|
||||||
|
|
||||||
|
return { exists: true, diffs }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stageUpload(
|
export async function stageUpload(
|
||||||
@@ -96,10 +112,14 @@ export async function stageUpload(
|
|||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
const data: unknown = await res.json()
|
||||||
|
|
||||||
if (!res.ok || !data.success) {
|
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||||
throw new Error(data.error || `Erreur serveur (${res.status})`)
|
throw new Error(getApiError(data, `Erreur serveur (${res.status})`))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.stagingId !== 'string' || typeof data.folderName !== 'string' || typeof data.filesCount !== 'number') {
|
||||||
|
throw new Error('Reponse serveur invalide')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -130,8 +150,10 @@ export async function uploadDrive(
|
|||||||
body: JSON.stringify({ stagingId, action }),
|
body: JSON.stringify({ stagingId, action }),
|
||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data: unknown = await res.json()
|
||||||
if (!data.success) return { success: false, error: data.error }
|
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||||
|
return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) }
|
||||||
|
}
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
@@ -166,14 +188,14 @@ export async function uploadGit(
|
|||||||
})
|
})
|
||||||
|
|
||||||
onProgress(80)
|
onProgress(80)
|
||||||
const data = await res.json()
|
const data: unknown = await res.json()
|
||||||
|
|
||||||
if (!data.success) {
|
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||||
return { success: false, error: data.error }
|
return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(100)
|
onProgress(100)
|
||||||
return { success: true, filename: data.folderName }
|
return { success: true, filename: typeof data.folderName === 'string' ? data.folderName : undefined }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
return { success: false, error: 'Upload annule' }
|
return { success: false, error: 'Upload annule' }
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export type DriveAction = 'new' | 'replace'
|
||||||
|
|
||||||
|
interface StagingRequestBody {
|
||||||
|
stagingId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DriveRequestBody extends StagingRequestBody {
|
||||||
|
action: DriveAction
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseStagingRequestBody(value: unknown): StagingRequestBody {
|
||||||
|
if (!isRecord(value) || typeof value.stagingId !== 'string' || value.stagingId.trim() === '') {
|
||||||
|
throw new Error('stagingId manquant')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stagingId: value.stagingId }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDriveRequestBody(value: unknown): DriveRequestBody {
|
||||||
|
const { stagingId } = parseStagingRequestBody(value)
|
||||||
|
const action = isRecord(value) && value.action === 'replace' ? 'replace' : 'new'
|
||||||
|
|
||||||
|
return { stagingId, action }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user