| 1 | import { app } from 'electron' |
| 2 | import fs from 'fs' |
| 3 | import path from 'path' |
| 4 | import { customAlphabet } from 'nanoid' |
| 5 | import { allowLocalAssetRoot } from '../io/local-asset-roots' |
| 6 | |
| 7 | const TEMPLATE_ID_RE = /^tpl_[a-zA-Z0-9_-]{8,80}$/ |
| 8 | const nanoidLower = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 12) |
| 9 | |
| 10 | export function createLowercaseId(): string { |
| 11 | return nanoidLower() |
| 12 | } |
| 13 | |
| 14 | export function resolveTemplatesRoot(): string { |
| 15 | const root = path.join(app.getPath('userData'), 'templates') |
| 16 | allowLocalAssetRoot(root) |
| 17 | return root |
| 18 | } |
| 19 | |
| 20 | export async function ensureTemplatesRoot(): Promise<string> { |
| 21 | const root = resolveTemplatesRoot() |
| 22 | await fs.promises.mkdir(root, { recursive: true }) |
| 23 | allowLocalAssetRoot(root) |
| 24 | return root |
| 25 | } |
| 26 | |
| 27 | export function isPathInside(targetPath: string, rootPath: string): boolean { |
| 28 | const relative = path.relative(rootPath, targetPath) |
| 29 | return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative)) |
| 30 | } |
| 31 | |
| 32 | export function normalizeTemplateId(value: unknown): string { |
| 33 | const id = typeof value === 'string' ? value.trim() : '' |
| 34 | if (!TEMPLATE_ID_RE.test(id)) throw new Error('模板 ID 无效') |
| 35 | return id |
| 36 | } |
| 37 | |
| 38 | export function resolveTemplateDir(templatesRoot: string, templateId: string): string { |
| 39 | const id = normalizeTemplateId(templateId) |
| 40 | const dir = path.resolve(templatesRoot, id) |
| 41 | if (!isPathInside(dir, templatesRoot)) throw new Error('模板路径越界') |
| 42 | return dir |
| 43 | } |
| 44 | |
| 45 | export function resolveTemplateManifestPath(templatesRoot: string, templateId: string): string { |
| 46 | return path.join(resolveTemplateDir(templatesRoot, templateId), 'manifest.json') |
| 47 | } |
| 48 | |
| 49 | export function resolveTemplateRelativePath(templateDir: string, relativePath?: string | null): string | null { |
| 50 | const raw = typeof relativePath === 'string' ? relativePath.trim() : '' |
| 51 | if (!raw) return null |
| 52 | const resolved = path.resolve(templateDir, raw) |
| 53 | if (!isPathInside(resolved, templateDir)) return null |
| 54 | return resolved |
| 55 | } |
| 56 | |
| 57 | export function createTemplateId(): string { |
| 58 | return `tpl_${createLowercaseId()}` |
| 59 | } |
| 60 |