diff --git a/server/utils/comfy.ts b/server/utils/comfy.ts index 38f371b..d95c2bf 100644 --- a/server/utils/comfy.ts +++ b/server/utils/comfy.ts @@ -1,4 +1,5 @@ import { LTX_NEGATIVE } from '~/utils/videoModels' +import { createHash } from 'node:crypto' import { comfyJobPrefix } from '~/utils/outputNames' let comfyHostOverride = '' @@ -124,7 +125,7 @@ export async function assertImageScaleToTotalPixelsNode() { export async function uploadImage(file: { filename: string; data: Buffer; type?: string }, jobId?: string) { const body = new FormData() const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' }) - const filename = comfyInputFilename(file.filename, jobId) + const filename = comfyInputFilename(file.filename, file.data, jobId) body.append('image', blob, filename) body.append('overwrite', 'true') body.append('type', 'input') @@ -140,12 +141,16 @@ export async function uploadImage(file: { filename: string; data: Buffer; type?: } } -export function comfyInputFilename(original: string, jobId?: string) { +/** Content-hash names so the same still overwrites itself (overwrite:true). Job id alone created hundreds of copies. */ +export function comfyInputFilename(original: string, data?: Buffer, jobId?: string) { const raw = String(original || 'still.png') const dot = raw.lastIndexOf('.') const ext = (dot >= 0 ? raw.slice(dot) : '.png').replace(/[^.a-zA-Z0-9]/g, '') || '.png' const base = (dot >= 0 ? raw.slice(0, dot) : raw).replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 48) || 'still' - return `${comfyJobPrefix(jobId)}_${base}${ext}` + const hash = data?.length + ? createHash('sha1').update(data).digest('hex').slice(0, 12) + : comfyJobPrefix(jobId) + return `aigen_${hash}_${base}${ext}` } function comfyQueueMessage(payload: unknown, status: number) { @@ -625,7 +630,9 @@ async function purgeOnDesktop(opts: { video: opts.video || null, audio: opts.audio || null, output: opts.output || (opts.video && /\.(png|jpe?g|webp|gif)$/i.test(opts.video.filename) ? opts.video : null), - sweep: false + // Sweep leftover studio uploads — exact delete alone left hundreds of copies when a job failed mid-way. + sweep: true, + sweepMaxAgeMs: 2 * 60 * 1000 }), signal: AbortSignal.timeout(12000) }) diff --git a/server/utils/imageComfy.ts b/server/utils/imageComfy.ts index a672a52..a194aef 100644 --- a/server/utils/imageComfy.ts +++ b/server/utils/imageComfy.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto' import { AsyncLocalStorage } from 'node:async_hooks' import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy' import { comfyJobPrefix } from '~/utils/outputNames' @@ -94,18 +95,21 @@ export function imageComfyWsUrl(clientId: string) { return `${getImageComfyHost().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}` } -function imageInputFilename(original: string, jobId?: string) { +function imageInputFilename(original: string, data?: Buffer, jobId?: string) { const raw = String(original || 'still.png') const dot = raw.lastIndexOf('.') const ext = (dot >= 0 ? raw.slice(dot) : '.png').replace(/[^.a-zA-Z0-9]/g, '') || '.png' const base = (dot >= 0 ? raw.slice(0, dot) : raw).replace(/[^a-zA-Z0-9._-]+/g, '_').slice(0, 48) || 'still' - return `${comfyJobPrefix(jobId)}_${base}${ext}` + const hash = data?.length + ? createHash('sha1').update(data).digest('hex').slice(0, 12) + : comfyJobPrefix(jobId) + return `aigen_${hash}_${base}${ext}` } export async function uploadImageEdit(file: { filename: string; data: Buffer; type?: string }, jobId?: string) { const body = new FormData() const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' }) - const filename = imageInputFilename(file.filename, jobId) + const filename = imageInputFilename(file.filename, file.data, jobId) body.append('image', blob, filename) body.append('overwrite', 'true') body.append('type', 'input') @@ -393,7 +397,8 @@ async function purgeImageOnDesktop(opts: { imageNames, imageSubfolder: opts.inputSubfolder || '', output: opts.output || null, - sweep: false + sweep: true, + sweepMaxAgeMs: 2 * 60 * 1000 }), signal: AbortSignal.timeout(8000) })