From 8f5e6a439de4a0b4b2f217eeb3da0f15bbebea3e Mon Sep 17 00:00:00 2001 From: DrMelone <27028174+Classic298@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:09:15 +0200 Subject: [PATCH] fix: gate Firefox focus sentinel on changeStarted; preserve file row on storage delete failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collectDirectoryFiles Firefox fallback now tracks whether the change event has started handling files and clears the window-focus sentinel timer as soon as it has. Previously the 500 ms timer could fire mid-hash for a large selection and resolve the Promise early with a partial files array — which in sync mode would misclassify unhashed files as removed and delete them from the KB even though the user actually selected them. Empty-picker cancellation still resolves cleanly because changeStarted stays false in that case. - remove_file_from_knowledge_by_id no longer drops the file DB row when Storage.delete_file fails. A transient S3/GCS failure would otherwise leave an orphan blob with no metadata row to retry against. The KB association and vector entries are already gone, so the caller sees the file removed from the KB; the 500 response surfaces the storage failure and a later retry (manual or automated) can complete the cleanup once storage recovers. --- backend/open_webui/routers/knowledge.py | 16 ++++++++++--- .../workspace/Knowledge/KnowledgeBase.svelte | 24 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 6c7bdc64dc..020cf00144 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -1057,14 +1057,24 @@ async def remove_file_from_knowledge_by_id( pass # Delete the object-storage blob before dropping the DB row so we - # still have file.path available; previously this endpoint only - # removed the DB record and orphaned the blob in S3/GCS/local. + # still have file.path available. If storage deletion fails we + # must NOT drop the DB row: a transient S3/GCS failure would + # otherwise leave an orphan blob with no metadata to retry + # against. Keeping the file row lets an operator re-issue the + # delete once the backend recovers. try: Storage.delete_file(file.path) except Exception as storage_err: - log.warning( + log.exception( f'Failed to delete storage blob for {form_data.file_id}: {storage_err}' ) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=( + 'File unlinked from knowledge base, but deleting the ' + 'stored blob failed; file record kept for retry.' + ), + ) # Delete file from database await Files.delete_file_by_id(form_data.file_id, db=db) diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index f04d0efea1..8d9e7e968a 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -481,9 +481,15 @@ // files, or we resolve with an empty list and the caller's // existing empty-check toasts. let settled = false; + let changeStarted = false; + let focusTimer: ReturnType | null = null; const finish = (err?: unknown) => { if (settled) return; settled = true; + if (focusTimer !== null) { + clearTimeout(focusTimer); + focusTimer = null; + } if (input.parentNode) { input.parentNode.removeChild(input); } @@ -496,11 +502,25 @@ }; const onFocus = () => { // 'change' fires after 'focus' returns to the window, so - // wait briefly before deciding the user cancelled. - setTimeout(() => finish(), 500); + // wait briefly before deciding the user cancelled. If a + // change event actually started handling files before the + // timer expires, changeStarted gates finish() so we don't + // resolve in the middle of hashing a large batch with a + // partial files array. + focusTimer = setTimeout(() => { + focusTimer = null; + if (!changeStarted) { + finish(); + } + }, 500); }; input.onchange = async () => { + changeStarted = true; + if (focusTimer !== null) { + clearTimeout(focusTimer); + focusTimer = null; + } try { const inputFiles = Array.from(input.files || []).filter( (file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.')