Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cell/entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,13 @@ class ContainerHost {
// and BACKUP_INTEGRITY); `{full}` when it does not fit on this disk (the
// shim stops at its floor, ENOSPC, and removes what it wrote). The Rust
// side tells both from a failure that may pass (it throws).
async restore(generation, record, ms) {
// `dir`, when named, is where it goes instead of its own directory (a
// folder set aside, coming back beside the live one).
async restore(generation, record, ms, dir) {
if (generation !== this.#generation) throw new Error(`start ${generation} is not the running one`);
try {
await bounded(`the restore of ${record.dir}`, ms, (signal) => this.#backups.restore(record, { signal }));
const options = dir ? { dir } : {};
await bounded(`the restore of ${dir || record.dir}`, ms, (signal) => this.#backups.restore(record, { ...options, signal }));
return null;
} catch (e) {
if (SandboxBackupError.is(e) && (e.code === "BACKUP_NOT_FOUND" || e.code === "BACKUP_INTEGRITY")) {
Expand Down
10 changes: 10 additions & 0 deletions cell/shell-tests/computer-aside.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@ test("each folder set aside is told, oldest first; small ones in MB, an unknown
`Your folder from ${day} (2.5 GB) didn't fit, so your computer started without it. It's kept safe.`,
);
});

test("its two ways out: one folder's buttons say it, several name their folders", async () => {
const { asideActionWords, asideDropAsk } = await import("../shell/computer.js");
const work = { record: "r1", dir: "/data/work", bytes: 5_887_397_516, save: 594, savedAt: at, at };
const files = { record: "r2", dir: "/data", bytes: 420_000_000, save: 590, savedAt: at, at };
assert.deepEqual(asideActionWords(work, false), ["Get it back", "Delete it"]);
assert.deepEqual(asideActionWords(work, true), [`Get back the work folder from ${day}`, `Delete the work folder from ${day}`]);
assert.deepEqual(asideActionWords(files, true), [`Get back the computer's own files from ${day}`, `Delete the computer's own files from ${day}`]);
assert.equal(asideDropAsk(work), `Delete your work folder from ${day} (5.9 GB)? It can't be brought back after.`);
});
58 changes: 56 additions & 2 deletions cell/shell/computer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ export const PHASE = { asleep: "Asleep", starting: "Starting", awake: "Awake", s
// oldest first, with the day it was saved and its saved size.
export const ASIDE_TITLE = "A folder didn't fit on your computer";
const ASIDE_FOLDER = { "/data/work": ["work folder", "it", "It's"], "/data": ["computer's own files", "them", "They're"] };
const size = (b) => (b >= 1e9 ? `${(b / 1e9).toFixed(1)} GB` : `${Math.max(1, Math.round(b / 1e6))} MB`);
const day = (ms) => new Date(ms).toLocaleDateString([], { month: "short", day: "numeric" });
// Its two ways out: back beside the live work (nothing there is replaced),
// or deleted. With several aside, each button names its folder.
export function asideActionWords(a, several) {
const [folder] = ASIDE_FOLDER[a.dir] ?? ["folder"];
return several ? [`Get back the ${folder} from ${day(a.savedAt)}`, `Delete the ${folder} from ${day(a.savedAt)}`] : ["Get it back", "Delete it"];
}
export function asideDropAsk(a) {
const [folder] = ASIDE_FOLDER[a.dir] ?? ["folder"];
return `Delete your ${folder} from ${day(a.savedAt)} (${size(a.bytes)})? It can't be brought back after.`;
}
export function setAsideSaid(aside) {
const size = (b) => (b >= 1e9 ? `${(b / 1e9).toFixed(1)} GB` : `${Math.max(1, Math.round(b / 1e6))} MB`);
const day = (ms) => new Date(ms).toLocaleDateString([], { month: "short", day: "numeric" });
return aside
.map((a) => {
const [folder, it, kept] = ASIDE_FOLDER[a.dir] ?? ["folder", "it", "It's"];
Expand Down Expand Up @@ -133,6 +143,20 @@ export function createComputer({ api, el, seg, named, section, line, actionLine,
actions.append(later);
} else if (n.kind === "set_aside") {
say(ASIDE_TITLE, setAsideSaid(n.aside));
for (const a of n.aside) {
const [backWords, dropWords] = asideActionWords(a, n.aside.length > 1);
const back = el("button", "primary", backWords);
back.type = "button";
back.dataset.action = "aside-back";
back.dataset.record = a.record;
back.onclick = () => asideBack(a, back);
const drop = el("button", null, dropWords);
drop.type = "button";
drop.dataset.action = "aside-drop";
drop.dataset.record = a.record;
drop.onclick = () => asideDrop(a, drop);
actions.append(back, drop);
}
} else if (n.kind === "deploying") {
copy.append(el("strong", null, "Updating — back in a moment"));
} else if (n.kind === "refresh") {
Expand Down Expand Up @@ -198,6 +222,36 @@ export function createComputer({ api, el, seg, named, section, line, actionLine,
computerIs(await api("GET", `/api/computers/${seg(c.computer)}`).catch(() => null));
}
}
// A folder set aside, back beside the live work (it wakes the computer
// if it sleeps; a big one takes minutes), or deleted once they confirm.
// What did not work says why under its button.
async function asideBack(a, button) {
const c = computer();
if (!c) return;
const label = button.textContent;
button.disabled = true;
button.textContent = "Getting it back…";
try {
const r = await api("POST", `/api/computers/${seg(c.computer)}/aside/${seg(a.record)}/back`, {});
computerIs(r.view);
} catch (e) {
button.disabled = false;
button.textContent = label;
const said = button.parentElement.querySelector(".form-error") ?? el("p", "form-error");
said.textContent = e.message;
button.parentElement.append(said);
}
}
async function asideDrop(a, button) {
const c = computer();
if (!c || !confirm(asideDropAsk(a))) return;
button.disabled = true;
try {
computerIs(await api("DELETE", `/api/computers/${seg(c.computer)}/aside/${seg(a.record)}`));
} catch {
computerIs(await api("GET", `/api/computers/${seg(c.computer)}`).catch(() => null));
}
}
// Later: this update is told no more, anywhere, until another is ready.
async function updateLater(version, button) {
const c = computer();
Expand Down
Loading
Loading