80 lines
3.6 KiB
JavaScript
80 lines
3.6 KiB
JavaScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, normalize, sep } from 'node:path'
|
|
|
|
const MAX_JSON_BYTES = 64 * 1024 * 1024
|
|
const MAX_IMAGE_BYTES = 32 * 1024 * 1024
|
|
const MAX_TOTAL_IMAGE_BYTES = 128 * 1024 * 1024
|
|
|
|
export function safeProjectName(value) {
|
|
const name = String(value || 'SpineParticle').trim().replace(/[\\/:*?"<>|\x00-\x1f]+/g, '_').replace(/^\.+/, '')
|
|
return name.slice(0, 100) || 'SpineParticle'
|
|
}
|
|
|
|
function safeImagePath(value) {
|
|
const raw = String(value || '').replace(/\\/g, '/')
|
|
if (!raw || raw.startsWith('/') || raw.includes('\0')) throw new Error('图片路径无效')
|
|
const path = normalize(raw)
|
|
if (path === '..' || path.startsWith(`..${sep}`)) throw new Error('图片路径不能超出 images 目录')
|
|
return path
|
|
}
|
|
|
|
function safeImagesDirectory(value) {
|
|
const raw = String(value || './images/').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '') || 'images'
|
|
if (raw.startsWith('/') || /^[a-z]+:/i.test(raw)) throw new Error('Spine 图片路径必须是工程内的相对路径')
|
|
return safeImagePath(raw)
|
|
}
|
|
|
|
function decodeBase64(value) {
|
|
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) throw new Error('图片数据格式无效')
|
|
const data = Buffer.from(value, 'base64')
|
|
if (data.length > MAX_IMAGE_BYTES) throw new Error('单张图片不能超过 32MB')
|
|
return data
|
|
}
|
|
|
|
export async function createConversionWorkspace(payload) {
|
|
if (!payload || payload.schemaVersion !== 1) throw new Error('不支持的连接器请求格式')
|
|
if (typeof payload.skeletonJson !== 'string' || Buffer.byteLength(payload.skeletonJson) > MAX_JSON_BYTES) throw new Error('Spine JSON 缺失或超过 64MB')
|
|
let skeleton
|
|
try { skeleton = JSON.parse(payload.skeletonJson) } catch { throw new Error('Spine JSON 格式无效') }
|
|
const jsonVersion = String(skeleton?.skeleton?.spine || '')
|
|
const spineVersion = /^(3\.8|4\.[0-3])(?:\.\d+)?$/.exec(jsonVersion)?.[1]
|
|
if (!spineVersion) throw new Error(`JSON 中的 Spine 版本不受支持:${jsonVersion || '未知'}`)
|
|
const requestedVersion = String(payload.spineVersion || spineVersion)
|
|
if (requestedVersion !== spineVersion) throw new Error(`请求版本 ${requestedVersion} 与 JSON 版本 ${jsonVersion} 不一致`)
|
|
const imagesDirectory = safeImagesDirectory(skeleton?.skeleton?.images)
|
|
|
|
const root = await mkdtemp(join(tmpdir(), 'spine-particle-'))
|
|
try {
|
|
const projectName = safeProjectName(payload.projectName)
|
|
const inputPath = join(root, `${projectName}.json`)
|
|
const outputPath = join(root, `${projectName}.spine`)
|
|
await writeFile(inputPath, payload.skeletonJson, 'utf8')
|
|
let total = 0
|
|
const archiveImages = []
|
|
for (const image of Array.isArray(payload.images) ? payload.images : []) {
|
|
const data = decodeBase64(image?.dataBase64)
|
|
total += data.length
|
|
if (total > MAX_TOTAL_IMAGE_BYTES) throw new Error('图片总大小不能超过 128MB')
|
|
const relativeImagePath = safeImagePath(image?.path)
|
|
const target = join(root, imagesDirectory, relativeImagePath)
|
|
await mkdir(dirname(target), { recursive: true })
|
|
await writeFile(target, data)
|
|
archiveImages.push({ name: `${imagesDirectory.replace(/\\/g, '/')}/${relativeImagePath.replace(/\\/g, '/')}`, data })
|
|
}
|
|
return {
|
|
root,
|
|
projectName,
|
|
spineVersion,
|
|
archiveImages,
|
|
inputPath,
|
|
outputPath,
|
|
readOutput: () => readFile(outputPath),
|
|
cleanup: () => rm(root, { recursive: true, force: true }),
|
|
}
|
|
} catch (error) {
|
|
await rm(root, { recursive: true, force: true })
|
|
throw error
|
|
}
|
|
}
|