v0.5.8: Windows ISOs just work (HTTP sanboot) + Storage UX

Windows boot, the "less is more" way. Windows ISOs now boot via iPXE
HTTP sanboot of the raw image — iPXE exposes the unmodified ISO as an
emulated CD backed by on-demand HTTP range reads, and Windows Setup
boots from it. This replaces the wimboot+SMB chain, which needed an SMB
server the host often can't provide (:445 collisions), served in-ISO
files via an ISO9660 lookup that failed on UDF-only Win11 ISOs, and was
gated behind a Settings toggle the WebUI never even exposed (so Windows
never booted). Now it needs only the HTTP port — works in any
environment, SMB or not — and nothing is injected into Windows (no
httpdisk.sys, no test certs, no trust-store changes; fully within the
project's hard rules).

- iso-store/store.rs: WindowsPe boot entry -> BootKind::SanBootIso of the
  raw iso/<id>.iso (render_entry already emits `sanboot --no-describe`).
- iso-store/introspect.rs: broaden Windows detection for UDF-only Win10/11
  ISOs — UTF-16LE markers (boot.wim/bootmgr/install.wim/microsoft),
  extra ASCII markers, and a filename heuristic, since their volume
  labels are cryptic and filenames are UTF-16. + unit tests.
- http-api/ipxe_script.rs: Windows installers submenu shows whenever a
  Windows ISO is present — no toggle, no "disabled in Settings".
- webui: dashboard no longer flags Windows ISOs (they boot now); the
  generic large-ISO warning reworded to read sensibly for genuinely
  non-bootable images (e.g. VMware VCSA appliance bundles).

Storage UX:
- Available images listed alphabetically by filename.
- Upload gains a Cancel button (aborts the chunk + discards the partial).
- beforeunload warning while an upload is in flight.

263 tests pass, clippy clean. NOTE: actual Windows boot is validated on
real hardware — code/script/range-serving are validated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-06-04 21:24:15 -04:00
co-authored by Claude Opus 4.8
parent ac433b30e9
commit 9fc9a9a1af
8 changed files with 189 additions and 65 deletions
+38 -10
View File
@@ -160,15 +160,18 @@
// tint borrowed from Bootimus v0.1.62. Returns {ok, reason}.
function bootability(iso, settings) {
const fam = iso.introspection.family;
const isWin = fam === 'windows_pe';
if (isWin && !settings.windows_enabled) {
return { ok: false, reason: 'Windows boot disabled in Settings' };
// v0.5.8: Windows ISOs boot via iPXE HTTP sanboot of the raw image —
// no Settings toggle, no SMB, no size limit. Always bootable.
if (fam === 'windows_pe') {
return { ok: true };
}
if (!isWin && !iso.introspection.kernel_path && fam !== 'windows_pe') {
// Linux without a detected kernel falls through to sanboot which
// rarely works for >1 GiB ISOs.
if (!iso.introspection.kernel_path) {
// Not Windows and no Linux kernel/initrd detected. Small images can
// still try the sanboot fallback; large ones almost certainly aren't
// network-bootable installers (e.g. appliance bundles like VMware
// VCSA) — flag them clearly instead of with a Linux-centric message.
if (iso.size_bytes > 1.5 * 1024 * 1024 * 1024) {
return { ok: false, reason: 'no kernel/initrd detected; ISO too large for sanboot fallback' };
return { ok: false, reason: "not a recognized network-bootable installer (no Windows or Linux boot files found) — this image can't be PXE-booted" };
}
return { ok: true, warn: 'no kernel detected — sanboot fallback may not work' };
}
@@ -533,6 +536,9 @@
style:'display:none', id:'file'});
const prog = el('div', {class:'progress', id:'prog'}, el('div', {class:'bar', id:'bar'}));
const upMsg = el('div', {class:'msg', id:'upmsg'});
// v0.5.8: cancel button — shown only while an upload is in flight.
const cancelUpload = el('button', {class:'danger', type:'button',
style:'display:none;margin-top:12px', id:'cancel-upload'}, 'Cancel upload');
drop.onclick = () => file.click();
drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('hover'); });
@@ -575,6 +581,16 @@
};
let uploadId = null;
// v0.5.8: cancel + leave-page guard. The AbortController stops the
// in-flight chunk; the beforeunload listener warns the operator
// that navigating away aborts the upload (the server-side partial
// is then cleaned up by the DELETE in the catch below).
const ac = new AbortController();
let canceled = false;
const warnLeave = (e) => { e.preventDefault(); e.returnValue = ''; return ''; };
window.addEventListener('beforeunload', warnLeave);
cancelUpload.style.display = '';
cancelUpload.onclick = () => { canceled = true; ac.abort(); };
setStatus('Preparing upload for ' + f.name + ' (' + fmtBytes(f.size) + ')');
prog.classList.add('active');
bar.style.width = '1%';
@@ -601,6 +617,7 @@
'x-openpxe-upload-complete': complete ? 'true' : 'false',
},
body: f.slice(offset, end),
signal: ac.signal,
});
if (!r.ok) throw new Error(await failText(r));
const j = await r.json();
@@ -616,8 +633,15 @@
try { await fetch('/api/uploads/' + encodeURIComponent(uploadId), {method: 'DELETE'}); }
catch {}
}
setStatus('Upload failed: ' + (err && err.message ? err.message : String(err)), 'err');
if (canceled || (err && err.name === 'AbortError')) {
setStatus('Upload canceled — partial file discarded.', '');
} else {
setStatus('Upload failed: ' + (err && err.message ? err.message : String(err)), 'err');
}
} finally {
window.removeEventListener('beforeunload', warnLeave);
cancelUpload.style.display = 'none';
cancelUpload.onclick = null;
prog.classList.remove('active');
if (!upMsg.className.includes('ok')) bar.style.width = '0';
}
@@ -629,7 +653,11 @@
// *next* row of the table. Keeps the markup flat and avoids the
// overhead of a real modal.
const rowsAndEditors = [];
isos.forEach(i => {
// v0.5.8: list Available images alphabetically by filename
// (case-insensitive, natural numeric order) instead of newest-first.
const sortedIsos = [...isos].sort((a, b) =>
(a.filename || '').localeCompare(b.filename || '', undefined, { sensitivity: 'base', numeric: true }));
sortedIsos.forEach(i => {
const b = bootability(i, settings);
// v0.4.65: SMB userspace consumer (smbclient).
// v0.4.67: NFS back as in-process Rust client (nfs3_client).
@@ -1164,7 +1192,7 @@
diskCard,
el('div', {class:'card'}, [
el('header', {}, el('h2', {}, 'Upload ISO')),
el('div', {class:'body'}, [drop, file, prog, upMsg]),
el('div', {class:'body'}, [drop, file, prog, upMsg, cancelUpload]),
]),
// v0.5.1: SMB + NFS unified into one "Remote shares" card with a
// protocol dropdown. Backend endpoints are unchanged; this is a