frontend: fix uploads
ci / docker (backend, backend/Dockerfile, backend) (push) Successful in 19s
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Successful in 33s

This commit is contained in:
2025-10-12 16:01:28 +02:00
parent 111d2cada3
commit 9cd69dc00c
+70 -70
View File
@@ -3050,64 +3050,47 @@ const AppLayout = () => {
} }
const items = Array.from(dataTransfer.items || []); const items = Array.from(dataTransfer.items || []);
console.info('[Uploads] drop start', { items: items.length, files: (dataTransfer.files || []).length });
const results = []; const results = [];
const seenKeys = new Set();
const pushFile = (file, ancestors) => { const pushFile = (file, ancestors = []) => {
if (file) { if (!file) return;
results.push({ const segments = (ancestors || []).filter(Boolean);
file, const key = `${segments.join('/')}/${file.name}:${file.size}`;
segments: (ancestors || []).filter(Boolean), if (seenKeys.has(key)) {
}); // skipped duplicate
return;
} }
seenKeys.add(key);
results.push({ file, segments });
// queued file
}; };
const supportsFileSystemAccess = const readAllEntries = async (reader) => {
window.isSecureContext && const entries = [];
items.some((item) => typeof item.getAsFileSystemHandle === 'function'); while (true) {
const supportsWebkitEntries = items.some(
(item) => typeof item.webkitGetAsEntry === 'function',
);
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 // eslint-disable-next-line no-await-in-loop
await walkDirectoryHandle(child, nextAncestors); const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
if (!batch.length) {
break;
} }
entries.push(...batch);
} }
return entries;
}; };
for (const item of items) { const walkEntry = async (entry, ancestors = []) => {
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) return;
if (entry.isFile) { if (entry.isFile) {
const file = await new Promise((resolve, reject) => { const file = await new Promise((resolve, reject) => {
try {
entry.file(resolve, reject); entry.file(resolve, reject);
} catch (error) {
console.warn('[Uploads] entry.file failed', error);
reject(error);
}
}); });
pushFile(file, ancestors); pushFile(file, ancestors);
return; return;
@@ -3115,38 +3098,53 @@ const AppLayout = () => {
if (entry.isDirectory) { if (entry.isDirectory) {
const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors]; const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors];
const reader = entry.createReader(); const reader = entry.createReader();
const readEntries = () => const entries = await readAllEntries(reader);
new Promise((resolve, reject) => { for (const child of entries) {
reader.readEntries(resolve, reject);
});
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const batch = await readEntries(); await walkEntry(child, nextAncestors);
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) { await Promise.all(
const entry = items.map(async (item, index) => {
typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null; if (item.kind !== 'file') return;
if (!entry) continue;
// eslint-disable-next-line no-await-in-loop const fileFromItem = typeof item.getAsFile === 'function' ? item.getAsFile() : null;
await walkWebkitEntry(entry, []); if (fileFromItem) {
const relativePath =
typeof fileFromItem.webkitRelativePath === 'string' ? fileFromItem.webkitRelativePath : '';
const segments = relativePath
? relativePath
.split('/')
.slice(0, -1)
.filter(Boolean)
: [];
pushFile(fileFromItem, segments);
} }
} else {
const files = Array.from(dataTransfer.files || []); if (typeof item.webkitGetAsEntry === 'function') {
files.forEach((file) => { 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);
}
}),
);
Array.from(dataTransfer.files || []).forEach((file, index) => {
if (!file) return; if (!file) return;
// FileList entry suppressed
const relativePath = const relativePath =
typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : ''; typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : '';
const segments = relativePath const segments = relativePath
@@ -3157,15 +3155,17 @@ const AppLayout = () => {
: []; : [];
pushFile(file, segments); pushFile(file, segments);
}); });
}
if (!results.length) { if (!results.length) {
throw new Error('No files detected in drop payload.'); throw new Error('No files detected in drop payload.');
} }
console.info('[Uploads] prepared files', results.length);
return results; return results;
}, []); }, []);
const handleFileDrop = useCallback( const handleFileDrop = useCallback(
async (dataTransfer, targetFolderId) => { async (dataTransfer, targetFolderId) => {
if (!token) { if (!token) {