fix: harden upload resilience and contracts
This commit is contained in:
+93
-45
@@ -1,16 +1,46 @@
|
||||
import { isRecord } from './guards'
|
||||
import { getErrorMessage, isRecord } from './guards'
|
||||
import type { FolderEntry } from './client-types'
|
||||
import type { DriveAction, FileDiff, GitModelMode, StagingUploadResult } from './types'
|
||||
import type {
|
||||
CheckUploadResult,
|
||||
DriveAction,
|
||||
DriveUploadResult,
|
||||
FileDiff,
|
||||
GitModelMode,
|
||||
GitUploadResult,
|
||||
StagingUploadResult,
|
||||
} from './types'
|
||||
|
||||
export interface CheckResult {
|
||||
exists: boolean
|
||||
diffs: FileDiff[]
|
||||
interface CompressionWarningPayload {
|
||||
compressionError?: unknown
|
||||
}
|
||||
|
||||
interface SuccessfulUploadData extends CompressionWarningPayload {
|
||||
success: true
|
||||
exists?: unknown
|
||||
diffs?: unknown
|
||||
stagingId?: unknown
|
||||
folderName?: unknown
|
||||
filesCount?: unknown
|
||||
}
|
||||
|
||||
type UploadJsonBody =
|
||||
| { stagingId: string }
|
||||
| { stagingId: string; action: DriveAction }
|
||||
|
||||
function getApiError(data: unknown, fallback: string) {
|
||||
return isRecord(data) && typeof data.error === 'string' ? data.error : fallback
|
||||
}
|
||||
|
||||
function getClientRequestError(err: unknown, label: string) {
|
||||
return `${label}: ${getErrorMessage(err)}`
|
||||
}
|
||||
|
||||
function getCompressionWarning(data: CompressionWarningPayload) {
|
||||
if (typeof data.compressionError !== 'string') return undefined
|
||||
|
||||
return `Compression GLB impossible. Le modele a ete prepare en GLTF separe. Detail : ${data.compressionError}`
|
||||
}
|
||||
|
||||
function getUploadJsonHeaders(secret: string) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -18,10 +48,35 @@ function getUploadJsonHeaders(secret: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function postUploadJson(
|
||||
endpoint: string,
|
||||
secret: string,
|
||||
body: UploadJsonBody,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: getUploadJsonHeaders(secret),
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
})
|
||||
|
||||
const data: unknown = await res.json()
|
||||
return { res, data }
|
||||
}
|
||||
|
||||
function isSuccessfulUploadData(data: unknown): data is SuccessfulUploadData {
|
||||
return isRecord(data) && data.success === true
|
||||
}
|
||||
|
||||
function isAbortError(err: unknown) {
|
||||
return err instanceof DOMException && err.name === 'AbortError'
|
||||
}
|
||||
|
||||
function getNetworkUploadError(err: unknown, label: string) {
|
||||
return isAbortError(err) ? 'Upload annule' : getClientRequestError(err, label)
|
||||
}
|
||||
|
||||
function isFileDiff(value: unknown): value is FileDiff {
|
||||
return isRecord(value)
|
||||
&& typeof value.name === 'string'
|
||||
@@ -50,27 +105,34 @@ export async function checkFolderDiffs(
|
||||
stagingId: string,
|
||||
secret: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CheckResult> {
|
||||
const res = await fetch('/api/upload/check', {
|
||||
method: 'POST',
|
||||
headers: getUploadJsonHeaders(secret),
|
||||
body: JSON.stringify({ stagingId }),
|
||||
signal,
|
||||
})
|
||||
|
||||
const data: unknown = await res.json()
|
||||
): Promise<CheckUploadResult> {
|
||||
const { res, data } = await postUploadJson('/api/upload/check', secret, { stagingId }, signal)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(getApiError(data, `Erreur serveur (${res.status})`))
|
||||
}
|
||||
|
||||
if (!isRecord(data) || data.success !== true || data.exists !== true) {
|
||||
return { exists: false, diffs: [] }
|
||||
if (!isSuccessfulUploadData(data)) {
|
||||
throw new Error('Reponse serveur invalide')
|
||||
}
|
||||
|
||||
const warning = getCompressionWarning(data)
|
||||
|
||||
if (data.exists !== true) {
|
||||
return {
|
||||
exists: false,
|
||||
diffs: [],
|
||||
warning,
|
||||
}
|
||||
}
|
||||
|
||||
const diffs = Array.isArray(data.diffs) ? data.diffs.filter(isFileDiff) : []
|
||||
|
||||
return { exists: true, diffs }
|
||||
return {
|
||||
exists: true,
|
||||
diffs,
|
||||
warning,
|
||||
}
|
||||
}
|
||||
|
||||
export async function stageUpload(
|
||||
@@ -89,7 +151,7 @@ export async function stageUpload(
|
||||
|
||||
const data: unknown = await res.json()
|
||||
|
||||
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||
if (!res.ok || !isSuccessfulUploadData(data)) {
|
||||
throw new Error(getApiError(data, `Erreur serveur (${res.status})`))
|
||||
}
|
||||
|
||||
@@ -109,24 +171,15 @@ export async function uploadDrive(
|
||||
secret: string,
|
||||
action: DriveAction,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
): Promise<DriveUploadResult> {
|
||||
try {
|
||||
const res = await fetch('/api/upload/drive', {
|
||||
method: 'POST',
|
||||
headers: getUploadJsonHeaders(secret),
|
||||
body: JSON.stringify({ stagingId, action }),
|
||||
signal,
|
||||
})
|
||||
const data: unknown = await res.json()
|
||||
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||
const { res, data } = await postUploadJson('/api/upload/drive', secret, { stagingId, action }, signal)
|
||||
if (!res.ok || !isSuccessfulUploadData(data)) {
|
||||
return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) }
|
||||
}
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
return { success: false, error: 'Upload annule' }
|
||||
}
|
||||
return { success: false, error: 'Erreur reseau (Drive)' }
|
||||
return { success: false, error: getNetworkUploadError(err, 'Erreur Drive') }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,30 +188,25 @@ export async function uploadGit(
|
||||
secret: string,
|
||||
onProgress: (pct: number) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ success: boolean; filename?: string; error?: string }> {
|
||||
): Promise<GitUploadResult> {
|
||||
onProgress(10)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload/git', {
|
||||
method: 'POST',
|
||||
headers: getUploadJsonHeaders(secret),
|
||||
body: JSON.stringify({ stagingId }),
|
||||
signal,
|
||||
})
|
||||
const { res, data } = await postUploadJson('/api/upload/git', secret, { stagingId }, signal)
|
||||
|
||||
onProgress(80)
|
||||
const data: unknown = await res.json()
|
||||
|
||||
if (!res.ok || !isRecord(data) || data.success !== true) {
|
||||
if (!res.ok || !isSuccessfulUploadData(data)) {
|
||||
return { success: false, error: getApiError(data, `Erreur serveur (${res.status})`) }
|
||||
}
|
||||
|
||||
onProgress(100)
|
||||
return { success: true, filename: typeof data.folderName === 'string' ? data.folderName : undefined }
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
return { success: false, error: 'Upload annule' }
|
||||
return {
|
||||
success: true,
|
||||
filename: typeof data.folderName === 'string' ? data.folderName : undefined,
|
||||
warning: getCompressionWarning(data),
|
||||
}
|
||||
return { success: false, error: 'Erreur reseau' }
|
||||
} catch (err) {
|
||||
return { success: false, error: getNetworkUploadError(err, 'Erreur GitHub') }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user