From 2d793ac2aa1d00c983f18903b469850caf0d06ad Mon Sep 17 00:00:00 2001 From: chodeus Date: Sat, 8 Aug 2026 20:33:02 +0800 Subject: [PATCH 1/2] fix: accept folder.view2 exports in Import Everything MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import Everything rejected any file without an fv3_export_version wrapper, so a folder.view2 backup dead-ended on "Not a valid FV3 backup file." The file was always importable — the dedicated Import Docker/Import VM buttons take it verbatim — but nothing pointed there, and those buttons already detect the reverse mistake and redirect. Only this direction was missing. Import Everything now detects a bare folder export (one folder object or an id => folder map) and offers Docker/VM buttons, reusing the same import path as the dedicated buttons. The type choice is buttons rather than confirm/cancel because sweetalert reports ESC and Cancel identically, so a dismissal would silently import as whichever type happened to map to false. They are bound with addEventListener: an inline onclick inside sweetalert's own markup compiles but never fires. Extract fv3ImportFolderMap() so the docker, vm and folder-export paths share one loop instead of three copies. Behaviour is unchanged, including vm import not calling sync_order. --- .../folder.view3/scripts/folderview3.js | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js index 9206c0d8..337aa940 100644 --- a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js +++ b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js @@ -141,6 +141,31 @@ const downloadDocker = async (id) => { } }; +// A folder export is either one folder object or an id => folder map (both shapes are what +// folder.view2 downloads). Neither carries a docker|vm marker — the caller supplies the type. +const fv3IsFolderShaped = (o) => !!o && typeof o === 'object' && !Array.isArray(o) + && typeof o.name === 'string' + && (Array.isArray(o.containers) || (!!o.settings && typeof o.settings === 'object')); + +const fv3CountFolderExport = (parsed) => { + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return 0; + if (fv3IsFolderShaped(parsed)) return 1; + const folders = Object.values(parsed); + return (folders.length && folders.every(fv3IsFolderShaped)) ? folders.length : 0; +}; + +const fv3ImportFolderMap = async (content, type) => { + if (content.name) { + await $.post('/plugins/folder.view3/server/create.php', { type: type, content: JSON.stringify(content) }); + } else { + for (const [id, folder] of Object.entries(content)) { + await $.post('/plugins/folder.view3/server/update.php', { type: type, content: JSON.stringify(folder), id: id }); + } + if (type === 'docker') await $.post('/plugins/folder.view3/server/sync_order.php', { type: 'docker' }); + } + populateTable(); +}; + const importDocker = () => { let input = document.getElementById('fv3-import-docker-file'); input.onchange = (e) => { @@ -169,15 +194,7 @@ const importDocker = () => { swal({ title: 'Wrong import', text: 'This is a full backup bundle — use "Import Everything" to restore it, or select a Docker folders export here.', type: 'error' }); return; } - if(content.name) { - await $.post('/plugins/folder.view3/server/create.php', { type: 'docker', content: JSON.stringify(content) }); - } else { - for (const [id, folder] of Object.entries(content)) { - await $.post('/plugins/folder.view3/server/update.php', { type: 'docker', content: JSON.stringify(folder), id: id }); - } - await $.post('/plugins/folder.view3/server/sync_order.php', { type: 'docker' }); - } - populateTable(); + await fv3ImportFolderMap(content, 'docker'); } } input.click(); @@ -211,14 +228,7 @@ const importVm = () => { swal({ title: 'Wrong import', text: 'This is a full backup bundle — use "Import Everything" to restore it, or select a VM folders export here.', type: 'error' }); return; } - if(content.name) { - await $.post('/plugins/folder.view3/server/create.php', { type: 'vm', content: JSON.stringify(content) }); - } else { - for (const [id, folder] of Object.entries(content)) { - await $.post('/plugins/folder.view3/server/update.php', { type: 'vm', content: JSON.stringify(folder), id: id }); - } - } - populateTable(); + await fv3ImportFolderMap(content, 'vm'); } } input.click(); @@ -595,6 +605,15 @@ const fv3ExportAll = async () => { }; window.fv3ExportAll = fv3ExportAll; +const fv3ImportFolderExport = async (content, type) => { + swal.close(); + try { + await fv3ImportFolderMap(content, type); + } catch (err) { + swal({ title: 'Error', text: 'Import failed.', type: 'error' }); + } +}; + $('#fv3-import-all-btn').on('click', () => $('#fv3-import-all').click()); $('#fv3-import-all').on('change', function() { const file = this.files[0]; @@ -604,7 +623,30 @@ $('#fv3-import-all').on('change', function() { reader.onload = async (e) => { try { const parsed = JSON.parse(e.target.result); - if (!parsed.fv3_export_version) { swal({ title: 'Error', text: 'Not a valid FV3 backup file.', type: 'error' }); return; } + if (!parsed.fv3_export_version) { + // folder.view2 exports land here — take them rather than dead-ending the user. + const count = fv3CountFolderExport(parsed); + if (!count) { swal({ title: 'Error', text: 'Not a valid FV3 backup file.', type: 'error' }); return; } + // The choice can't ride on the confirm/cancel boolean: swal reports ESC and Cancel + // identically, so a dismissal would silently import as whichever type lost the coin toss. + swal({ + title: 'Folder export detected', + text: `

This file holds ${count} folder${count === 1 ? '' : 's'} — a FolderView2 or per-type export, not a full FV3 backup.

` + + '

Import as:

' + + '' + + '

', + html: true, + showConfirmButton: false, + showCancelButton: true, + cancelButtonText: 'Cancel' + }); + // An inline onclick inside swal's own markup never fires — bind explicitly instead. + // The buttons exist synchronously once swal() has returned. + document.querySelectorAll('.sweet-alert button.fv3-choice').forEach(b => { + b.addEventListener('click', () => fv3ImportFolderExport(parsed, b.dataset.fv3Type)); + }); + return; + } const items = []; if (parsed.docker && Object.keys(parsed.docker).length) items.push(Object.keys(parsed.docker).length + ' Docker folders'); if (parsed.vm && Object.keys(parsed.vm).length) items.push(Object.keys(parsed.vm).length + ' VM folders'); From 7e503c920a33e99178b65cd3df774ca81508aa56 Mon Sep 17 00:00:00 2001 From: chodeus Date: Sun, 9 Aug 2026 08:03:52 +0800 Subject: [PATCH 2/2] fix: route folder imports by structure, not name truthiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fv3ImportFolderMap picked its import mode with `if (content.name)`. An empty name is legal, so a single-folder export named "" fell through to the map branch and its own keys were posted as folder ids — name, icon, settings, regex, containers, actions all pass update.php's id charset, so six junk folders got written. The inverse also misfired: a map whose folder id is the string "name" was treated as a single folder. Select on fv3IsFolderShaped() instead, which is the same predicate detection already uses, so the two agree. Pre-existing in importDocker/importVm; fixing it in the shared helper covers both buttons. --- .../local/emhttp/plugins/folder.view3/scripts/folderview3.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js index 337aa940..d9874c19 100644 --- a/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js +++ b/src/folder.view3/usr/local/emhttp/plugins/folder.view3/scripts/folderview3.js @@ -155,7 +155,9 @@ const fv3CountFolderExport = (parsed) => { }; const fv3ImportFolderMap = async (content, type) => { - if (content.name) { + // Structural test, not `content.name` — an empty name is legal and would otherwise route a + // single folder down the map path, writing its own keys (name, icon, settings…) as folder ids. + if (fv3IsFolderShaped(content)) { await $.post('/plugins/folder.view3/server/create.php', { type: type, content: JSON.stringify(content) }); } else { for (const [id, folder] of Object.entries(content)) {