Media upload: show percent while sending, read the body in 1MB chunks, write off the event loop
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6rGxH5eLym2ADye6YDgEW
This commit is contained in:
@@ -95,7 +95,7 @@ export default {
|
||||
const note = c.text.sm({ text: "" });
|
||||
const drop = c.dropzone.default({ label: "Drop or choose files", hint: "images, clips, audio",
|
||||
onFiles: async (files) => {
|
||||
try { const r = await api.uploadMedia(files, (i, n, name) => note.setText(`Uploading ${i + 1}/${n}: ${name}`)); (r.problems || []).forEach(tell); await refresh(); } catch (err) { tell(err.message); }
|
||||
try { const r = await api.uploadMedia(files, (i, n, name, f) => note.setText(`Uploading ${i + 1}/${n}: ${name}${f ? ` ${f >= 1 ? "(saving...)" : `${Math.round(f * 100)}%`}` : ""}`)); (r.problems || []).forEach(tell); await refresh(); } catch (err) { tell(err.message); }
|
||||
note.setText("");
|
||||
} });
|
||||
const seg = (label, options, key) => c.segmented.sm({ label, options, value: show[key], onChange: (v) => { show[key] = v; draw(true); } });
|
||||
|
||||
+18
-3
@@ -19,6 +19,20 @@ export async function call(method, path, body) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** POST a form. fetch cannot report bytes sent, so use XHR where it exists (browsers) and fetch elsewhere.
|
||||
* Resolves a fetch-like {ok, status, json()}. */
|
||||
function post(url, form, onFraction) {
|
||||
if (typeof XMLHttpRequest === "undefined") return fetch(url, { method: "POST", body: form });
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", url);
|
||||
xhr.upload.onprogress = (e) => { if (e.lengthComputable) onFraction(e.loaded / e.total); };
|
||||
xhr.onerror = () => reject(new Error("the upload was interrupted"));
|
||||
xhr.onload = () => resolve({ ok: xhr.status >= 200 && xhr.status < 300, status: xhr.status, statusText: xhr.statusText, json: async () => JSON.parse(xhr.responseText) });
|
||||
xhr.send(form);
|
||||
});
|
||||
}
|
||||
|
||||
export const api = {
|
||||
pipeline: () => call("GET", "/api/pipeline"),
|
||||
editPipeline: (body) => call("POST", "/api/pipeline", body),
|
||||
@@ -167,15 +181,16 @@ export const api = {
|
||||
deleteMedia: (id) => call("DELETE", `/api/media/${encodeURIComponent(id)}`),
|
||||
/** Upload files as media, one request each so one refusal (too big, wrong type) does not sink the rest.
|
||||
* Resolves {media, problems}. Not JSON, so not through call(). */
|
||||
async uploadMedia(files, onProgress) { // onProgress(done, total, name) before each file
|
||||
async uploadMedia(files, onProgress) { // onProgress(done, total, name, fraction) before each file, then again as bytes go out
|
||||
const out = { media: [], problems: [] };
|
||||
let done = 0;
|
||||
for (const file of files) {
|
||||
if (onProgress) onProgress(done++, files.length, file.name);
|
||||
const at = done++;
|
||||
if (onProgress) onProgress(at, files.length, file.name, 0);
|
||||
const form = new FormData();
|
||||
form.append("file", file, file.name);
|
||||
try {
|
||||
const res = await fetch(`${BASE}/api/media`, { method: "POST", body: form });
|
||||
const res = await post(`${BASE}/api/media`, form, (f) => onProgress && onProgress(at, files.length, file.name, f));
|
||||
const payload = await res.json().catch(() => null);
|
||||
if (!res.ok) throw Object.assign(new Error(why(res, payload)), { status: res.status }); // status: a refusal (4xx) is not an outage
|
||||
out.media.push(...(payload.media || [])); out.problems.push(...(payload.problems || []));
|
||||
|
||||
+13
-2
@@ -971,9 +971,20 @@ def register(routes, prefix=None):
|
||||
break
|
||||
if not part.filename:
|
||||
continue # a form field that is not a file
|
||||
data = await part.read(decode=False)
|
||||
# 1MB reads (the default is 8KB: thousands of loop turns for a clip) and a
|
||||
# stop at the limit, so a too-big file is refused without buffering all of it
|
||||
buf = bytearray()
|
||||
while True:
|
||||
chunk = await part.read_chunk(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
if len(buf) > media.MAX_BYTES:
|
||||
break
|
||||
data = bytes(buf)
|
||||
try:
|
||||
saved.append(media.save_upload(part.filename, data))
|
||||
# to_thread: writing a big clip must not stall the one event loop ComfyUI has
|
||||
saved.append(await asyncio.to_thread(media.save_upload, part.filename, data))
|
||||
except ValueError as exc:
|
||||
problems.append(f"{part.filename}: {exc}")
|
||||
if not saved and problems:
|
||||
|
||||
Reference in New Issue
Block a user