// AVA — server-side preset folder-tree helpers. // // The preset library is a virtual file tree rooted at "root" with two // structural areas: "root/embedded" (the shipped Winamp AVS ports) and // "root/user created" (the user's own space). Every folder below those is // fully manageable (create / rename / delete / export / import). import { db } from '@/lib/db' export const MAX_DEPTH = 8 export const MAX_SEGMENT = 80 /** Folders that anchor the tree — they can never be renamed or deleted. */ export const PROTECTED_FOLDERS = new Set(['root', 'root/embedded', 'root/user created']) export interface ParsedPath { path: string segments: string[] } /** * Validate + normalize a folder path ("root/a/b"). Returns null when the path * is malformed (wrong root, too deep, bad characters, empty segments). */ export function normalizeFolderPath(raw: unknown): ParsedPath | null { if (typeof raw !== 'string') return null const trimmed = raw.trim().replace(/\/+$/, '') if (!trimmed) return null const parts = trimmed .split('/') .map((s) => s.trim()) .filter((s) => s.length > 0) if (parts.length === 0 || parts.length > MAX_DEPTH) return null if (parts[0] !== 'root') return null for (const seg of parts) { if (seg.length > MAX_SEGMENT) return null if (/[\u0000-\u001f\u007f]/.test(seg)) return null if (seg === '.' || seg === '..') return null } return { path: parts.join('/'), segments: parts } } /** Validate a preset / folder display name (no separators, sane length). */ export function isValidName(raw: unknown, maxLen = 120): string | null { if (typeof raw !== 'string') return null const name = raw.trim() if (!name || name.length > maxLen) return null if (/[/\u0000-\u001f\u007f]/.test(name)) return null return name } /** Parent path of a segment list: ["root","a","b"] → "root/a". */ export function parentOf(segments: string[]): string { return segments.slice(0, -1).join('/') } /** Prisma where-clause matching presets in a folder and every folder below it. */ export function subtreeFilter(path: string) { return { OR: [{ folderPath: path }, { folderPath: { startsWith: `${path}/` } }], } } /** Same, but for Folder rows (their column is `path`). */ export function subtreeFolderFilter(path: string) { return { OR: [{ path }, { path: { startsWith: `${path}/` } }], } } /** * Ensure Folder rows exist for the given paths AND all of their ancestors. * Returns how many rows were created. */ export async function ensureFolderRows(paths: string[]): Promise { const wanted = new Set() for (const p of paths) { const segs = p.split('/') for (let i = 1; i <= segs.length; i++) wanted.add(segs.slice(0, i).join('/')) } const existing = await db.folder.findMany({ where: { path: { in: [...wanted] } }, select: { path: true }, }) for (const e of existing) wanted.delete(e.path) if (wanted.size === 0) return 0 await db.folder.createMany({ data: [...wanted].map((path) => ({ path })) }) return wanted.size }