Skip to content
Merged
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
85 changes: 59 additions & 26 deletions apps/web/components/dashboard/preview/AttachmentBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,43 +101,52 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
prefetch={false}
>
{ASSET_TYPE_TO_ICON[asset.assetType]}
<p>{humanFriendlyNameForAssertType(asset.assetType)}</p>
<p>
{asset.assetType === "userUploaded" && asset.fileName
? asset.fileName
: humanFriendlyNameForAssertType(asset.assetType)}
</p>
</Link>
<div className="flex gap-2">
<Link
title="Download"
target="_blank"
href={getAssetUrl(asset.id)}
className="flex items-center gap-1"
download={humanFriendlyNameForAssertType(asset.assetType)}
download={
asset.assetType === "userUploaded" && asset.fileName
? asset.fileName
: humanFriendlyNameForAssertType(asset.assetType)
}
prefetch={false}
>
<Download className="size-4" />
</Link>
{isAllowedToAttachAsset(asset.assetType) && (
<FilePickerButton
title="Replace"
loading={isReplacing}
accept=".jgp,.JPG,.jpeg,.png,.webp"
multiple={false}
variant="none"
size="none"
className="flex items-center gap-2"
onFileSelect={(file) =>
uploadAsset(file, {
onSuccess: (resp) => {
replaceAsset({
bookmarkId: bookmark.id,
oldAssetId: asset.id,
newAssetId: resp.assetId,
});
},
})
}
>
<Pencil className="size-4" />
</FilePickerButton>
)}
{isAllowedToAttachAsset(asset.assetType) &&
asset.assetType !== "userUploaded" && (
<FilePickerButton
title="Replace"
loading={isReplacing}
accept=".jgp,.JPG,.jpeg,.png,.webp"
multiple={false}
variant="none"
size="none"
className="flex items-center gap-2"
onFileSelect={(file) =>
uploadAsset(file, {
onSuccess: (resp) => {
replaceAsset({
bookmarkId: bookmark.id,
oldAssetId: asset.id,
newAssetId: resp.assetId,
});
},
})
}
>
<Pencil className="size-4" />
</FilePickerButton>
)}
{isAllowedToDetachAsset(asset.assetType) && (
<ActionConfirmingDialog
title="Delete Attachment?"
Expand Down Expand Up @@ -194,6 +203,30 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
Attach a Banner
</FilePickerButton>
)}
<FilePickerButton
title="Upload File"
loading={isAttaching}
multiple={false}
variant="ghost"
size="none"
className="flex w-full items-center justify-center gap-2"
onFileSelect={(file) =>
uploadAsset(file, {
onSuccess: (resp) => {
attachAsset({
Comment on lines +212 to +216

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Backend still rejects non whitelisted file types

The new "Upload File" button allows selecting any file, but the upload endpoint (packages/api/utils/upload.tsuploadAsset) still checks SUPPORTED_UPLOAD_ASSET_TYPES and returns "Unsupported asset type" for MIME types outside the existing image/video/pdf list. As a result, attempting to attach a ZIP or DOCX fails even though the UI now advertises arbitrary uploads. Consider widening the whitelist or skipping that check for userUploaded assets so the feature works as described.

Useful? React with 👍 / 👎.

bookmarkId: bookmark.id,
asset: {
id: resp.assetId,
assetType: "userUploaded",
},
});
},
})
}
>
<Plus className="size-4" />
Upload File
</FilePickerButton>
</CollapsibleContent>
</Collapsible>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FileCode,
Image,
Paperclip,
Upload,
Video,
} from "lucide-react";

Expand All @@ -18,5 +19,6 @@ export const ASSET_TYPE_TO_ICON: Record<ZAssetType, React.ReactNode> = {
video: <Video className="size-4" />,
bookmarkAsset: <Paperclip className="size-4" />,
linkHtmlContent: <FileCode className="size-4" />,
userUploaded: <Upload className="size-4" />,
unknown: <Paperclip className="size-4" />,
};
2 changes: 2 additions & 0 deletions packages/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export const enum AssetTypes {
LINK_VIDEO = "linkVideo",
LINK_HTML_CONTENT = "linkHtmlContent",
BOOKMARK_ASSET = "bookmarkAsset",
USER_UPLOADED = "userUploaded",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

LGTM: USER_UPLOADED asset type properly added to schema.

The new asset type is correctly declared in both the enum definition and the table constraint list, following the existing pattern for other asset types.

Please ensure a database migration has been created to support the new USER_UPLOADED asset type in the schema. Run the following script to check for recent migrations:

Also applies to: 250-250


🏁 Script executed:

#!/bin/bash
# Check for recent migration files that might include the USER_UPLOADED asset type
fd -e sql -e ts migration packages/db | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Explore packages/db directory structure
echo "=== Directory structure of packages/db ==="
find packages/db -type f -name "*migration*" -o -name "*migrate*" 2>/dev/null | head -20

echo -e "\n=== All files in packages/db ==="
find packages/db -type f | head -30

echo -e "\n=== Search for USER_UPLOADED in packages/db ==="
rg -n "USER_UPLOADED" packages/db/

Length of output: 1600


🏁 Script executed:

#!/bin/bash
# Check the schema.ts file around the lines mentioned
echo "=== Content of schema.ts around lines 231 and 250 ==="
sed -n '225,260p' packages/db/schema.ts

Length of output: 1226


🏁 Script executed:

#!/bin/bash
# Search entire codebase for migration-related patterns
echo "=== Search for migration files/folders ==="
find . -type d -name "*migrat*" 2>/dev/null | grep -v node_modules

echo -e "\n=== Search for USER_UPLOADED in entire codebase ==="
rg -n "USER_UPLOADED" --type ts --type sql 2>/dev/null | head -30

Length of output: 434



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the latest migration file
echo "=== Latest migration file (0026) ==="
cat packages/db/drizzle/0026_silky_imperial_guard.sql

echo -e "\n=== Check for any 0027+ migration files ==="
ls -la packages/db/drizzle/00* 2>/dev/null | tail -5

echo -e "\n=== Check if there are uncommitted migrations ==="
ls -la packages/db/drizzle/ | wc -l

Length of output: 715


🏁 Script executed:

#!/bin/bash
# Search for references to USER_UPLOADED to understand scope
echo "=== All references to USER_UPLOADED in codebase ==="
rg -n "USER_UPLOADED" --type ts --type sql

echo -e "\n=== Check packages/trpc/lib/attachments.ts context ==="
sed -n '15,45p' packages/trpc/lib/attachments.ts

Length of output: 1497


🏁 Script executed:

#!/bin/bash
# Verify schema.ts has the complete AssetTypes enum
echo "=== Complete AssetTypes enum ==="
rg -A 15 "enum AssetTypes" packages/db/schema.ts

Length of output: 501


Missing database migration for USER_UPLOADED asset type.

The schema.ts changes are correct, but there is no corresponding migration file in packages/db/drizzle/. Since USER_UPLOADED is already referenced in packages/trpc/lib/attachments.ts (lines 19, 37), the database schema must be updated to support this new enum value. A migration file needs to be generated to add the USER_UPLOADED enum variant to the assets table's assetType constraint.

🤖 Prompt for AI Agents
In packages/db/schema.ts around line 231, you added USER_UPLOADED to the asset
type enum but did not add a DB migration; create a new migration file under
packages/db/drizzle (timestamped like
<timestamp>_add_user_uploaded_asset_type.sql) that alters the existing Postgres
enum used by the assets table (or updates the CHECK/constraint if you used one)
to include the new value 'userUploaded'—for a Postgres enum use ALTER TYPE
<enum_name> ADD VALUE 'userUploaded'; ensure the migration is
numbered/registered with your migration runner and include a corresponding down
migration or reversible steps if required by your tooling, then run the
migration locally to verify packages/trpc/lib/attachments.ts references work
with the updated schema.

UNKNOWN = "unknown",
}

Expand All @@ -246,6 +247,7 @@ export const assets = sqliteTable(
AssetTypes.LINK_VIDEO,
AssetTypes.LINK_HTML_CONTENT,
AssetTypes.BOOKMARK_ASSET,
AssetTypes.USER_UPLOADED,
AssetTypes.UNKNOWN,
],
}).notNull(),
Expand Down
11 changes: 11 additions & 0 deletions packages/open-api/karakeep-openapi-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,13 @@
"video",
"bookmarkAsset",
"precrawledArchive",
"userUploaded",
"unknown"
]
},
"fileName": {
"type": "string",
"nullable": true
}
},
"required": [
Expand Down Expand Up @@ -1703,6 +1708,7 @@
"video",
"bookmarkAsset",
"precrawledArchive",
"userUploaded",
"unknown"
]
}
Expand Down Expand Up @@ -1737,8 +1743,13 @@
"video",
"bookmarkAsset",
"precrawledArchive",
"userUploaded",
"unknown"
]
},
"fileName": {
"type": "string",
"nullable": true
}
},
"required": [
Expand Down
6 changes: 5 additions & 1 deletion packages/open-api/lib/bookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { z } from "zod";

import {
zAssetSchema,
zAssetTypesSchema,
zBareBookmarkSchema,
zManipulatedTagSchema,
zNewBookmarkRequestSchema,
Expand Down Expand Up @@ -418,7 +419,10 @@ registry.registerPath({
description: "The asset to attach",
content: {
"application/json": {
schema: zAssetSchema,
schema: z.object({
id: z.string(),
assetType: zAssetTypesSchema,
}),
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/types/bookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export const zAssetTypesSchema = z.enum([
"video",
"bookmarkAsset",
"precrawledArchive",
"userUploaded",
"unknown",
]);
export type ZAssetType = z.infer<typeof zAssetTypesSchema>;

export const zAssetSchema = z.object({
id: z.string(),
assetType: zAssetTypesSchema,
fileName: z.string().nullish(),
});

export const zBookmarkedLinkSchema = z.object({
Expand Down
5 changes: 5 additions & 0 deletions packages/trpc/lib/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function mapDBAssetTypeToUserType(assetType: AssetTypes): ZAssetType {
[AssetTypes.LINK_VIDEO]: "video",
[AssetTypes.LINK_HTML_CONTENT]: "linkHtmlContent",
[AssetTypes.BOOKMARK_ASSET]: "bookmarkAsset",
[AssetTypes.USER_UPLOADED]: "userUploaded",
[AssetTypes.UNKNOWN]: "bannerImage",
};
return map[assetType];
Expand All @@ -33,6 +34,7 @@ export function mapSchemaAssetTypeToDB(
video: AssetTypes.LINK_VIDEO,
bookmarkAsset: AssetTypes.BOOKMARK_ASSET,
linkHtmlContent: AssetTypes.LINK_HTML_CONTENT,
userUploaded: AssetTypes.USER_UPLOADED,
unknown: AssetTypes.UNKNOWN,
};
return map[assetType];
Expand All @@ -48,6 +50,7 @@ export function humanFriendlyNameForAssertType(type: ZAssetType) {
video: "Video",
bookmarkAsset: "Bookmark Asset",
linkHtmlContent: "HTML Content",
userUploaded: "User Uploaded File",
unknown: "Unknown",
};
return map[type];
Expand All @@ -63,6 +66,7 @@ export function isAllowedToAttachAsset(type: ZAssetType) {
video: true,
bookmarkAsset: false,
linkHtmlContent: false,
userUploaded: true,
unknown: false,
};
return map[type];
Expand All @@ -78,6 +82,7 @@ export function isAllowedToDetachAsset(type: ZAssetType) {
video: true,
bookmarkAsset: false,
linkHtmlContent: false,
userUploaded: true,
unknown: false,
};
return map[type];
Expand Down
1 change: 1 addition & 0 deletions packages/trpc/models/bookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export class Bookmark implements PrivacyAware {
acc[bookmarkId].assets.push({
id: row.assets.id,
assetType: mapDBAssetTypeToUserType(row.assets.assetType),
fileName: row.assets.fileName,
});
}

Expand Down
17 changes: 13 additions & 4 deletions packages/trpc/routers/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ export const assetsAppRouter = router({
.input(
z.object({
bookmarkId: z.string(),
asset: zAssetSchema,
asset: z.object({
id: z.string(),
assetType: zAssetTypesSchema,
}),
}),
)
.output(zAssetSchema)
Expand All @@ -112,16 +115,22 @@ export const assetsAppRouter = router({
message: "You can't attach this type of asset",
});
}
await ctx.db
const [updatedAsset] = await ctx.db
.update(assets)
.set({
assetType: mapSchemaAssetTypeToDB(input.asset.assetType),
bookmarkId: input.bookmarkId,
})
.where(
and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)),
);
return input.asset;
)
.returning();

return {
id: updatedAsset.id,
assetType: mapDBAssetTypeToUserType(updatedAsset.assetType),
fileName: updatedAsset.fileName,
};
}),
replaceAsset: authedProcedure
.input(
Expand Down
1 change: 1 addition & 0 deletions packages/trpc/routers/bookmarks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ async function toZodSchema(
assets: assets.map((a) => ({
id: a.id,
assetType: mapDBAssetTypeToUserType(a.assetType),
fileName: a.fileName,
})),
...rest,
};
Expand Down
Loading