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
@@ -0,0 +1,70 @@
import { describe, expect, test } from "bun:test";
import resolveComputerHomeDir from "./resolve-computer-home-dir";
const data_dir = "/data";
describe("resolveComputerHomeDir", () => {
test("uses the directory the app bind-mounted", () => {
/**
* The truth for a container the app provisioned: on a bind mount
* that can be anywhere on the docker host, not under DATA_DIR.
*/
expect(
resolveComputerHomeDir({
computer_id: 2,
kind: "sandbox",
metadata: JSON.stringify({ data_dir: "/srv/homes/2", host_dir: "/host/2" }),
main_data_dir: "/wrong",
data_dir,
}),
).toBe("/srv/homes/2");
});
test("falls back to MAIN_COMPUTER_DATA_DIR for the main computer", () => {
expect(
resolveComputerHomeDir({
computer_id: 1,
kind: "main",
metadata: null,
main_data_dir: "/media/docker/aio-sandbox/data/home",
data_dir,
}),
).toBe("/media/docker/aio-sandbox/data/home");
});
test("leaves a sandbox alone whatever the main setting says", () => {
expect(
resolveComputerHomeDir({
computer_id: 3,
kind: "sandbox",
metadata: null,
main_data_dir: "/media/docker/aio-sandbox/data/home",
data_dir,
}),
).toBe("/data/computers/3");
});
test("uses the conventional folder when nothing is recorded", () => {
expect(resolveComputerHomeDir({ computer_id: 4, kind: "main", data_dir })).toBe(
"/data/computers/4",
);
});
test("ignores an empty or unreadable record", () => {
expect(
resolveComputerHomeDir({ computer_id: 5, metadata: JSON.stringify({ data_dir: " " }), data_dir }),
).toBe("/data/computers/5");
expect(resolveComputerHomeDir({ computer_id: 5, metadata: "not json", data_dir })).toBe(
"/data/computers/5",
);
expect(resolveComputerHomeDir({ computer_id: 5, metadata: null, data_dir })).toBe(
"/data/computers/5",
);
});
test("normalizes a recorded relative path", () => {
expect(
resolveComputerHomeDir({ computer_id: 6, metadata: JSON.stringify({ data_dir: "/srv/a/../b" }), data_dir }),
).toBe("/srv/b");
});
});
@@ -0,0 +1,63 @@
import path from "node:path";
import { AppData } from "@/src/data/app-data";
import parseJobJson from "@/src/bots-queue/parse-job-json";
type Params = {
computer_id: number | string;
/**
* The computer's kind: only the install's main computer can be
* reached at `MAIN_COMPUTER_URL`, so only it uses
* `MAIN_COMPUTER_DATA_DIR`.
*/
kind?: string | null;
/**
* Its container's `metadata`, as `provisionComputer` wrote it (the
* `data_dir` it bound into the container).
*/
metadata?: string | null;
/**
* What `MAIN_COMPUTER_DATA_DIR` is set to, already read
* (`grabEnv`).
*/
main_data_dir?: string | null;
/**
* The data volume's root as this app sees it (`DATA_DIR`).
*/
data_dir: string;
};
/**
* Where a computer's home (`AppData.ComputerHomeDir` in its container)
* actually is on this app's disk, for the files browser and for anything
* else that has to read a computer's files.
*
* A container the app provisioned recorded the directory it bind-mounted
* (`containers.metadata.data_dir`), which is the truth: on a bind mount
* that path can be anywhere the docker host allows. A computer the app
* didn't provision has no record. That is the install's main computer on a
* server reached at `MAIN_COMPUTER_URL` (a dev server outside docker),
* whose sandbox keeps its home wherever its compose file says, so
* `MAIN_COMPUTER_DATA_DIR` names that directory for this app. Without
* either, the conventional place every app-provisioned computer uses:
* `DATA_DIR/<AppData.ComputerDataDir>/<id>`.
*
* Kept apart from the docker and database calls so the precedence is
* testable on its own.
*/
export default function resolveComputerHomeDir({
computer_id,
kind,
metadata,
main_data_dir,
data_dir,
}: Params) {
const recorded = parseJobJson({ text: metadata || "" })?.data_dir;
if (typeof recorded == "string" && recorded.trim()) return path.resolve(recorded.trim());
const external = kind == "main" ? main_data_dir?.trim() : "";
if (external) return path.resolve(external);
return path.join(data_dir, AppData.ComputerDataDir, String(computer_id));
}