chore: prepare v1.0.0 release
This commit is contained in:
+1
-13
@@ -1,18 +1,10 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// File diff classification — compares local files against a remote file map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { MODEL_EXTENSIONS } from './constants'
|
||||
import type { FileChange, PushFile } from './types'
|
||||
|
||||
export interface DiffResult {
|
||||
/** Map of lowercase filename → change status (for commit message) */
|
||||
interface DiffResult {
|
||||
fileChanges: Map<string, FileChange>
|
||||
/** Files that actually need to be pushed (new or changed) */
|
||||
changedFilesToPush: PushFile[]
|
||||
/** Filenames that were on remote but not in the new upload */
|
||||
deletedFileNames: string[]
|
||||
/** Full paths for deletion on remote */
|
||||
deletePaths: string[]
|
||||
}
|
||||
|
||||
@@ -41,13 +33,10 @@ export function classifyFileChanges(
|
||||
const isModel = MODEL_EXTENSIONS.has(ext)
|
||||
|
||||
if (isModel) {
|
||||
// Model: always re-push since compression makes size comparison unreliable.
|
||||
// Mark as 'unchanged' for the commit message when the folder already exists.
|
||||
const remoteSize = remoteFileMap.get(filename.toLowerCase())
|
||||
fileChanges.set(filename.toLowerCase(), remoteSize === undefined ? 'new' : 'unchanged')
|
||||
changedFilesToPush.push(f)
|
||||
} else {
|
||||
// Texture: compare by size
|
||||
const localSize = Buffer.from(f.contentBase64, 'base64').length
|
||||
const remoteSize = remoteFileMap.get(filename.toLowerCase())
|
||||
|
||||
@@ -63,7 +52,6 @@ export function classifyFileChanges(
|
||||
}
|
||||
}
|
||||
|
||||
// Files on remote not in the new upload → deleted (orphans)
|
||||
const newFileNames = new Set(filesToPush.map((f) => (f.path.split('/').pop() ?? '').toLowerCase()))
|
||||
const deletedFileNames: string[] = []
|
||||
const deletePaths: string[] = []
|
||||
|
||||
+59
-57
@@ -1,14 +1,11 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import { LFS_EXTENSIONS } from './constants'
|
||||
import { isRecord } from './guards'
|
||||
import type { PushFile, RemoteFile } from './types'
|
||||
|
||||
const LFS_BATCH_SIZE = 100
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Octokit helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isHttpError(err: unknown): err is { status: number } {
|
||||
return typeof err === 'object' && err !== null && 'status' in err && typeof (err as Record<string, unknown>).status === 'number'
|
||||
}
|
||||
@@ -33,22 +30,15 @@ function parseRepoUrl(): { owner: string; repo: string } {
|
||||
return { owner: match[1], repo: match[2] }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git LFS helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Check if a file path should be tracked by LFS based on its extension. */
|
||||
function isLfsFile(filePath: string): boolean {
|
||||
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase()
|
||||
return LFS_EXTENSIONS.has(ext)
|
||||
}
|
||||
|
||||
/** Build an LFS pointer file (text content stored in the Git blob). */
|
||||
function buildLfsPointer(sha256: string, size: number): string {
|
||||
return `version https://git-lfs.github.com/spec/v1\noid sha256:${sha256}\nsize ${size}\n`
|
||||
}
|
||||
|
||||
/** Parse an LFS pointer to extract the real file size. Returns null if not a pointer. */
|
||||
function parseLfsPointer(content: string): { oid: string; size: number } | null {
|
||||
if (!content.startsWith('version https://git-lfs.github.com/spec/v1')) return null
|
||||
const sizeMatch = content.match(/^size (\d+)$/m)
|
||||
@@ -81,14 +71,62 @@ interface LfsObject {
|
||||
contentBase64: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload binary objects to the Git LFS server via the Batch API.
|
||||
*
|
||||
* Flow:
|
||||
* 1. POST to the LFS batch endpoint with operation "upload"
|
||||
* 2. For each object that has an "upload" action, PUT the binary content
|
||||
* 3. If the server omits "actions", the object already exists — skip upload
|
||||
*/
|
||||
interface LfsBatchObject {
|
||||
oid: string
|
||||
size: number
|
||||
actions?: {
|
||||
upload?: { href: string; header?: Record<string, string> }
|
||||
verify?: { href: string; header?: Record<string, string> }
|
||||
}
|
||||
error?: { code: number; message: string }
|
||||
}
|
||||
|
||||
function isStringRecord(value: unknown): value is Record<string, string> {
|
||||
return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string')
|
||||
}
|
||||
|
||||
function parseLfsAction(value: unknown) {
|
||||
if (!isRecord(value) || typeof value.href !== 'string') return undefined
|
||||
return {
|
||||
href: value.href,
|
||||
header: isStringRecord(value.header) ? value.header : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function parseLfsBatchObject(value: unknown): LfsBatchObject | null {
|
||||
if (!isRecord(value) || typeof value.oid !== 'string' || typeof value.size !== 'number') return null
|
||||
|
||||
const actions = isRecord(value.actions)
|
||||
? {
|
||||
upload: parseLfsAction(value.actions.upload),
|
||||
verify: parseLfsAction(value.actions.verify),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const error = isRecord(value.error) && typeof value.error.code === 'number' && typeof value.error.message === 'string'
|
||||
? { code: value.error.code, message: value.error.message }
|
||||
: undefined
|
||||
|
||||
return { oid: value.oid, size: value.size, actions, error }
|
||||
}
|
||||
|
||||
function parseLfsBatchResponse(value: unknown): LfsBatchObject[] {
|
||||
if (!isRecord(value) || !Array.isArray(value.objects)) {
|
||||
throw new Error('LFS batch response invalide')
|
||||
}
|
||||
|
||||
const objects: LfsBatchObject[] = []
|
||||
for (const object of value.objects) {
|
||||
const parsed = parseLfsBatchObject(object)
|
||||
if (!parsed) {
|
||||
throw new Error('LFS batch object invalide')
|
||||
}
|
||||
objects.push(parsed)
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
async function uploadToLfs(
|
||||
owner: string,
|
||||
repo: string,
|
||||
@@ -118,7 +156,6 @@ async function uploadToLfsBatch(
|
||||
const token = process.env.GITHUB_TOKEN!
|
||||
const lfsUrl = `https://github.com/${owner}/${repo}.git/info/lfs/objects/batch`
|
||||
|
||||
// 1. Batch request — ask for upload URLs
|
||||
const batchRes = await fetch(lfsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -142,27 +179,15 @@ async function uploadToLfsBatch(
|
||||
throw new Error(`LFS batch request failed (${batchRes.status}): ${text}`)
|
||||
}
|
||||
|
||||
const batchData = (await batchRes.json()) as {
|
||||
objects: Array<{
|
||||
oid: string
|
||||
size: number
|
||||
actions?: {
|
||||
upload?: { href: string; header?: Record<string, string> }
|
||||
verify?: { href: string; header?: Record<string, string> }
|
||||
}
|
||||
error?: { code: number; message: string }
|
||||
}>
|
||||
}
|
||||
const batchObjects = parseLfsBatchResponse(await batchRes.json())
|
||||
|
||||
// 2. Upload each object that has an upload action
|
||||
const objectMap = new Map(objects.map((o) => [o.oid, o]))
|
||||
|
||||
for (const obj of batchData.objects) {
|
||||
for (const obj of batchObjects) {
|
||||
if (obj.error) {
|
||||
throw new Error(`LFS error for ${obj.oid}: ${obj.error.message} (${obj.error.code})`)
|
||||
}
|
||||
|
||||
// No actions = server already has this object, skip
|
||||
if (!obj.actions?.upload) continue
|
||||
|
||||
const local = objectMap.get(obj.oid)
|
||||
@@ -187,7 +212,6 @@ async function uploadToLfsBatch(
|
||||
throw new Error(`LFS upload failed for ${obj.oid} (${uploadRes.status}): ${text}`)
|
||||
}
|
||||
|
||||
// 3. Verify if required
|
||||
if (obj.actions.verify) {
|
||||
const verifyAction = obj.actions.verify
|
||||
const verifyHeaders: Record<string, string> = {
|
||||
@@ -214,10 +238,6 @@ async function uploadToLfsBatch(
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read remote folder contents (with real file sizes for LFS files)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function getRemoteFolder(
|
||||
folderPath: string,
|
||||
): Promise<{ exists: boolean; files: RemoteFile[] }> {
|
||||
@@ -237,16 +257,12 @@ export async function getRemoteFolder(
|
||||
return { exists: false, files: [] }
|
||||
}
|
||||
|
||||
// For LFS-tracked files, the "size" from getContent is the pointer size (~130 bytes),
|
||||
// not the real file size. We need to fetch each LFS pointer to get the real size.
|
||||
const files: RemoteFile[] = await Promise.all(
|
||||
data.map(async (f): Promise<RemoteFile> => {
|
||||
if (!isLfsFile(f.name) || f.size > 1024) {
|
||||
// Not LFS or too large to be a pointer — use size as-is
|
||||
return { name: f.name, size: f.size }
|
||||
}
|
||||
|
||||
// Fetch the blob content to check if it's an LFS pointer
|
||||
try {
|
||||
const { data: fileData } = await octokit.repos.getContent({
|
||||
owner,
|
||||
@@ -279,10 +295,6 @@ export async function getRemoteFolder(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push all files in a single commit (with optional deletions + LFS support)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function pushAllToGitHub(
|
||||
files: PushFile[],
|
||||
deletePaths: string[],
|
||||
@@ -292,7 +304,6 @@ export async function pushAllToGitHub(
|
||||
const { owner, repo } = parseRepoUrl()
|
||||
const branch = process.env.GIT_BRANCH ?? 'main'
|
||||
|
||||
// --- Separate LFS files from regular files ---
|
||||
const lfsFiles: { path: string; contentBase64: string; oid: string; size: number }[] = []
|
||||
const regularFiles: PushFile[] = []
|
||||
|
||||
@@ -306,7 +317,6 @@ export async function pushAllToGitHub(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Upload LFS objects to the LFS server ---
|
||||
if (lfsFiles.length > 0) {
|
||||
await uploadToLfs(
|
||||
owner,
|
||||
@@ -315,7 +325,6 @@ export async function pushAllToGitHub(
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Get latest commit on branch
|
||||
const { data: ref } = await octokit.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
@@ -323,21 +332,18 @@ export async function pushAllToGitHub(
|
||||
})
|
||||
const latestCommitSha = ref.object.sha
|
||||
|
||||
// 2. Get that commit's tree
|
||||
const { data: commit } = await octokit.git.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: latestCommitSha,
|
||||
})
|
||||
|
||||
// 3. Create blobs — LFS files get pointer blobs, regular files get raw blobs
|
||||
const allFiles = [...regularFiles, ...lfsFiles]
|
||||
|
||||
const blobResults = await Promise.all(
|
||||
allFiles.map((f) => {
|
||||
const lfs = lfsFiles.find((lf) => lf.path === f.path)
|
||||
if (lfs) {
|
||||
// Create a blob with the LFS pointer text (NOT the binary content)
|
||||
const pointer = buildLfsPointer(lfs.oid, lfs.size)
|
||||
return octokit.git.createBlob({
|
||||
owner,
|
||||
@@ -346,7 +352,6 @@ export async function pushAllToGitHub(
|
||||
encoding: 'base64',
|
||||
})
|
||||
}
|
||||
// Regular file — push content as-is
|
||||
return octokit.git.createBlob({
|
||||
owner,
|
||||
repo,
|
||||
@@ -356,7 +361,6 @@ export async function pushAllToGitHub(
|
||||
}),
|
||||
)
|
||||
|
||||
// 4. Build tree entries: new/changed files + deletions
|
||||
const newFilePaths = new Set(files.map((f) => f.path))
|
||||
const deleteEntries = deletePaths
|
||||
.filter((p) => !newFilePaths.has(p))
|
||||
@@ -382,7 +386,6 @@ export async function pushAllToGitHub(
|
||||
],
|
||||
})
|
||||
|
||||
// 5. Create a single commit
|
||||
const { data: newCommit } = await octokit.git.createCommit({
|
||||
owner,
|
||||
repo,
|
||||
@@ -391,7 +394,6 @@ export async function pushAllToGitHub(
|
||||
parents: [latestCommitSha],
|
||||
})
|
||||
|
||||
// 6. Update branch ref
|
||||
await octokit.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown, fallback = 'Erreur inconnue') {
|
||||
return error instanceof Error ? error.message : fallback
|
||||
}
|
||||
+1
-32
@@ -1,11 +1,5 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nextcloud WebDAV client
|
||||
// Uses native fetch — no npm package needed.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MAX_VERSIONS = 1000
|
||||
|
||||
// Lazy-cached config to avoid recomputing on every request
|
||||
let cachedConfig: { davBase: string; auth: string } | null = null
|
||||
|
||||
function getConfig() {
|
||||
@@ -19,7 +13,6 @@ function getConfig() {
|
||||
throw new Error('Nextcloud non configure (NEXTCLOUD_URL, NEXTCLOUD_SHARE_TOKEN)')
|
||||
}
|
||||
|
||||
// Public share WebDAV: https://cloud.example.com/public.php/webdav/
|
||||
const davBase = `${url.replace(/\/+$/, '')}/public.php/webdav`
|
||||
const auth = 'Basic ' + Buffer.from(`${token}:${password}`).toString('base64')
|
||||
|
||||
@@ -32,10 +25,6 @@ function davUrl(davBase: string, path: string): string {
|
||||
return `${davBase}/${clean}`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Low-level WebDAV helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function davRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
@@ -57,12 +46,7 @@ async function davRequest(
|
||||
return res
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Check if a folder exists on the Nextcloud instance. */
|
||||
export async function folderExists(path: string): Promise<boolean> {
|
||||
async function folderExists(path: string): Promise<boolean> {
|
||||
const res = await davRequest('PROPFIND', path + '/', null, { Depth: '0' })
|
||||
|
||||
if (res.status === 404) return false
|
||||
@@ -72,10 +56,6 @@ export async function folderExists(path: string): Promise<boolean> {
|
||||
throw new Error(`PROPFIND ${path} failed (${res.status}): ${text.slice(0, 200)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a folder and all parent segments if they don't exist.
|
||||
* Like `mkdir -p`. Attempts MKCOL directly and handles 405 (already exists).
|
||||
*/
|
||||
export async function mkdirRecursive(path: string): Promise<void> {
|
||||
const segments = path.replace(/^\/+/, '').replace(/\/+$/, '').split('/')
|
||||
let current = ''
|
||||
@@ -84,14 +64,12 @@ export async function mkdirRecursive(path: string): Promise<void> {
|
||||
current += '/' + seg
|
||||
const res = await davRequest('MKCOL', current + '/')
|
||||
if (res.status !== 201 && res.status !== 405) {
|
||||
// 201 = created, 405 = already exists — both are fine
|
||||
const text = await res.text().catch(() => '')
|
||||
throw new Error(`MKCOL ${current} failed (${res.status}): ${text.slice(0, 200)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Upload a file (overwrite if exists). */
|
||||
export async function uploadFile(path: string, content: Buffer): Promise<void> {
|
||||
const res = await davRequest('PUT', path, content, {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
@@ -104,7 +82,6 @@ export async function uploadFile(path: string, content: Buffer): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Move (rename) a folder or file. */
|
||||
export async function moveFolder(from: string, to: string): Promise<void> {
|
||||
const { davBase } = getConfig()
|
||||
const destination = davUrl(davBase, to) + '/'
|
||||
@@ -120,14 +97,6 @@ export async function moveFolder(from: string, to: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// High-level: find next available version folder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find the next available Vx folder for archiving.
|
||||
* E.g. if V1/coffeetest exists but V2/coffeetest doesn't, returns "V2".
|
||||
*/
|
||||
export async function findNextVersion(
|
||||
basePath: string,
|
||||
folderName: string,
|
||||
|
||||
+2
-21
@@ -4,19 +4,11 @@ import { sanitizeFilename } from './sanitize'
|
||||
import { ALL_ALLOWED_EXTENSIONS, MODEL_EXTENSIONS, MAX_FILE_SIZE } from './constants'
|
||||
import type { ParsedFile } from './types'
|
||||
|
||||
export interface ParsedUpload {
|
||||
interface ParsedUpload {
|
||||
folderName: string
|
||||
files: ParsedFile[]
|
||||
/** Any extra string fields from the FormData (e.g. "action") */
|
||||
extra: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a multi-file FormData upload request.
|
||||
* Validates file extensions, file sizes, and returns parsed files.
|
||||
* Extra string fields (beyond folderName, files, fileTypes, textureNames)
|
||||
* are returned in `extra`.
|
||||
*/
|
||||
export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload> {
|
||||
const formData = await req.formData()
|
||||
const folderValue = formData.get('folderName')
|
||||
@@ -27,16 +19,6 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
||||
const fileTypes = formData.getAll('fileTypes').filter((value): value is string => typeof value === 'string')
|
||||
const textureNames = formData.getAll('textureNames').filter((value): value is string => typeof value === 'string')
|
||||
|
||||
// Collect extra string fields
|
||||
const knownKeys = new Set(['folderName', 'files', 'fileTypes', 'textureNames'])
|
||||
const extra: Record<string, string> = {}
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (!knownKeys.has(key) && typeof value === 'string') {
|
||||
extra[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime validation: ensure entries are actual File objects
|
||||
const fileEntries: File[] = []
|
||||
for (const entry of rawFiles) {
|
||||
if (!(entry instanceof File)) {
|
||||
@@ -56,7 +38,6 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
||||
const file = fileEntries[i]
|
||||
if (!file || file.size === 0) continue
|
||||
|
||||
// File size limit
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
throw new Error(
|
||||
`Fichier "${file.name}" trop volumineux (${(file.size / 1024 / 1024).toFixed(1)} MB). Maximum: ${MAX_FILE_SIZE / 1024 / 1024} MB.`,
|
||||
@@ -100,5 +81,5 @@ export async function parseMultiUpload(req: NextRequest): Promise<ParsedUpload>
|
||||
throw new Error('Un seul fichier model.gltf est autorise')
|
||||
}
|
||||
|
||||
return { folderName: safeFolderName, files: parsed, extra }
|
||||
return { folderName: safeFolderName, files: parsed }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { extname } from 'path'
|
||||
import sharp from 'sharp'
|
||||
import { getErrorMessage } from './guards'
|
||||
|
||||
interface TextureCompressionResult {
|
||||
buffer: Buffer
|
||||
@@ -35,7 +36,7 @@ export async function compressTextureBuffer(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const message = getErrorMessage(err, String(err))
|
||||
return {
|
||||
buffer,
|
||||
compressed: false,
|
||||
|
||||
+3
-42
@@ -1,7 +1,4 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client-side API helpers for upload operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { isRecord } from './guards'
|
||||
import type { FolderEntry } from './client-types'
|
||||
import type { FileDiff } from './types'
|
||||
|
||||
@@ -10,16 +7,12 @@ export interface CheckResult {
|
||||
diffs: FileDiff[]
|
||||
}
|
||||
|
||||
export interface StageResult {
|
||||
interface StageResult {
|
||||
stagingId: string
|
||||
folderName: string
|
||||
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
|
||||
}
|
||||
@@ -30,23 +23,10 @@ function isFileDiff(value: unknown): value is FileDiff {
|
||||
&& (value.status === 'new' || value.status === 'changed' || value.status === 'deleted')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared FormData builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildUploadFormData(
|
||||
folder: FolderEntry,
|
||||
extra?: Record<string, string>,
|
||||
): FormData {
|
||||
function buildUploadFormData(folder: FolderEntry): FormData {
|
||||
const formData = new FormData()
|
||||
formData.append('folderName', folder.folderName)
|
||||
|
||||
if (extra) {
|
||||
for (const [key, value] of Object.entries(extra)) {
|
||||
formData.append(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
formData.append('files', folder.modelFile)
|
||||
formData.append('fileTypes', 'model')
|
||||
formData.append('textureNames', '')
|
||||
@@ -60,14 +40,6 @@ function buildUploadFormData(
|
||||
return formData
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check folder diffs against remote (GitHub)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check whether a folder already exists on the remote repo and compute diffs.
|
||||
* Throws on auth/network errors so callers can surface them to the user.
|
||||
*/
|
||||
export async function checkFolderDiffs(
|
||||
stagingId: string,
|
||||
secret: string,
|
||||
@@ -85,7 +57,6 @@ export async function checkFolderDiffs(
|
||||
|
||||
const data: unknown = await res.json()
|
||||
|
||||
// Surface auth/server errors to the caller
|
||||
if (!res.ok) {
|
||||
throw new Error(getApiError(data, `Erreur serveur (${res.status})`))
|
||||
}
|
||||
@@ -129,11 +100,6 @@ export async function stageUpload(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Upload original files to Nextcloud Drive
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Upload original files to Nextcloud Drive. */
|
||||
export async function uploadDrive(
|
||||
stagingId: string,
|
||||
secret: string,
|
||||
@@ -163,11 +129,6 @@ export async function uploadDrive(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Upload files to GitHub
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Upload files to GitHub. */
|
||||
export async function uploadGit(
|
||||
stagingId: string,
|
||||
secret: string,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export type DriveAction = 'new' | 'replace'
|
||||
import { isRecord } from './guards'
|
||||
|
||||
type DriveAction = 'new' | 'replace'
|
||||
|
||||
interface StagingRequestBody {
|
||||
stagingId: string
|
||||
@@ -8,10 +10,6 @@ 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')
|
||||
|
||||
@@ -65,7 +65,7 @@ async function writeManifest(manifest: StagingManifest) {
|
||||
await writeFile(getManifestPath(manifest.stagingId), JSON.stringify(manifest, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
export async function cleanupExpiredStagingUploads() {
|
||||
async function cleanupExpiredStagingUploads() {
|
||||
if (!existsSync(STAGING_ROOT)) return
|
||||
|
||||
const entries = await readdir(STAGING_ROOT, { withFileTypes: true })
|
||||
|
||||
+18
-8
@@ -1,9 +1,6 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client-side folder validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { ASSET_EXTENSIONS, TEXTURE_EXTENSIONS } from '@/lib/constants'
|
||||
import { formatAssetFamilies, getAssetFamily, getForbiddenAssetFamilyAlias } from '@/lib/asset-naming'
|
||||
import { getErrorMessage, isRecord } from '@/lib/guards'
|
||||
import type { TextureFile } from '@/lib/client-types'
|
||||
|
||||
const SUPPORT_FILE_EXT_ARRAY = [...TEXTURE_EXTENSIONS, ...ASSET_EXTENSIONS]
|
||||
@@ -22,7 +19,9 @@ export type ValidationResult =
|
||||
| { ok: false; errors: string[] }
|
||||
|
||||
function isGltfJson(value: unknown): value is GltfJson {
|
||||
return typeof value === 'object' && value !== null
|
||||
if (!isRecord(value)) return false
|
||||
if (value.buffers === undefined) return true
|
||||
return Array.isArray(value.buffers) && value.buffers.every(isRecord)
|
||||
}
|
||||
|
||||
function getReferencedBufferNames(gltf: GltfJson) {
|
||||
@@ -83,10 +82,12 @@ async function getGltfWarnings(model: File, supportFiles: File[]) {
|
||||
try {
|
||||
parsed = JSON.parse(await model.text())
|
||||
} catch {
|
||||
return warnings
|
||||
throw new Error('model.gltf contient un JSON invalide')
|
||||
}
|
||||
|
||||
if (!isGltfJson(parsed)) return warnings
|
||||
if (!isGltfJson(parsed)) {
|
||||
throw new Error('model.gltf a une structure invalide')
|
||||
}
|
||||
|
||||
const supportFilenames = new Set(supportFiles.map((file) => file.name.toLowerCase()))
|
||||
const binFiles = supportFiles.filter((file) => file.name.toLowerCase().endsWith('.bin'))
|
||||
@@ -144,7 +145,16 @@ export async function validateFolder(files: File[]): Promise<ValidationResult> {
|
||||
return { ok: false, errors }
|
||||
}
|
||||
|
||||
const warnings = await getGltfWarnings(modelFiles[0], supportFiles)
|
||||
let warnings: string[] = []
|
||||
try {
|
||||
warnings = await getGltfWarnings(modelFiles[0], supportFiles)
|
||||
} catch (err) {
|
||||
errors.push(getErrorMessage(err, 'model.gltf invalide'))
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { ok: false, errors }
|
||||
}
|
||||
|
||||
return { ok: true, model: modelFiles[0], textures, warnings }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user