diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index cd064bc..39f2c8b 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -3050,122 +3050,122 @@ const AppLayout = () => { } const items = Array.from(dataTransfer.items || []); - const results = []; + console.info('[Uploads] drop start', { items: items.length, files: (dataTransfer.files || []).length }); - const pushFile = (file, ancestors) => { - if (file) { - results.push({ - file, - segments: (ancestors || []).filter(Boolean), + const results = []; + const seenKeys = new Set(); + + const pushFile = (file, ancestors = []) => { + if (!file) return; + const segments = (ancestors || []).filter(Boolean); + const key = `${segments.join('/')}/${file.name}:${file.size}`; + if (seenKeys.has(key)) { + // skipped duplicate + return; + } + seenKeys.add(key); + results.push({ file, segments }); + // queued file + }; + + const readAllEntries = async (reader) => { + const entries = []; + while (true) { + // eslint-disable-next-line no-await-in-loop + const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject)); + if (!batch.length) { + break; + } + entries.push(...batch); + } + return entries; + }; + + const walkEntry = async (entry, ancestors = []) => { + if (!entry) return; + if (entry.isFile) { + const file = await new Promise((resolve, reject) => { + try { + entry.file(resolve, reject); + } catch (error) { + console.warn('[Uploads] entry.file failed', error); + reject(error); + } }); + pushFile(file, ancestors); + return; + } + if (entry.isDirectory) { + const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors]; + const reader = entry.createReader(); + const entries = await readAllEntries(reader); + for (const child of entries) { + // eslint-disable-next-line no-await-in-loop + await walkEntry(child, nextAncestors); + } } }; - const supportsFileSystemAccess = - window.isSecureContext && - items.some((item) => typeof item.getAsFileSystemHandle === 'function'); - const supportsWebkitEntries = items.some( - (item) => typeof item.webkitGetAsEntry === 'function', + await Promise.all( + items.map(async (item, index) => { + if (item.kind !== 'file') return; + + const fileFromItem = typeof item.getAsFile === 'function' ? item.getAsFile() : null; + if (fileFromItem) { + const relativePath = + typeof fileFromItem.webkitRelativePath === 'string' ? fileFromItem.webkitRelativePath : ''; + const segments = relativePath + ? relativePath + .split('/') + .slice(0, -1) + .filter(Boolean) + : []; + pushFile(fileFromItem, segments); + } + + if (typeof item.webkitGetAsEntry === 'function') { + try { + const entry = item.webkitGetAsEntry(); + if (entry) { + // processing entry + await walkEntry(entry, []); + return; + } + } catch (error) { + console.warn('[Uploads] webkitGetAsEntry failed', error); + } + } + + if (!fileFromItem) { + console.info('[Uploads] item missing file handle', index); + } + }), ); - if (supportsFileSystemAccess) { - const walkDirectoryHandle = async (handle, ancestors) => { - const nextAncestors = handle.name ? [...ancestors, handle.name] : [...ancestors]; - for await (const child of handle.values()) { - if (child.kind === 'file') { - const file = await child.getFile(); - pushFile(file, nextAncestors); - } else if (child.kind === 'directory') { - // eslint-disable-next-line no-await-in-loop - await walkDirectoryHandle(child, nextAncestors); - } - } - }; - - for (const item of items) { - if (item.kind !== 'file') continue; - const getHandle = item.getAsFileSystemHandle?.bind(item); - if (!getHandle) { - continue; - } - - // eslint-disable-next-line no-await-in-loop - const handle = await getHandle(); - if (!handle) continue; - - if (handle.kind === 'file') { - // eslint-disable-next-line no-await-in-loop - const file = await handle.getFile(); - pushFile(file, []); - } else if (handle.kind === 'directory') { - // eslint-disable-next-line no-await-in-loop - await walkDirectoryHandle(handle, []); - } - } - } else if (supportsWebkitEntries) { - const walkWebkitEntry = async (entry, ancestors) => { - if (!entry) return; - if (entry.isFile) { - const file = await new Promise((resolve, reject) => { - entry.file(resolve, reject); - }); - pushFile(file, ancestors); - return; - } - if (entry.isDirectory) { - const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors]; - const reader = entry.createReader(); - const readEntries = () => - new Promise((resolve, reject) => { - reader.readEntries(resolve, reject); - }); - - // eslint-disable-next-line no-constant-condition - while (true) { - // eslint-disable-next-line no-await-in-loop - const batch = await readEntries(); - if (!batch.length) { - break; - } - // eslint-disable-next-line no-await-in-loop - for (const child of batch) { - // eslint-disable-next-line no-await-in-loop - await walkWebkitEntry(child, nextAncestors); - } - } - } - }; - - for (const item of items) { - const entry = - typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null; - if (!entry) continue; - // eslint-disable-next-line no-await-in-loop - await walkWebkitEntry(entry, []); - } - } else { - const files = Array.from(dataTransfer.files || []); - files.forEach((file) => { - if (!file) return; - const relativePath = - typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : ''; - const segments = relativePath - ? relativePath - .split('/') - .slice(0, -1) - .filter(Boolean) - : []; - pushFile(file, segments); - }); - } + Array.from(dataTransfer.files || []).forEach((file, index) => { + if (!file) return; + // FileList entry suppressed + const relativePath = + typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : ''; + const segments = relativePath + ? relativePath + .split('/') + .slice(0, -1) + .filter(Boolean) + : []; + pushFile(file, segments); + }); if (!results.length) { throw new Error('No files detected in drop payload.'); } + console.info('[Uploads] prepared files', results.length); + return results; }, []); + const handleFileDrop = useCallback( async (dataTransfer, targetFolderId) => { if (!token) {