Files
upload-gltf/lib/upload-api.ts
T
2026-05-12 23:49:30 +02:00

213 lines
5.5 KiB
TypeScript

import { getErrorMessage, isRecord } from './guards'
import type { FolderEntry } from './client-types'
import type {
CheckUploadResult,
DriveAction,
DriveUploadResult,
FileDiff,
GitModelMode,
GitUploadResult,
StagingUploadResult,
} from './types'
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',
'x-upload-secret': secret.trim(),
}
}
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'
&& (value.status === 'new' || value.status === 'changed' || value.status === 'deleted')
}
function buildUploadFormData(folder: FolderEntry, gitModelMode: GitModelMode): FormData {
const formData = new FormData()
formData.append('folderName', folder.folderName)
formData.append('gitModelMode', gitModelMode)
formData.append('files', folder.modelFile)
formData.append('fileTypes', 'model')
formData.append('textureNames', '')
for (const tex of folder.textures) {
formData.append('files', tex.file)
formData.append('fileTypes', 'texture')
formData.append('textureNames', tex.name)
}
return formData
}
export async function checkFolderDiffs(
stagingId: string,
secret: string,
signal?: AbortSignal,
): 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 (!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,
warning,
}
}
export async function stageUpload(
folder: FolderEntry,
gitModelMode: GitModelMode,
secret: string,
signal?: AbortSignal,
): Promise<StagingUploadResult> {
const formData = buildUploadFormData(folder, gitModelMode)
const res = await fetch('/api/upload/stage', {
method: 'POST',
headers: { 'x-upload-secret': secret.trim() },
body: formData,
signal,
})
const data: unknown = await res.json()
if (!res.ok || !isSuccessfulUploadData(data)) {
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 {
stagingId: data.stagingId,
folderName: data.folderName,
filesCount: data.filesCount,
}
}
export async function uploadDrive(
stagingId: string,
secret: string,
action: DriveAction,
signal?: AbortSignal,
): Promise<DriveUploadResult> {
try {
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) {
return { success: false, error: getNetworkUploadError(err, 'Erreur Drive') }
}
}
export async function uploadGit(
stagingId: string,
secret: string,
onProgress: (pct: number) => void,
signal?: AbortSignal,
): Promise<GitUploadResult> {
onProgress(10)
try {
const { res, data } = await postUploadJson('/api/upload/git', secret, { stagingId }, signal)
onProgress(80)
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,
warning: getCompressionWarning(data),
}
} catch (err) {
return { success: false, error: getNetworkUploadError(err, 'Erreur GitHub') }
}
}