mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-06 08:18:53 +00:00
fix: gate Firefox focus sentinel on changeStarted; preserve file row on storage delete failure
- 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.
This commit is contained in:
parent
a2238f17b6
commit
8f5e6a439d
2 changed files with 35 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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('.')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue