275 lines
7.1 KiB
TypeScript
275 lines
7.1 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 }
|
|
|
|
const RESPONSE_PREVIEW_MAX_LENGTH = 160
|
|
|
|
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(),
|
|
}
|
|
}
|
|
|
|
function getResponsePreview(body: string) {
|
|
return body
|
|
.replace(/<[^>]*>/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, RESPONSE_PREVIEW_MAX_LENGTH)
|
|
}
|
|
|
|
function getUnexpectedJsonResponseError(res: Response, body: string) {
|
|
const contentType = res.headers.get('content-type') || 'type inconnu'
|
|
const preview = getResponsePreview(body)
|
|
const detail = preview ? ` Detail : ${preview}` : ''
|
|
|
|
if (res.status === 413) {
|
|
return `Upload trop volumineux ou bloque par le proxy (${res.status}). Verifiez la limite de taille Coolify/Nginx.${detail}`
|
|
}
|
|
|
|
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
return `API upload indisponible (${res.status}). Le serveur a probablement redemarre ou plante pendant le traitement.${detail}`
|
|
}
|
|
|
|
return `Reponse serveur inattendue (${res.status}, ${contentType}).${detail}`
|
|
}
|
|
|
|
async function readUploadJson(res: Response): Promise<unknown> {
|
|
const body = await res.text()
|
|
|
|
if (body.trim() === '') {
|
|
return {}
|
|
}
|
|
|
|
const contentType = res.headers.get('content-type') || ''
|
|
|
|
if (!contentType.toLowerCase().includes('json')) {
|
|
throw new Error(getUnexpectedJsonResponseError(res, body))
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(body)
|
|
} catch {
|
|
throw new Error(getUnexpectedJsonResponseError(res, body))
|
|
}
|
|
}
|
|
|
|
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 = await readUploadJson(res)
|
|
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 parseFileDiffs(value: unknown): FileDiff[] {
|
|
if (!Array.isArray(value)) {
|
|
throw new Error('Reponse serveur invalide')
|
|
}
|
|
|
|
const diffs: FileDiff[] = []
|
|
|
|
for (const diff of value) {
|
|
if (!isFileDiff(diff)) {
|
|
throw new Error('Reponse serveur invalide')
|
|
}
|
|
|
|
diffs.push(diff)
|
|
}
|
|
|
|
return diffs
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
return {
|
|
exists: true,
|
|
diffs: parseFileDiffs(data.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 = await readUploadJson(res)
|
|
|
|
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 Git') }
|
|
}
|
|
}
|