import { resolve, relative, isAbsolute } from 'node:path' import { existsSync, realpathSync, statSync, unlinkSync } from 'node:fs' // Keep the host cleanup endpoint independently deployable from the web application. function scopedFile(file, prefix) { const path=[file.subfolder,file.filename].map(s=>String(s||'').replaceAll('\\','/')).filter(Boolean).join('/') const p=String(prefix||'').replaceAll('\\','/').replace(/\/$/,'') return !!p && !path.startsWith('/') && !path.split('/').some(s=>s==='..'||s==='.') && path.startsWith(p+'/') } /** Exact Studio 2 files only: no folder fallback, sibling deletion, or sweeping. */ export function purgeStudio2Files({prefix, files}, rootsForType) { if (!Array.isArray(files) || !files.length || files.length>100 || !prefix) throw new Error('Invalid Studio 2 purge manifest') const targets=[] for (const file of files) { if (!['input','output'].includes(file.type) || !scopedFile(file,prefix) || !String(file.subfolder).replaceAll('\\','/').startsWith(String(prefix).replace(/\/$/,'')+'/studio2/')) throw new Error('Purge file is outside the Studio 2 prefix') const roots=rootsForType(file.type) if(!roots.length)throw new Error('No media root is configured for cleanup') for (const root of roots) { const target=resolve(root,file.subfolder,file.filename),rel=relative(resolve(root),target) if (!rel || rel.startsWith('..') || isAbsolute(rel)) throw new Error('Purge path escapes its media root') if (!existsSync(target)) continue const realRoot=realpathSync(root),realTarget=realpathSync(target),realRel=relative(realRoot,realTarget) if (!realRel || realRel.startsWith('..') || isAbsolute(realRel) || !statSync(realTarget).isFile()) throw new Error('Purge target is not a media file inside its root') const prefixRel=relative(resolve(realRoot,prefix),realTarget) if(!prefixRel || prefixRel.startsWith('..') || isAbsolute(prefixRel))throw new Error('Purge target resolves outside the instance prefix') targets.push(target) } } for(const target of new Set(targets))unlinkSync(target) return {ok:true,cleared:files.length,deleted:[...new Set(targets)]} }