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:
@@ -47,7 +47,9 @@ function fetchFile({
|
||||
|
||||
/**
|
||||
* A file a bot named in a message (`fileRef`): the name it is saved under,
|
||||
* linked to its bytes. The conversation's files come with the page, so most
|
||||
* linked to its bytes at `/media/<id>` (private: only someone who reaches
|
||||
* the file's folder can load it). A folder the bot named is a `folderRef`
|
||||
* instead, and its chip opens the files browser. The conversation's files come with the page, so most
|
||||
* chips need no request; one that isn't among them (older than the page's
|
||||
* first page) is fetched by id. A file the user doesn't reach, or one
|
||||
* deleted since, shows as its id.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { folderRefUrl } from "@/src/utils/folder-ref";
|
||||
import { storagePathName, type StorageSpec } from "@/src/utils/storage-volumes";
|
||||
|
||||
type Props = {
|
||||
spec: StorageSpec;
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A folder a bot named in a message (`folderRef`): the name it ends with,
|
||||
* linked into the files browser, where the user can look at everything in
|
||||
* it. Nothing is downloaded: a folder ref never serves bytes, and this chip
|
||||
* never guesses from the name whether it is a file or a folder, because the
|
||||
* ref itself already says it is a folder. A file the bot saved is a file
|
||||
* ref (`fileRef`) and opens the file instead.
|
||||
*
|
||||
* Written without a literal ref in this comment on purpose: Tailwind scans
|
||||
* every page and partial, reads a bracketed `name:value` as an
|
||||
* arbitrary-property utility, and emits it as a real CSS property (esbuild
|
||||
* then warns that `file` is not one).
|
||||
*/
|
||||
export default function FolderRefChip({ spec, path }: Props) {
|
||||
const name = storagePathName({ volume: spec.volume, path });
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={folderRefUrl({ volume: spec.volume, owner_id: spec.owner_id, path })}
|
||||
title={`Open ${path || name} in Files`}
|
||||
className="inline-flex h-6 max-w-full gap-1.5 items-center px-1.5 align-middle rounded-default-xs border-0 bg-sunken text-[13px] font-bold leading-4 text-ink hover:opacity-80"
|
||||
>
|
||||
<FolderOpen size={14} aria-hidden className="shrink-0" />
|
||||
<Span className="truncate">{name}</Span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
TakeOverRefHrefPrefix,
|
||||
WorkspaceRefHrefPrefix,
|
||||
} from "@/src/utils/link-computer-refs";
|
||||
import { parsePathRefHref } from "@/src/utils/path-ref";
|
||||
import { parseFolderRefHref } from "@/src/utils/folder-ref";
|
||||
import BotRefChip from "./bot-ref-chip";
|
||||
import ComputerRefChip from "./computer-ref-chip";
|
||||
import FileRefChip from "./file-ref-chip";
|
||||
import PathRefChip from "./path-ref-chip";
|
||||
import FolderRefChip from "./folder-ref-chip";
|
||||
import TakeOverChip from "./take-over-chip";
|
||||
import WorkspaceRefChip from "./workspace-ref-chip";
|
||||
|
||||
@@ -20,8 +20,9 @@ import WorkspaceRefChip from "./workspace-ref-chip";
|
||||
* A link in a bot message. A computer the bot named (`linkComputerRefs`)
|
||||
* becomes its switch button, a take-over request a button that opens the
|
||||
* computer, a bot it named a link to that bot's conversation, a workspace
|
||||
* a link to that workspace, a file it saved a chip that opens it; anything
|
||||
* else opens in a new tab.
|
||||
* a link to that workspace, a file it saved a chip that downloads it, a
|
||||
* folder it named a chip that opens it in the files browser; anything else
|
||||
* opens in a new tab.
|
||||
*/
|
||||
export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
if (href?.startsWith(ComputerRefHrefPrefix)) {
|
||||
@@ -63,9 +64,9 @@ export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
if (file_id) return <FileRefChip file_id={file_id} />;
|
||||
}
|
||||
|
||||
const path_ref = parsePathRefHref({ href });
|
||||
const folder_ref = parseFolderRefHref({ href });
|
||||
|
||||
if (path_ref) return <PathRefChip spec={path_ref.spec} path={path_ref.path} />;
|
||||
if (folder_ref) return <FolderRefChip spec={folder_ref.spec} path={folder_ref.path} />;
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { FolderOpen, Paperclip } from "lucide-react";
|
||||
import { pathRefUrl } from "@/src/utils/path-ref";
|
||||
import { storagePathName, type StorageSpec } from "@/src/utils/storage-volumes";
|
||||
|
||||
type Props = {
|
||||
spec: StorageSpec;
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A folder or file a bot named in a message (`pathRef`): the name it ends
|
||||
* with, linked into the files browser, where the user can look at
|
||||
* everything around it. That is how evidence a bot left on a computer is
|
||||
* reached without copying it into its own files first.
|
||||
*/
|
||||
export default function PathRefChip({ spec, path }: Props) {
|
||||
const name = storagePathName({ volume: spec.volume, path });
|
||||
const is_folder = !/\.[a-z0-9]{1,8}$/i.test(name);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={pathRefUrl({ volume: spec.volume, owner_id: spec.owner_id, path })}
|
||||
title={`Open ${path || name} in Files`}
|
||||
className="inline-flex h-6 max-w-full gap-1.5 items-center px-1.5 align-middle rounded-default-xs border-0 bg-sunken text-[13px] font-bold leading-4 text-ink hover:opacity-80"
|
||||
>
|
||||
{is_folder ? (
|
||||
<FolderOpen size={14} aria-hidden className="shrink-0" />
|
||||
) : (
|
||||
<Paperclip size={14} aria-hidden className="shrink-0" />
|
||||
)}
|
||||
<Span className="truncate">{name}</Span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +65,6 @@ export const handler: BunextAPIRouteHandler<
|
||||
media_paradigms: ["bot-attachment"],
|
||||
media: { title: name },
|
||||
extract_media_text_content: media_type == "document",
|
||||
is_private: true,
|
||||
user: { id: Number(user.id) },
|
||||
}).catch(async (error) => {
|
||||
await logError({ title: `Attachment ${name} couldn't be saved`, error }).catch(() => {});
|
||||
|
||||
@@ -28,7 +28,11 @@ export default async function (
|
||||
};
|
||||
}
|
||||
|
||||
const GET = await grabMedia({ query, user });
|
||||
/**
|
||||
* A non-admin only ever lists their own uploads: the filter above is
|
||||
* the one that is queried, so it can't be bypassed by the request.
|
||||
*/
|
||||
const GET = await grabMedia({ query: final_sql_query as typeof query, user });
|
||||
|
||||
return GET;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject<TaskDetail>> = asy
|
||||
media_paradigms: ["task-media"],
|
||||
media: { title: name },
|
||||
extract_media_text_content: media_type == "document",
|
||||
is_private: true,
|
||||
user: { id: Number(user.id) },
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
reauth,
|
||||
return_media_text_content,
|
||||
media,
|
||||
is_media_private,
|
||||
media_name,
|
||||
} = body;
|
||||
|
||||
@@ -105,7 +104,6 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
media,
|
||||
media_mime_type: ext,
|
||||
user: { id: target_user_id },
|
||||
is_private: is_media_private,
|
||||
media_name,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user