Fix async picker race
This commit is contained in:
@@ -146,6 +146,26 @@
|
||||
|
||||
ZIP с `folder/../../escape.txt`, `/absolute.md`, `windows\\..\\bad.txt`, `folder/./dot.txt` и безопасным `safe/normal.txt` оставил только `paths.zip/safe/normal.txt`. Группы `..`, пустая группа и абсолютный путь больше не отображаются. Результат: `1 файлов · 2 B`.
|
||||
|
||||
## Исправление async race — 2026-09-05
|
||||
|
||||
### Причина
|
||||
|
||||
`onFilesChange` запускал асинхронный `addFiles` без ожидания и без установки `state.busy` до первого `await`. При быстрых повторных событиях выбора ZIP несколько обработчиков могли одновременно менять дерево и вызывать `render`.
|
||||
|
||||
### Исправление
|
||||
|
||||
- `onFilesChange` теперь сразу устанавливает `state.busy`, ожидает `addFiles` и сбрасывает флаг в `finally`.
|
||||
- `onFolderChange` использует тот же порядок: ранний выход при занятом состоянии, блокировка до асинхронного разбора ZIP и гарантированный сброс в `finally`.
|
||||
- Ошибка разбора ZIP больше не оставляет picker заблокированным.
|
||||
|
||||
### Проверки
|
||||
|
||||
- Все frontend `.js` файлы проходят `node --input-type=module --check`.
|
||||
- `site/app.py` проходит `python3 -m py_compile`.
|
||||
- `git diff --check` проходит.
|
||||
- Flask smoke test загрузил страницу и оба изменённых ES-модуля без HTTP-ошибок.
|
||||
- Узкий Node-тест не применён: в проекте отсутствует `package.json` с `type: module`, поэтому Node трактует browser ES-модули как CommonJS; синтаксис проверен отдельным module-check, а загрузка проверена браузером.
|
||||
|
||||
- `c0b8788 Implement reusable upload platform` — первоначальный reusable upload platform.
|
||||
- `85605fe Remove VM upload layer; keep file picker only` — удаление VM upload и переход к picker-only.
|
||||
- `a965f18 Add hierarchical file picker` — иерархическое дерево, вложенные ZIP, удаление групп и файлов.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ with (ROOT / "config.json").open(encoding="utf-8") as config_file:
|
||||
CONFIG = json.load(config_file)
|
||||
|
||||
|
||||
VERSION = "0.1.8"
|
||||
VERSION = "0.1.9"
|
||||
|
||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||
|
||||
|
||||
@@ -25,7 +25,13 @@ export async function addFiles(state, cfg, files, elements) {
|
||||
}
|
||||
|
||||
export function onFilesChange(state, cfg, elements) {
|
||||
return () => {
|
||||
if (!state.busy) addFiles(state, cfg, elements.fileInputEl.files, elements);
|
||||
return async () => {
|
||||
if (state.busy) return;
|
||||
state.busy = true;
|
||||
try {
|
||||
await addFiles(state, cfg, elements.fileInputEl.files, elements);
|
||||
} finally {
|
||||
state.busy = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -12,49 +12,54 @@ function rebaseTree(root, prefix) {
|
||||
export function onFolderChange(state, cfg, elements) {
|
||||
return async () => {
|
||||
if (state.busy) return;
|
||||
const roots = new Map();
|
||||
for (const file of Array.from(elements.folderInputEl.files)) {
|
||||
const parts = (file.webkitRelativePath || file.name).split('/');
|
||||
const relativePath = parts.slice(1).join('/') || file.name;
|
||||
const lowerPath = relativePath.toLowerCase();
|
||||
const rootName = parts[0] || file.name;
|
||||
if (!roots.has(rootName)) {
|
||||
roots.set(rootName, { id: crypto.randomUUID(), kind: 'folder', name: rootName,
|
||||
path: rootName, children: [], expanded: true });
|
||||
}
|
||||
const root = roots.get(rootName);
|
||||
const addToFolder = (node) => {
|
||||
let current = root;
|
||||
const nodeParts = node.path.split('/').slice(1);
|
||||
nodeParts.forEach((part, index) => {
|
||||
const last = index === nodeParts.length - 1;
|
||||
let child = current.children.find((item) => item.name === part);
|
||||
if (!child) {
|
||||
child = last ? node : { id: crypto.randomUUID(), kind: 'folder', name: part,
|
||||
path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`,
|
||||
children: [], expanded: true };
|
||||
current.children.push(child);
|
||||
}
|
||||
current = child;
|
||||
});
|
||||
};
|
||||
if (lowerPath.endsWith('.zip')) {
|
||||
try {
|
||||
const zip = await listZipFiles(file, cfg.allowedExt);
|
||||
if (zip) {
|
||||
rebaseTree(zip, rootName);
|
||||
addToFolder(zip);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
state.busy = true;
|
||||
try {
|
||||
const roots = new Map();
|
||||
for (const file of Array.from(elements.folderInputEl.files)) {
|
||||
const parts = (file.webkitRelativePath || file.name).split('/');
|
||||
const relativePath = parts.slice(1).join('/') || file.name;
|
||||
const lowerPath = relativePath.toLowerCase();
|
||||
const rootName = parts[0] || file.name;
|
||||
if (!roots.has(rootName)) {
|
||||
roots.set(rootName, { id: crypto.randomUUID(), kind: 'folder', name: rootName,
|
||||
path: rootName, children: [], expanded: true });
|
||||
}
|
||||
const root = roots.get(rootName);
|
||||
const addToFolder = (node) => {
|
||||
let current = root;
|
||||
const nodeParts = node.path.split('/').slice(1);
|
||||
nodeParts.forEach((part, index) => {
|
||||
const last = index === nodeParts.length - 1;
|
||||
let child = current.children.find((item) => item.name === part);
|
||||
if (!child) {
|
||||
child = last ? node : { id: crypto.randomUUID(), kind: 'folder', name: part,
|
||||
path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`,
|
||||
children: [], expanded: true };
|
||||
current.children.push(child);
|
||||
}
|
||||
current = child;
|
||||
});
|
||||
};
|
||||
if (lowerPath.endsWith('.zip')) {
|
||||
try {
|
||||
const zip = await listZipFiles(file, cfg.allowedExt);
|
||||
if (zip) {
|
||||
rebaseTree(zip, rootName);
|
||||
addToFolder(zip);
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
} else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) {
|
||||
addToFolder({ id: crypto.randomUUID(), kind: 'file', name: parts.at(-1),
|
||||
path: `${rootName}/${relativePath}`, file, children: [], expanded: true });
|
||||
}
|
||||
} else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) {
|
||||
addToFolder({ id: crypto.randomUUID(), kind: 'file', name: parts.at(-1),
|
||||
path: `${rootName}/${relativePath}`, file, children: [], expanded: true });
|
||||
}
|
||||
roots.forEach((root) => addFileWithDedup(state, root));
|
||||
elements.folderInputEl.value = '';
|
||||
render(state, elements);
|
||||
} finally {
|
||||
state.busy = false;
|
||||
}
|
||||
roots.forEach((root) => addFileWithDedup(state, root));
|
||||
elements.folderInputEl.value = '';
|
||||
render(state, elements);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user