Open a folder chip on its folder, in the folder that really holds it

A bot's folder chip went to bare /admin/files instead of the folder it
named, and even once there the computer volume pointed at the wrong
directory, so the folder a bot linked to looked empty.

The chip lost its folder because folderRefUrl only accepted a ref's
volume name (computer) while the chip hands it the browser's
(computers), so storageVolumeFromRef found nothing and the whole query
was dropped. Both spellings resolve now, through parseStorageSpec, so a
bad owner falls back cleanly instead of emitting owner=0.

The deeper bug: a computer's home was assumed to be
DATA_DIR/computers/<id>, but on a bind mount it can be anywhere on the
docker host, and a computer the app didn't provision (the main one on a
server reached at MAIN_COMPUTER_URL, i.e. a dev server outside docker)
records nothing at all. resolveComputerHomeDir holds the precedence:
the directory provisionComputer actually bound in
(containers.metadata.data_dir), else the new MAIN_COMPUTER_DATA_DIR app
setting for the main computer, else the conventional folder.

A path that isn't on disk also no longer reads as an empty folder:
listStorageEntries reports it as missing and the page says so, so a
stale folder link can't look like a folder nothing has written yet.
This commit is contained in:
2026-10-22 10:57:14 +01:00
parent 841149921b
commit fd75111bb1
13 changed files with 329 additions and 17 deletions
@@ -70,9 +70,11 @@ export default async function listStorageEntries({
/**
* A folder nothing has written to yet (a computer's home before it
* starts, a bot's before its first file) shows as empty rather than as
* an error.
* an error. `missing` keeps that apart from a path that isn't there at
* all, which a stale folder link lands on (the page says so, instead of
* claiming the folder is empty).
*/
if (!existsSync(absolute)) return { entries: [], total: 0, offset };
if (!existsSync(absolute)) return { entries: [], total: 0, offset, missing: true };
const rows = readdirSync(absolute, { withFileTypes: true });
const indexed = await botFileRows({ resolved, paths: rows.map((row) => joinPath({ relative, name: row.name })) });
@@ -145,6 +147,7 @@ export default async function listStorageEntries({
entries: entries.slice(offset, offset + AppData.StorageBrowserPageSize),
total: entries.length,
offset,
missing: false,
};
}
@@ -10,6 +10,8 @@ import { storageVolumeLabel, type StorageSpec } from "@/src/utils/storage-volume
import grabCrewRole from "../db/access/grab-crew-role";
import grabUserBot from "../db/bots/grab-user-bot";
import grabUserComputer from "../db/computers/grab-user-computer";
import resolveComputerHomeDir from "../db/computers/resolve-computer-home-dir";
import grabEnv from "../db/envs/grab-env";
import grabUserWorkspace from "../db/workspaces/grab-user-workspace";
type Params = {
@@ -69,10 +71,19 @@ async function storageRoot({
? path.join(DATA_DIR, AppData.WorkspaceDataDir, String(spec.owner_id))
: null;
case "computers":
return (await grabUserComputer({ computer_id: spec.owner_id, user_id }))
? path.join(DATA_DIR, AppData.ComputerDataDir, String(spec.owner_id))
: null;
case "computers": {
const computer = await grabUserComputer({ computer_id: spec.owner_id, user_id });
if (!computer) return null;
return resolveComputerHomeDir({
computer_id: spec.owner_id,
kind: computer.kind,
metadata: await computerContainerMetadata({ container_id: computer.container_id }),
main_data_dir: await grabEnv({ key: "MAIN_COMPUTER_DATA_DIR" }),
data_dir: DATA_DIR,
});
}
case "media":
/**
@@ -100,6 +111,23 @@ async function storageRoot({
}
}
/**
* A computer's container `metadata`, where `provisionComputer` recorded
* the directory it bind-mounted as the computer's home. Null when the app
* didn't create the container (a computer reached at
* `MAIN_COMPUTER_URL`), which is what `MAIN_COMPUTER_DATA_DIR` is for.
*/
async function computerContainerMetadata({ container_id }: { container_id?: number | "" }) {
if (!container_id) return null;
const res = await BunSQLite.sql<{ metadata?: string | null }>({
sql: `SELECT metadata FROM containers WHERE id = ? LIMIT 1`,
values: [container_id],
});
return res.payload?.[0]?.metadata || null;
}
/**
* What the page calls the folder: the bot's, workspace's or computer's
* own name where one is known.