Stop Comfy input folder filling with duplicate still uploads.

Name uploads by content hash so the same still overwrites itself, enable stale input sweeps on purge, and remove the pile already on the desktop Shared input folder.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-04 08:20:39 -05:00
co-authored by Cursor
parent 20f373b172
commit c043890ac0
2 changed files with 20 additions and 8 deletions
+11 -4
View File
@@ -1,4 +1,5 @@
import { LTX_NEGATIVE } from '~/utils/videoModels' import { LTX_NEGATIVE } from '~/utils/videoModels'
import { createHash } from 'node:crypto'
import { comfyJobPrefix } from '~/utils/outputNames' import { comfyJobPrefix } from '~/utils/outputNames'
let comfyHostOverride = '' let comfyHostOverride = ''
@@ -124,7 +125,7 @@ export async function assertImageScaleToTotalPixelsNode() {
export async function uploadImage(file: { filename: string; data: Buffer; type?: string }, jobId?: string) { export async function uploadImage(file: { filename: string; data: Buffer; type?: string }, jobId?: string) {
const body = new FormData() const body = new FormData()
const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' }) 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('image', blob, filename)
body.append('overwrite', 'true') body.append('overwrite', 'true')
body.append('type', 'input') 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 raw = String(original || 'still.png')
const dot = raw.lastIndexOf('.') const dot = raw.lastIndexOf('.')
const ext = (dot >= 0 ? raw.slice(dot) : '.png').replace(/[^.a-zA-Z0-9]/g, '') || '.png' 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' 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) { function comfyQueueMessage(payload: unknown, status: number) {
@@ -625,7 +630,9 @@ async function purgeOnDesktop(opts: {
video: opts.video || null, video: opts.video || null,
audio: opts.audio || null, audio: opts.audio || null,
output: opts.output || (opts.video && /\.(png|jpe?g|webp|gif)$/i.test(opts.video.filename) ? opts.video : 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) signal: AbortSignal.timeout(12000)
}) })
+9 -4
View File
@@ -1,3 +1,4 @@
import { createHash } from 'node:crypto'
import { AsyncLocalStorage } from 'node:async_hooks' import { AsyncLocalStorage } from 'node:async_hooks'
import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy' import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy'
import { comfyJobPrefix } from '~/utils/outputNames' import { comfyJobPrefix } from '~/utils/outputNames'
@@ -94,18 +95,21 @@ export function imageComfyWsUrl(clientId: string) {
return `${getImageComfyHost().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}` 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 raw = String(original || 'still.png')
const dot = raw.lastIndexOf('.') const dot = raw.lastIndexOf('.')
const ext = (dot >= 0 ? raw.slice(dot) : '.png').replace(/[^.a-zA-Z0-9]/g, '') || '.png' 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' 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) { export async function uploadImageEdit(file: { filename: string; data: Buffer; type?: string }, jobId?: string) {
const body = new FormData() const body = new FormData()
const blob = new Blob([new Uint8Array(file.data)], { type: file.type || 'application/octet-stream' }) 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('image', blob, filename)
body.append('overwrite', 'true') body.append('overwrite', 'true')
body.append('type', 'input') body.append('type', 'input')
@@ -393,7 +397,8 @@ async function purgeImageOnDesktop(opts: {
imageNames, imageNames,
imageSubfolder: opts.inputSubfolder || '', imageSubfolder: opts.inputSubfolder || '',
output: opts.output || null, output: opts.output || null,
sweep: false sweep: true,
sweepMaxAgeMs: 2 * 60 * 1000
}), }),
signal: AbortSignal.timeout(8000) signal: AbortSignal.timeout(8000)
}) })