Files
aigen/scripts/studio2-purge.mjs

32 lines
2.1 KiB
JavaScript

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)]}
}