Files
xteink-web-emulator/web/sdstore.js
T
evan 78f181e924 feat(web): improve SD file browser
Add directory navigation, folder creation, inline deletion, clear controls, and a focused editor view. Include the opt-in FAT16 vvfat experiment and fail-fast browser diagnostics; raw FAT32 remains the default.
2026-07-21 15:37:00 -04:00

84 lines
2.8 KiB
JavaScript

// OPFS-backed SD card store — the source of truth for /sdcard contents.
// It persists across page reloads, so "edit files, then reboot to apply" works
// by reloading the page: edits survive in OPFS and are seeded into the guest's
// MEMFS /sdcard on the next boot. Guest-side writes remain ephemeral.
const SD_DIR = 'sdcard';
async function sdRoot() {
const root = await navigator.storage.getDirectory();
return root.getDirectoryHandle(SD_DIR, { create: true });
}
async function resolveParent(path, create) {
const parts = path.split('/').filter(Boolean);
const filename = parts.pop();
let dir = await sdRoot();
for (const part of parts) {
dir = await dir.getDirectoryHandle(part, { create });
}
return [dir, filename];
}
export async function listSdFiles() {
const entries = [];
const walk = async (dir, prefix) => {
for await (const [name, handle] of dir.entries()) {
const path = prefix ? `${prefix}/${name}` : name;
if (handle.kind === 'directory') {
entries.push({ path: `${path}/`, directory: true });
await walk(handle, path);
} else {
entries.push({ path, size: (await handle.getFile()).size });
}
}
};
await walk(await sdRoot(), '');
entries.sort((a, b) => a.path.localeCompare(b.path));
return entries;
}
export async function readSdFile(path) {
const [dir, name] = await resolveParent(path, false);
const handle = await dir.getFileHandle(name);
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
}
export async function writeSdFile(path, data) {
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
const [dir, name] = await resolveParent(path, true);
const handle = await dir.getFileHandle(name, { create: true });
const writable = await handle.createWritable();
await writable.write(bytes);
await writable.close();
}
// ponytail: empty dirs live only in OPFS; readAllSdFiles skips them, so they vanish on reboot. Add a placeholder file if guest-side empty dirs ever matter.
export async function mkdirSd(path) {
let dir = await sdRoot();
for (const part of path.split('/').filter(Boolean)) {
dir = await dir.getDirectoryHandle(part, { create: true });
}
}
export async function deleteSdFile(path) {
const [dir, name] = await resolveParent(path, false);
await dir.removeEntry(name, { recursive: true });
}
export async function clearSdFiles() {
const root = await navigator.storage.getDirectory();
await root.removeEntry(SD_DIR, { recursive: true });
}
// Flatten to [{ path, bytes }] for seeding the guest's MEMFS /sdcard at boot.
export async function readAllSdFiles() {
const out = [];
for (const entry of await listSdFiles()) {
if (!entry.directory) {
out.push({ path: entry.path, bytes: await readSdFile(entry.path) });
}
}
return out;
}