Serve every media file privately at /media/<id>
There is no public media any more. `/media/public/<id>` served a media row with no session at all, so anyone with the id could read a file. The public/private split is gone: the path is `/media/<id>` (and `/media/<id>/thumbnail`), every request is signed in first, and `grabUserMedia` decides whether the user may see it: their own upload, a bot's or workspace's picture or file they reach, or a file on a task they reach. Anything else is a 404 before a path is built. The `media.is_private` column and the two per-user directories went with it: uploads live in `media/<user_id>/` and there is nothing to pick between. The files browser's media volume is now the viewer's own folder whatever their role. Bots also say which of the two things they mean. A `[file:ID]` ref is a saved file and its chip opens the bytes; a folder is a new `[folder:volume:owner:path]` ref (`src/utils/folder-ref.ts`) whose chip opens the files browser there and downloads nothing. The old `[path:...]` ref and the extension sniffing that guessed file from folder are gone, so `import_file` names the folder holding a file it copied rather than the file itself. The prompts say what the Assistant got wrong: list before importing or saving, one copy per artifact, never split a file into parts unless asked, never claim the store is empty without a fresh list, and never invent a size cap. A re-import now says the path already existed instead of silently overwriting and reporting a fresh copy.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* How a bot names one of its files in a message: `[file:12]`. The chat
|
||||
* turns it into a link to the file (`linkComputerRefs`), which a chip
|
||||
* renders with the file's name.
|
||||
* renders with the file's name and opens the bytes at `/media/<id>`.
|
||||
*
|
||||
* A file ref is always a file. A folder is named with a `folderRef`
|
||||
* (`[folder:bot:3:reports]`), which opens the files browser instead and
|
||||
* never downloads anything.
|
||||
*/
|
||||
export default function fileRef({ id }: { id: number | string }) {
|
||||
return `[file:${id}]`;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import folderRef, { folderRefHref, folderRefUrl, parseFolderRefHref } from "./folder-ref";
|
||||
import linkComputerRefs from "./link-computer-refs";
|
||||
|
||||
describe("folderRef", () => {
|
||||
test("writes a folder, never a file", () => {
|
||||
expect(folderRef({ volume: "computer", owner_id: 3, path: "seo/coderank/raw" })).toBe(
|
||||
"[folder:computer:3:seo/coderank/raw]",
|
||||
);
|
||||
expect(folderRef({ volume: "computer", owner_id: 3, path: "/seo/raw/ " })).toBe(
|
||||
"[folder:computer:3:seo/raw]",
|
||||
);
|
||||
expect(folderRef({ volume: "bot", owner_id: 1 })).toBe("[folder:bot:1]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("folderRefHref", () => {
|
||||
test("encodes the path into the link", () => {
|
||||
expect(folderRefHref({ volume: "computer", owner_id: 3, path: "seo/raw mes" })).toBe(
|
||||
`#folder-${encodeURIComponent("computer:3:seo/raw mes")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("reads back what it wrote", () => {
|
||||
const href = folderRefHref({ volume: "computer", owner_id: 3, path: "seo/raw mes/#1" });
|
||||
|
||||
expect(parseFolderRefHref({ href })).toEqual({
|
||||
spec: { volume: "computers", owner_id: 3 },
|
||||
path: "seo/raw mes/#1",
|
||||
});
|
||||
});
|
||||
|
||||
test("maps the ref's volume name to the browser's", () => {
|
||||
expect(parseFolderRefHref({ href: folderRefHref({ volume: "bot", owner_id: 1, path: "a" }) })).toEqual({
|
||||
spec: { volume: "bots", owner_id: 1 },
|
||||
path: "a",
|
||||
});
|
||||
expect(parseFolderRefHref({ href: folderRefHref({ volume: "workspace", owner_id: 2 }) })).toEqual({
|
||||
spec: { volume: "workspaces", owner_id: 2 },
|
||||
path: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("refuses an unknown volume, a bad owner, a file ref or nothing", () => {
|
||||
expect(parseFolderRefHref({ href: "#folder-secrets:1:a" })).toBeNull();
|
||||
expect(parseFolderRefHref({ href: "#folder-computer:abc:a" })).toBeNull();
|
||||
expect(parseFolderRefHref({ href: "#file-12" })).toBeNull();
|
||||
expect(parseFolderRefHref({ href: undefined })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("folderRefUrl", () => {
|
||||
test("opens the files browser on the folder", () => {
|
||||
expect(folderRefUrl({ volume: "computer", owner_id: 3, path: "seo/raw" })).toBe(
|
||||
`/admin/files?volume=computers&owner=3&path=seo%2Fraw`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("linkComputerRefs", () => {
|
||||
test("turns a folder ref into a link", () => {
|
||||
const linked = linkComputerRefs({ text: "In [folder:computer:3:seo/raw]." });
|
||||
|
||||
expect(linked).toContain("(#folder-");
|
||||
expect(parseFolderRefHref({ href: linked.match(/\((#folder-[^)]+)\)/)?.[1] })).toEqual({
|
||||
spec: { volume: "computers", owner_id: 3 },
|
||||
path: "seo/raw",
|
||||
});
|
||||
});
|
||||
|
||||
test("leaves a file ref to the file chip", () => {
|
||||
const linked = linkComputerRefs({ text: "The report: [file:12]." });
|
||||
|
||||
expect(linked).toContain("(#file-12)");
|
||||
expect(parseFolderRefHref({ href: linked.match(/\((#file-[^)]+)\)/)?.[1] })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* How a bot names a folder on the app's data volume in a message:
|
||||
* `[folder:bot:3:reports]`, `[folder:computer:3:seo/coderank/raw]`,
|
||||
* `[folder:workspace:2]`.
|
||||
*
|
||||
* A folder ref is always a folder: the chat turns it into a chip that opens
|
||||
* the files browser (`/admin/files`) at that folder, and nothing is
|
||||
* downloaded. A file the bot saved is named with a `[file:ID]` ref instead
|
||||
* (`fileRef`), which is what downloads. There is no guessing from the
|
||||
* name's extension: the ref says which one it is.
|
||||
*/
|
||||
import {
|
||||
StorageBrowserPath,
|
||||
storageSpecHref,
|
||||
storageVolumeFromRef,
|
||||
type StorageSpec,
|
||||
} from "./storage-volumes";
|
||||
|
||||
/**
|
||||
* The link target a folder ref becomes, e.g.
|
||||
* `#folder-bot%3A3%3Areports`.
|
||||
*/
|
||||
export const FolderRefHrefPrefix = "#folder-";
|
||||
|
||||
type RefParams = {
|
||||
/**
|
||||
* The volume by the name a ref uses: `bot`, `workspace`, `computer`,
|
||||
* `media`, `mcp` or `db`.
|
||||
*/
|
||||
volume: string;
|
||||
owner_id: number | string;
|
||||
path?: string | null;
|
||||
};
|
||||
|
||||
function cleanPath({ path }: { path?: string | null }) {
|
||||
return (path || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* How a bot names a folder in a message. The path is written as it is; the
|
||||
* chat encodes it into the link.
|
||||
*/
|
||||
export default function folderRef({ volume, owner_id, path }: RefParams) {
|
||||
const clean = cleanPath({ path });
|
||||
|
||||
return clean
|
||||
? `[folder:${volume}:${owner_id}:${clean}]`
|
||||
: `[folder:${volume}:${owner_id}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The href a folder ref becomes: everything after `#folder-`, URI encoded,
|
||||
* so spaces, `#` and `:` in a path survive markdown.
|
||||
*/
|
||||
export function folderRefHref({ volume, owner_id, path }: RefParams) {
|
||||
const clean = cleanPath({ path });
|
||||
|
||||
return `${FolderRefHrefPrefix}${encodeURIComponent(
|
||||
clean ? `${volume}:${owner_id}:${clean}` : `${volume}:${owner_id}`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a folder ref's link back into a folder and a path inside it, or
|
||||
* null when it names a volume that isn't one (or has no owner).
|
||||
*/
|
||||
export function parseFolderRefHref({
|
||||
href,
|
||||
}: {
|
||||
href?: string | null;
|
||||
}): { spec: StorageSpec; path: string } | null {
|
||||
if (!href?.startsWith(FolderRefHrefPrefix)) return null;
|
||||
|
||||
let decoded = "";
|
||||
|
||||
try {
|
||||
decoded = decodeURIComponent(href.slice(FolderRefHrefPrefix.length));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [name, raw_owner, ...rest] = decoded.split(":");
|
||||
const volume = storageVolumeFromRef({ name });
|
||||
const owner_id = Number(raw_owner);
|
||||
|
||||
if (!volume) return null;
|
||||
|
||||
if (!Number.isFinite(owner_id) || owner_id < 0) return null;
|
||||
|
||||
return {
|
||||
spec: { volume, owner_id: Math.trunc(owner_id) },
|
||||
path: rest.join(":").replace(/^\/+|\/+$/g, ""),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a folder ref opens: the files browser, on that folder. A volume the
|
||||
* browser doesn't know lands on its first folder.
|
||||
*/
|
||||
export function folderRefUrl({ volume, owner_id, path }: RefParams) {
|
||||
const known = storageVolumeFromRef({ name: volume });
|
||||
|
||||
if (!known) return StorageBrowserPath;
|
||||
|
||||
return storageSpecHref({ spec: { volume: known, owner_id: Number(owner_id) }, path });
|
||||
}
|
||||
+10
-14
@@ -16,26 +16,22 @@ export default function grabDirNames(params?: Params) {
|
||||
const MEDIA_RELATIVE_DIR = "media";
|
||||
const MEDIA_DIR = path.join(DATA_DIR, MEDIA_RELATIVE_DIR);
|
||||
|
||||
const USER_MEDIA_PUBLIC_DIR = params?.user?.id
|
||||
? path.join(MEDIA_DIR, String(params.user.id), "public")
|
||||
/**
|
||||
* A user's uploads: one directory, every file private (there is no
|
||||
* public media any more).
|
||||
*/
|
||||
const USER_MEDIA_DIR = params?.user?.id
|
||||
? path.join(MEDIA_DIR, String(params.user.id))
|
||||
: undefined;
|
||||
const USER_MEDIA_PUBLIC_RELATIVE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_RELATIVE_DIR, String(params.user.id), "public")
|
||||
: undefined;
|
||||
const USER_MEDIA_PRIVATE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_DIR, String(params.user.id), "private")
|
||||
: undefined;
|
||||
const USER_MEDIA_PRIVATE_RELATIVE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_RELATIVE_DIR, String(params.user.id), "private")
|
||||
const USER_MEDIA_RELATIVE_DIR = params?.user?.id
|
||||
? path.join(MEDIA_RELATIVE_DIR, String(params.user.id))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ROOT_DIR,
|
||||
MEDIA_DIR,
|
||||
USER_MEDIA_PUBLIC_DIR,
|
||||
USER_MEDIA_PRIVATE_DIR,
|
||||
USER_MEDIA_DIR,
|
||||
USER_MEDIA_RELATIVE_DIR,
|
||||
DATA_DIR,
|
||||
USER_MEDIA_PUBLIC_RELATIVE_DIR,
|
||||
USER_MEDIA_PRIVATE_RELATIVE_DIR,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,19 +31,23 @@ export const WorkspaceRefHrefPrefix = "#workspace-";
|
||||
export const FileRefHrefPrefix = "#file-";
|
||||
|
||||
/**
|
||||
* The link target a path ref (`pathRef`) becomes, e.g.
|
||||
* `#path-computer%3A3%3Aseo%2Fraw`. The chat renders it as a chip that
|
||||
* The link target a folder ref (`folderRef`) becomes, e.g.
|
||||
* `#folder-computer%3A3%3Aseo%2Fraw`. The chat renders it as a chip that
|
||||
* opens that folder in the files browser.
|
||||
*/
|
||||
export const PathRefHrefPrefix = "#path-";
|
||||
export const FolderRefHrefPrefix = "#folder-";
|
||||
|
||||
/**
|
||||
* Turns every computer ref (`[computer:3]`) and take-over ref
|
||||
* (`[take-over:3:browser]`), bot ref (`[bot:3]`), workspace ref
|
||||
* (`[workspace:2]`), file ref (`[file:12]`) and path ref
|
||||
* (`[path:computer:3:seo/raw]`) in a message into a markdown link, so it
|
||||
* survives markdown rendering as a link the chat can swap for a button or
|
||||
* chip.
|
||||
* (`[workspace:2]`), file ref (`[file:12]`) and folder ref
|
||||
* (`[folder:computer:3:seo/raw]`) in a message into a markdown link, so it
|
||||
* survives markdown rendering as a link the chat can swap for a button,
|
||||
* chip or download.
|
||||
*
|
||||
* A file ref and a folder ref are different things and are never guessed
|
||||
* from a name: `[file:12]` is a saved file (its chip downloads it),
|
||||
* `[folder:...]` a folder (its chip opens the files browser there).
|
||||
*/
|
||||
export default function linkComputerRefs({ text }: { text: string }) {
|
||||
return text
|
||||
@@ -61,10 +65,10 @@ export default function linkComputerRefs({ text }: { text: string }) {
|
||||
(_, id: string) => `[workspace ${id}](${WorkspaceRefHrefPrefix}${id})`,
|
||||
)
|
||||
.replace(/\[file:(\d+)\](?!\()/g, (_, id: string) => `[file ${id}](${FileRefHrefPrefix}${id})`)
|
||||
.replace(/\[path:([a-z]+):(\d+):([^\]\n]+)\](?!\()/g, (_, volume: string, owner: string, path: string) =>
|
||||
`[${path}](${PathRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}:${path}`)})`,
|
||||
.replace(/\[folder:([a-z]+):(\d+):([^\]\n]+)\](?!\()/g, (_, volume: string, owner: string, path: string) =>
|
||||
`[${path}](${FolderRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}:${path}`)})`,
|
||||
)
|
||||
.replace(/\[path:([a-z]+):(\d+)\](?!\()/g, (_, volume: string, owner: string) =>
|
||||
`[${volume} ${owner}](${PathRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}`)})`,
|
||||
.replace(/\[folder:([a-z]+):(\d+)\](?!\()/g, (_, volume: string, owner: string) =>
|
||||
`[${volume} ${owner}](${FolderRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}`)})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("needsPrecachePage", () => {
|
||||
expect(needsPrecachePage({ ...page, pathname: "/api/admin/bots/send-message" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/.bunext/public/vendor/react.js" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/computers/2/vnc/vnc.html" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/media/1/private/a" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/media/1/thumbnail" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/build-info.json" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/sw.js" })).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import pathRef, { parsePathRefHref, pathRefHref, pathRefUrl } from "./path-ref";
|
||||
import linkComputerRefs from "./link-computer-refs";
|
||||
|
||||
describe("pathRef", () => {
|
||||
test("writes a folder and a file", () => {
|
||||
expect(pathRef({ volume: "computer", owner_id: 3, path: "seo/coderank/raw" })).toBe(
|
||||
"[path:computer:3:seo/coderank/raw]",
|
||||
);
|
||||
expect(pathRef({ volume: "computer", owner_id: 3, path: "/seo/raw.json " })).toBe(
|
||||
"[path:computer:3:seo/raw.json]",
|
||||
);
|
||||
expect(pathRef({ volume: "bot", owner_id: 1 })).toBe("[path:bot:1]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pathRefHref", () => {
|
||||
test("encodes the path into the link", () => {
|
||||
expect(pathRefHref({ volume: "computer", owner_id: 3, path: "seo/raw mes" })).toBe(
|
||||
`#path-${encodeURIComponent("computer:3:seo/raw mes")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("reads back what it wrote", () => {
|
||||
const href = pathRefHref({ volume: "computer", owner_id: 3, path: "seo/raw mes/#1" });
|
||||
|
||||
expect(parsePathRefHref({ href })).toEqual({
|
||||
spec: { volume: "computers", owner_id: 3 },
|
||||
path: "seo/raw mes/#1",
|
||||
});
|
||||
});
|
||||
|
||||
test("maps the ref's volume name to the browser's", () => {
|
||||
expect(parsePathRefHref({ href: pathRefHref({ volume: "bot", owner_id: 1, path: "a" }) })).toEqual({
|
||||
spec: { volume: "bots", owner_id: 1 },
|
||||
path: "a",
|
||||
});
|
||||
expect(
|
||||
parsePathRefHref({ href: pathRefHref({ volume: "workspace", owner_id: 2 }) }),
|
||||
).toEqual({
|
||||
spec: { volume: "workspaces", owner_id: 2 },
|
||||
path: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("refuses an unknown volume or owner", () => {
|
||||
expect(parsePathRefHref({ href: "#path-secrets:1:a" })).toBeNull();
|
||||
expect(parsePathRefHref({ href: "#path-computer:abc:a" })).toBeNull();
|
||||
expect(parsePathRefHref({ href: "#file-12" })).toBeNull();
|
||||
expect(parsePathRefHref({ href: undefined })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("pathRefUrl", () => {
|
||||
test("opens the files browser on the folder", () => {
|
||||
expect(pathRefUrl({ volume: "computer", owner_id: 3, path: "seo/raw" })).toBe(
|
||||
`/admin/files?volume=computers&owner=3&path=seo%2Fraw`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("linkComputerRefs", () => {
|
||||
test("turns a path ref into a link", () => {
|
||||
const linked = linkComputerRefs({ text: "In [path:computer:3:seo/raw]." });
|
||||
|
||||
expect(linked).toContain("(#path-");
|
||||
expect(parsePathRefHref({ href: linked.match(/\((#path-[^)]+)\)/)?.[1] })).toEqual({
|
||||
spec: { volume: "computers", owner_id: 3 },
|
||||
path: "seo/raw",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Where a bot names a folder or file on the app's data volume in a
|
||||
* message: `[path:computer:3:seo/coderank/raw]`. The chat turns it into a
|
||||
* chip that opens it in the files browser (`/admin/files`), which is how a
|
||||
* bot points the user at something it left on a computer (raw evidence,
|
||||
* a crawl, a report it never copied) without importing it first.
|
||||
*/
|
||||
import { StorageBrowserPath, storageSpecHref, storageVolumeFromRef, type StorageSpec } from "./storage-volumes";
|
||||
|
||||
/**
|
||||
* The link target a path ref becomes, e.g. `#path-computer%3A3%3Aseo%2Fraw`.
|
||||
*/
|
||||
export const PathRefHrefPrefix = "#path-";
|
||||
|
||||
type RefParams = {
|
||||
/**
|
||||
* The volume by the name a ref uses: `bot`, `workspace`, `computer`,
|
||||
* `media`, `mcp` or `db`.
|
||||
*/
|
||||
volume: string;
|
||||
owner_id: number | string;
|
||||
path?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* How a bot names a folder or file in a message. The path is written as it
|
||||
* is; the chat encodes it into the link.
|
||||
*/
|
||||
export default function pathRef({ volume, owner_id, path }: RefParams) {
|
||||
const clean = (path || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
|
||||
return clean
|
||||
? `[path:${volume}:${owner_id}:${clean}]`
|
||||
: `[path:${volume}:${owner_id}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The href a path ref becomes: everything after `#path-`, URI encoded, so
|
||||
* spaces, `#` and `:` in a path survive markdown.
|
||||
*/
|
||||
export function pathRefHref({ volume, owner_id, path }: RefParams) {
|
||||
const clean = (path || "").trim().replace(/^\/+|\/+$/g, "");
|
||||
|
||||
return `${PathRefHrefPrefix}${encodeURIComponent(
|
||||
clean ? `${volume}:${owner_id}:${clean}` : `${volume}:${owner_id}`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a path ref's link back into a folder and a path inside it, or null
|
||||
* when it names a volume that isn't one (or has no owner).
|
||||
*/
|
||||
export function parsePathRefHref({
|
||||
href,
|
||||
}: {
|
||||
href?: string | null;
|
||||
}): { spec: StorageSpec; path: string } | null {
|
||||
if (!href?.startsWith(PathRefHrefPrefix)) return null;
|
||||
|
||||
const decoded = decodeURIComponent(href.slice(PathRefHrefPrefix.length));
|
||||
const [name, raw_owner, ...rest] = decoded.split(":");
|
||||
const volume = storageVolumeFromRef({ name });
|
||||
const owner_id = Number(raw_owner);
|
||||
|
||||
if (!volume) return null;
|
||||
|
||||
if (!Number.isFinite(owner_id) || owner_id < 0) return null;
|
||||
|
||||
return {
|
||||
spec: { volume, owner_id: Math.trunc(owner_id) },
|
||||
path: rest.join("/").replace(/^\/+|\/+$/g, ""),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a path ref opens: the files browser, on that folder. A volume the
|
||||
* browser doesn't know lands on its first folder.
|
||||
*/
|
||||
export function pathRefUrl({ volume, owner_id, path }: RefParams) {
|
||||
const known = storageVolumeFromRef({ name: volume });
|
||||
|
||||
if (!known) return StorageBrowserPath;
|
||||
|
||||
return storageSpecHref({ spec: { volume: known, owner_id: Number(owner_id) }, path });
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import profileImageUrls from "./profile-image-urls";
|
||||
describe("profileImageUrls", () => {
|
||||
test("serves a picture privately, with a thumbnail", () => {
|
||||
expect(profileImageUrls({ media_id: 7 })).toEqual({
|
||||
src: "/media/private/7",
|
||||
thumbnail_src: "/media/private/7/thumbnail",
|
||||
src: "/media/7",
|
||||
thumbnail_src: "/media/7/thumbnail",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ type Params = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a bot's or workspace's picture is served (a private image, so
|
||||
* only its owner can load it). Empty when it has none.
|
||||
* Where a bot's or workspace's picture is served (`/media/<id>`, private,
|
||||
* so only someone who reaches the bot or workspace can load it). Empty when
|
||||
* it has none.
|
||||
*/
|
||||
export default function profileImageUrls({ media_id }: Params) {
|
||||
if (!media_id) return {};
|
||||
|
||||
const src = `/media/private/${media_id}`;
|
||||
const src = `/media/${media_id}`;
|
||||
|
||||
return { src, thumbnail_src: `${src}/thumbnail` };
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("storageSpecHref", () => {
|
||||
});
|
||||
|
||||
describe("volume names", () => {
|
||||
test("maps a path ref's name to its volume", () => {
|
||||
test("maps a folder ref's name to its volume", () => {
|
||||
expect(storageVolumeFromRef({ name: "computer" })).toBe("computers");
|
||||
expect(storageVolumeFromRef({ name: "bot" })).toBe("bots");
|
||||
expect(storageVolumeFromRef({ name: "nope" })).toBeNull();
|
||||
|
||||
@@ -12,7 +12,7 @@ export const StorageVolumeDefs = [
|
||||
value: "bots",
|
||||
title: "Bots",
|
||||
/**
|
||||
* How a path ref names it (`[path:bot:3:reports]`).
|
||||
* How a folder ref names it (`[folder:bot:3:reports]`).
|
||||
*/
|
||||
ref: "bot",
|
||||
owner_kind: "bot",
|
||||
@@ -84,7 +84,7 @@ export function isStorageVolume(volume?: string | null): volume is StorageVolume
|
||||
}
|
||||
|
||||
/**
|
||||
* The volume a path ref names (`bot`, `computer`, ...), or null.
|
||||
* The volume a folder ref names (`bot`, `computer`, ...), or null.
|
||||
*/
|
||||
export function storageVolumeFromRef({ name }: { name?: string | null }): StorageVolume | null {
|
||||
const def = StorageVolumeDefs.find((it) => it.ref == name);
|
||||
|
||||
Reference in New Issue
Block a user