refactor: centralize all download utils across app and apply special cloud-specific behavior (#6188)

## Summary

Centralized all download functionalities across app. Then changed
downloadFile on the cloud distribution to stream assets via blob fetches
while desktop/local retains direct anchor downloads. This fixes issue
where trying to download cross-origin resources opens them in the
window, potentially losing the user's unsaved changes.

## Changes

- **What**: Moved `downloadBlob` into `downloadUtil`, routed all callers
(3D exporter, recording manager, node template export, workflow/palette
export, Litegraph save, ~~`useDownload` consumers~~) through shared
helpers, and changed `downloadFile` to `fetch` first when `isCloud` so
cross-origin URLs download reliably
- `useDownload` is the exception since we simply cannot do model
downloads through blob (forcing user to transfer the entire model data
twice is bad). Fortunately on cloud, the user doesn't need to download
models locally anyway.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6188-refactor-centralize-all-download-utils-across-app-and-apply-special-cloud-specific-behav-2946d73d365081de9f27f0994950511d)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2025-10-23 12:08:30 -07:00
committed by GitHub
parent 647e62d4b7
commit 4e5eba6c54
9 changed files with 160 additions and 89 deletions

View File

@@ -3,6 +3,7 @@ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { downloadBlob } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -38,13 +39,7 @@ export class ModelExporter {
try {
const response = await fetch(url)
const blob = await response.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = desiredFilename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(desiredFilename, blob)
} catch (error) {
console.error('Error downloading from URL:', error)
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
@@ -152,19 +147,11 @@ export class ModelExporter {
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
const blob = new Blob([buffer], { type: 'application/octet-stream' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(filename, blob)
}
private static saveString(text: string, filename: string): void {
const blob = new Blob([text], { type: 'text/plain' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(filename, blob)
}
}

View File

@@ -1,5 +1,7 @@
import * as THREE from 'three'
import { downloadBlob } from '@/base/common/downloadUtil'
import { type EventManagerInterface } from './interfaces'
export class RecordingManager {
@@ -149,17 +151,7 @@ export class RecordingManager {
try {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
document.body.appendChild(a)
a.style.display = 'none'
a.href = url
a.download = filename
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
downloadBlob(filename, blob)
this.eventManager.emitEvent('recordingExported', null)
} catch (error) {

View File

@@ -1,3 +1,4 @@
import { downloadBlob } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -145,18 +146,7 @@ class ManageTemplates extends ComfyDialog {
const json = JSON.stringify({ templates: this.templates }, null, 2) // convert the data to a JSON string
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
download: 'node_templates.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
downloadBlob('node_templates.json', blob)
}
override show() {
@@ -298,19 +288,9 @@ class ManageTemplates extends ComfyDialog {
const blob = new Blob([json], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
// @ts-expect-error fixme ts strict error
download: (nameInput.value || t.name) + '.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
// @ts-expect-error fixme ts strict error
const name = (nameInput.value || t.name) + '.json'
downloadBlob(name, blob)
}
}),
$el('button', {