From 3c0c3a204367c7af8d9ff0676cd08ecd3b5d11f6 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 24 Oct 2026 08:31:19 +0300 Subject: [PATCH] Reset picker status between selections and harden upload guards --- site/routes/api_bp.py | 6 ++++++ site/static/dist/file-picker.esm.js | 6 +++++- site/static/dist/file-picker.iife.js | 6 +++++- site/templates/index.html | 14 ++++++++++++-- upload/frontend/index.js | 2 ++ upload/frontend/table/on_files_change.js | 3 +++ upload/frontend/table/on_folder_change.js | 21 +++++++++++++++------ 7 files changed, 48 insertions(+), 10 deletions(-) diff --git a/site/routes/api_bp.py b/site/routes/api_bp.py index e44aca4..01af493 100644 --- a/site/routes/api_bp.py +++ b/site/routes/api_bp.py @@ -63,6 +63,12 @@ def upload(): if not name: had_unnamed = True continue + # Ранняя проверка размера по Content-Length позволяет не читать в память + # тело файла, заведомо превышающего лимит. Проверка выполняется до f.read(). + if f.content_length is not None and f.content_length > MAX_FILE_BYTES: + log.warning("upload: file exceeds %dMB, skipped sid=%s file=%r size=%d", + MAX_FILE_BYTES // (1024 * 1024), sid, name, f.content_length) + continue data = f.read() log.info("upload: sid=%s file=%r size=%d", sid, name, len(data)) if len(data) > MAX_FILE_BYTES: diff --git a/site/static/dist/file-picker.esm.js b/site/static/dist/file-picker.esm.js index d9f7ac1..1ff9eec 100644 --- a/site/static/dist/file-picker.esm.js +++ b/site/static/dist/file-picker.esm.js @@ -1,4 +1,4 @@ -// node_modules/fflate/esm/browser.js +// .agent-tmp/node_modules/fflate/esm/browser.js var u8 = Uint8Array; var u16 = Uint16Array; var i32 = Int32Array; @@ -717,6 +717,7 @@ function onFilesChange(state, cfg, elements) { return async () => { if (state.busy) return; state.busy = true; + if (elements.statusEl) elements.statusEl.textContent = ""; try { await addFiles(state, cfg, elements.fileInputEl.files, elements); } finally { @@ -731,6 +732,7 @@ function onFolderChange(state, cfg, elements) { return async () => { if (state.busy) return; state.busy = true; + if (elements.statusEl) elements.statusEl.textContent = ""; try { const roots = /* @__PURE__ */ new Map(); for (const file of Array.from(elements.folderInputEl.files)) { @@ -1153,6 +1155,7 @@ function initFilePicker(config) { if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ""; redraw(); notify(cfg, state); }); @@ -1174,6 +1177,7 @@ function initFilePicker(config) { clear: () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ""; redraw(); notify(cfg, state); }, diff --git a/site/static/dist/file-picker.iife.js b/site/static/dist/file-picker.iife.js index 0659d05..ec46e8f 100644 --- a/site/static/dist/file-picker.iife.js +++ b/site/static/dist/file-picker.iife.js @@ -26,7 +26,7 @@ var FilePicker = (() => { uploadViaVM: () => uploadViaVM }); - // node_modules/fflate/esm/browser.js + // .agent-tmp/node_modules/fflate/esm/browser.js var u8 = Uint8Array; var u16 = Uint16Array; var i32 = Int32Array; @@ -745,6 +745,7 @@ var FilePicker = (() => { return async () => { if (state.busy) return; state.busy = true; + if (elements.statusEl) elements.statusEl.textContent = ""; try { await addFiles(state, cfg, elements.fileInputEl.files, elements); } finally { @@ -759,6 +760,7 @@ var FilePicker = (() => { return async () => { if (state.busy) return; state.busy = true; + if (elements.statusEl) elements.statusEl.textContent = ""; try { const roots = /* @__PURE__ */ new Map(); for (const file of Array.from(elements.folderInputEl.files)) { @@ -1181,6 +1183,7 @@ var FilePicker = (() => { if (root.querySelector("#fp-clear-btn")) listen(root.querySelector("#fp-clear-btn"), "click", () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ""; redraw(); notify(cfg, state); }); @@ -1202,6 +1205,7 @@ var FilePicker = (() => { clear: () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ""; redraw(); notify(cfg, state); }, diff --git a/site/templates/index.html b/site/templates/index.html index ae6ba12..de546b8 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -877,13 +877,23 @@ if (!sessionDone) ub.disabled = false; } + function parseFilename(disp) { + if (!disp) return 'obfuscator.zip'; + // RFC 5987: filename*=UTF-8''... имеет приоритет над обычным filename=. + const utf8 = disp.match(/filename\*=UTF-8''([^;]+)/i); + if (utf8) { + try { return decodeURIComponent(utf8[1]); } catch (e) { /* повреждённый percent-encoding */ } + } + const plain = disp.match(/filename="?([^";]+)"?/i); + return plain ? plain[1] : 'obfuscator.zip'; + } + function downloadZip() { if (!currentSid) return; fetch('/api/download/' + currentSid) .then(r => { const disp = r.headers.get('Content-Disposition'); - const m = disp && disp.match(/filename="?(.+?)"?$/); - return Promise.all([r.blob(), m ? m[1] : 'obfuscator.zip']); + return Promise.all([r.blob(), parseFilename(disp)]); }) .then(([blob, fname]) => { const a = document.createElement('a'); diff --git a/upload/frontend/index.js b/upload/frontend/index.js index f360dd7..2c1803c 100644 --- a/upload/frontend/index.js +++ b/upload/frontend/index.js @@ -156,6 +156,7 @@ export function initFilePicker(config) { if (root.querySelector('#fp-clear-btn')) listen(root.querySelector('#fp-clear-btn'), 'click', () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ''; redraw(); notify(cfg, state); }); @@ -174,6 +175,7 @@ export function initFilePicker(config) { clear: () => { state.nodes = []; state.fileKeys.clear(); + if (elements.statusEl) elements.statusEl.textContent = ''; redraw(); notify(cfg, state); }, diff --git a/upload/frontend/table/on_files_change.js b/upload/frontend/table/on_files_change.js index 6e9ee0b..cc628c5 100644 --- a/upload/frontend/table/on_files_change.js +++ b/upload/frontend/table/on_files_change.js @@ -61,6 +61,9 @@ export function onFilesChange(state, cfg, elements) { return async () => { if (state.busy) return; state.busy = true; + // Сброс статуса перед новым выбором: иначе сообщение об ошибке предыдущего + // выбора (например, «Ошибка чтения архива») останется висеть в интерфейсе. + if (elements.statusEl) elements.statusEl.textContent = ''; try { await addFiles(state, cfg, elements.fileInputEl.files, elements); } finally { diff --git a/upload/frontend/table/on_folder_change.js b/upload/frontend/table/on_folder_change.js index 5ffed6c..5c6850d 100644 --- a/upload/frontend/table/on_folder_change.js +++ b/upload/frontend/table/on_folder_change.js @@ -20,6 +20,9 @@ export function onFolderChange(state, cfg, elements) { return async () => { if (state.busy) return; state.busy = true; + // Сброс статуса перед новым выбором: иначе сообщение об ошибке предыдущего + // выбора (например, «Ошибка чтения архива») останется висеть в интерфейсе. + if (elements.statusEl) elements.statusEl.textContent = ''; try { const roots = new Map(); for (const file of Array.from(elements.folderInputEl.files)) { @@ -30,8 +33,10 @@ export function onFolderChange(state, cfg, elements) { const rootName = parts[0] || file.name; if (!roots.has(rootName)) { // Один root на выбранный каталог позволяет сохранить дерево целиком. - roots.set(rootName, { id: crypto.randomUUID(), kind: 'folder', name: rootName, - path: rootName, children: [], expanded: false }); + roots.set(rootName, { + id: crypto.randomUUID(), kind: 'folder', name: rootName, + path: rootName, children: [], expanded: false + }); } const root = roots.get(rootName); // Вставляет узел по его пути, создавая отсутствующие промежуточные папки. @@ -42,9 +47,11 @@ export function onFolderChange(state, cfg, elements) { 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, + child = last ? node : { + id: crypto.randomUUID(), kind: 'folder', name: part, path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`, - children: [], expanded: false }; + children: [], expanded: false + }; current.children.push(child); } current = child; @@ -68,8 +75,10 @@ export function onFolderChange(state, cfg, elements) { 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 }); + addToFolder({ + id: crypto.randomUUID(), kind: 'file', name: parts.at(-1), + path: `${rootName}/${relativePath}`, file, children: [], expanded: true + }); } } // Дедупликация выполняется после сборки каждого корня каталога.