diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/api/assets/[assetId]/route.ts | 11 | ||||
| -rw-r--r-- | apps/web/app/api/assets/route.ts | 17 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/AttachmentBox.tsx | 112 | ||||
| -rw-r--r-- | apps/workers/crawlerWorker.ts | 143 |
4 files changed, 167 insertions, 116 deletions
diff --git a/apps/web/app/api/assets/[assetId]/route.ts b/apps/web/app/api/assets/[assetId]/route.ts index f3cf1ab4..73237d8d 100644 --- a/apps/web/app/api/assets/[assetId]/route.ts +++ b/apps/web/app/api/assets/[assetId]/route.ts @@ -1,5 +1,7 @@ import { createContextFromRequest } from "@/server/api/client"; +import { and, eq } from "drizzle-orm"; +import { assets } from "@hoarder/db/schema"; import { readAsset } from "@hoarder/shared/assetdb"; export const dynamic = "force-dynamic"; @@ -11,6 +13,15 @@ export async function GET( if (!ctx.user) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } + + const assetDb = await ctx.db.query.assets.findFirst({ + where: and(eq(assets.id, params.assetId), eq(assets.userId, ctx.user.id)), + }); + + if (!assetDb) { + return Response.json({ error: "Asset not found" }, { status: 404 }); + } + const { asset, metadata } = await readAsset({ userId: ctx.user.id, assetId: params.assetId, diff --git a/apps/web/app/api/assets/route.ts b/apps/web/app/api/assets/route.ts index 9028f556..0e52ff93 100644 --- a/apps/web/app/api/assets/route.ts +++ b/apps/web/app/api/assets/route.ts @@ -2,6 +2,7 @@ import { createContextFromRequest } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; import type { ZUploadResponse } from "@hoarder/shared/types/uploads"; +import { assets, AssetTypes } from "@hoarder/db/schema"; import { newAssetId, saveAsset, @@ -43,8 +44,22 @@ export async function POST(request: Request) { return Response.json({ error: "Bad request" }, { status: 400 }); } - const assetId = newAssetId(); const fileName = data.name; + const [assetDb] = await ctx.db + .insert(assets) + .values({ + id: newAssetId(), + // Initially, uploads are uploaded for unknown purpose + // And without an attached bookmark. + assetType: AssetTypes.UNKNOWN, + bookmarkId: null, + userId: ctx.user.id, + contentType, + size: data.size, + fileName, + }) + .returning(); + const assetId = assetDb.id; await saveAsset({ userId: ctx.user.id, diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx index a8eaf0f4..436f1026 100644 --- a/apps/web/components/dashboard/preview/AttachmentBox.tsx +++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx @@ -16,6 +16,7 @@ import { ChevronsDownUp, Download, Image, + Paperclip, Pencil, Plus, Trash2, @@ -35,6 +36,7 @@ import { import { humanFriendlyNameForAssertType, isAllowedToAttachAsset, + isAllowedToDetachAsset, } from "@hoarder/trpc/lib/attachments"; export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { @@ -42,6 +44,8 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { screenshot: <Camera className="size-4" />, fullPageArchive: <Archive className="size-4" />, bannerImage: <Image className="size-4" />, + bookmarkAsset: <Paperclip className="size-4" />, + unknown: <Paperclip className="size-4" />, }; const { mutate: attachAsset, isPending: isAttaching } = @@ -100,11 +104,6 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { bookmark.assets.sort((a, b) => a.assetType.localeCompare(b.assetType)); - if (bookmark.content.type == BookmarkTypes.ASSET) { - // Currently, we don't allow attaching assets to assets types. - return null; - } - return ( <Collapsible> <CollapsibleTrigger className="flex w-full items-center justify-between gap-2 text-sm text-gray-400"> @@ -156,59 +155,62 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { <Pencil className="size-4" /> </FilePickerButton> )} - <ActionConfirmingDialog - title="Delete Attachment?" - description={`Are you sure you want to delete the attachment of the bookmark?`} - actionButton={(setDialogOpen) => ( - <ActionButton - loading={isDetaching} - variant="destructive" - onClick={() => - detachAsset( - { bookmarkId: bookmark.id, assetId: asset.id }, - { onSettled: () => setDialogOpen(false) }, - ) - } - > - <Trash2 className="mr-2 size-4" /> - Delete - </ActionButton> - )} - > - <Button variant="none" size="none" title="Delete"> - <Trash2 className="size-4" /> - </Button> - </ActionConfirmingDialog> + {isAllowedToDetachAsset(asset.assetType) && ( + <ActionConfirmingDialog + title="Delete Attachment?" + description={`Are you sure you want to delete the attachment of the bookmark?`} + actionButton={(setDialogOpen) => ( + <ActionButton + loading={isDetaching} + variant="destructive" + onClick={() => + detachAsset( + { bookmarkId: bookmark.id, assetId: asset.id }, + { onSettled: () => setDialogOpen(false) }, + ) + } + > + <Trash2 className="mr-2 size-4" /> + Delete + </ActionButton> + )} + > + <Button variant="none" size="none" title="Delete"> + <Trash2 className="size-4" /> + </Button> + </ActionConfirmingDialog> + )} </div> </div> ))} - {!bookmark.assets.some((asset) => asset.assetType == "bannerImage") && ( - <FilePickerButton - title="Attach a Banner" - loading={isAttaching} - accept=".jgp,.JPG,.jpeg,.png,.webp" - multiple={false} - variant="ghost" - size="none" - className="flex w-full items-center justify-center gap-2" - onFileSelect={(file) => - uploadAsset(file, { - onSuccess: (resp) => { - attachAsset({ - bookmarkId: bookmark.id, - asset: { - id: resp.assetId, - assetType: "bannerImage", - }, - }); - }, - }) - } - > - <Plus className="size-4" /> - Attach a Banner - </FilePickerButton> - )} + {!bookmark.assets.some((asset) => asset.assetType == "bannerImage") && + bookmark.content.type != BookmarkTypes.ASSET && ( + <FilePickerButton + title="Attach a Banner" + loading={isAttaching} + accept=".jgp,.JPG,.jpeg,.png,.webp" + multiple={false} + variant="ghost" + size="none" + className="flex w-full items-center justify-center gap-2" + onFileSelect={(file) => + uploadAsset(file, { + onSuccess: (resp) => { + attachAsset({ + bookmarkId: bookmark.id, + asset: { + id: resp.assetId, + assetType: "bannerImage", + }, + }); + }, + }) + } + > + <Plus className="size-4" /> + Attach a Banner + </FilePickerButton> + )} </CollapsibleContent> </Collapsible> ); diff --git a/apps/workers/crawlerWorker.ts b/apps/workers/crawlerWorker.ts index f830c500..74413c63 100644 --- a/apps/workers/crawlerWorker.ts +++ b/apps/workers/crawlerWorker.ts @@ -36,6 +36,7 @@ import { DequeuedJob, Runner } from "@hoarder/queue"; import { ASSET_TYPES, deleteAsset, + getAssetSize, IMAGE_ASSET_TYPES, newAssetId, saveAsset, @@ -192,6 +193,8 @@ export class CrawlerWorker { } } +type DBAssetType = typeof assets.$inferInsert; + async function changeBookmarkStatus( bookmarkId: string, crawlStatus: "success" | "failure", @@ -353,16 +356,18 @@ async function storeScreenshot( return null; } const assetId = newAssetId(); + const contentType = "image/png"; + const fileName = "screenshot.png"; await saveAsset({ userId, assetId, - metadata: { contentType: "image/png", fileName: "screenshot.png" }, + metadata: { contentType, fileName }, asset: screenshot, }); logger.info( `[Crawler][${jobId}] Stored the screenshot as assetId: ${assetId}`, ); - return assetId; + return { assetId, contentType, fileName, size: screenshot.byteLength }; } async function downloadAndStoreFile( @@ -396,7 +401,7 @@ async function downloadAndStoreFile( `[Crawler][${jobId}] Downloaded ${fileType} as assetId: ${assetId}`, ); - return assetId; + return { assetId, userId, contentType, size: buffer.byteLength }; } catch (e) { logger.error( `[Crawler][${jobId}] Failed to download and store ${fileType}: ${e}`, @@ -433,12 +438,14 @@ async function archiveWebpage( input: html, })`monolith - -Ije -t 5 -b ${url} -o ${assetPath}`; + const contentType = "text/html"; + await saveAssetFromFile({ userId, assetId, assetPath, metadata: { - contentType: "text/html", + contentType, }, }); @@ -446,7 +453,11 @@ async function archiveWebpage( `[Crawler][${jobId}] Done archiving the page as assetId: ${assetId}`, ); - return assetId; + return { + assetId, + contentType, + size: await getAssetSize({ userId, assetId }), + }; } async function getContentType( @@ -489,17 +500,31 @@ async function handleAsAssetBookmark( jobId: string, bookmarkId: string, ) { - const assetId = await downloadAndStoreFile(url, userId, jobId, assetType); - if (!assetId) { + const downloaded = await downloadAndStoreFile(url, userId, jobId, assetType); + if (!downloaded) { return; } + const fileName = path.basename(new URL(url).pathname); await db.transaction(async (trx) => { + await updateAsset( + undefined, + { + id: downloaded.assetId, + bookmarkId, + userId, + assetType: AssetTypes.BOOKMARK_ASSET, + contentType: downloaded.contentType, + size: downloaded.size, + fileName, + }, + trx, + ); await trx.insert(bookmarkAssets).values({ id: bookmarkId, assetType, - assetId, + assetId: downloaded.assetId, content: null, - fileName: path.basename(new URL(url).pathname), + fileName, sourceUrl: url, }); // Switch the type of the bookmark from LINK to ASSET @@ -527,14 +552,24 @@ async function crawlAndParseUrl( url: browserUrl, } = await crawlPage(jobId, url); - const [meta, readableContent, screenshotAssetId] = await Promise.all([ + const [meta, readableContent, screenshotAssetInfo] = await Promise.all([ extractMetadata(htmlContent, browserUrl, jobId), extractReadableContent(htmlContent, browserUrl, jobId), storeScreenshot(screenshot, userId, jobId), ]); - let imageAssetId: string | null = null; + let imageAssetInfo: DBAssetType | null = null; if (meta.image) { - imageAssetId = await downloadAndStoreImage(meta.image, userId, jobId); + const downloaded = await downloadAndStoreImage(meta.image, userId, jobId); + if (downloaded) { + imageAssetInfo = { + id: downloaded.assetId, + bookmarkId, + userId, + assetType: AssetTypes.LINK_BANNER_IMAGE, + contentType: downloaded.contentType, + size: downloaded.size, + }; + } } // TODO(important): Restrict the size of content to store @@ -552,22 +587,24 @@ async function crawlAndParseUrl( }) .where(eq(bookmarkLinks.id, bookmarkId)); - await updateAsset( - screenshotAssetId, - oldScreenshotAssetId, - bookmarkId, - userId, - AssetTypes.LINK_SCREENSHOT, - txn, - ); - await updateAsset( - imageAssetId, - oldImageAssetId, - bookmarkId, - userId, - AssetTypes.LINK_BANNER_IMAGE, - txn, - ); + if (screenshotAssetInfo) { + await updateAsset( + oldScreenshotAssetId, + { + id: screenshotAssetInfo.assetId, + bookmarkId, + userId, + assetType: AssetTypes.LINK_SCREENSHOT, + contentType: screenshotAssetInfo.contentType, + size: screenshotAssetInfo.size, + fileName: screenshotAssetInfo.fileName, + }, + txn, + ); + } + if (imageAssetInfo) { + await updateAsset(oldImageAssetId, imageAssetInfo, txn); + } }); // Delete the old assets if any @@ -582,20 +619,24 @@ async function crawlAndParseUrl( return async () => { if (serverConfig.crawler.fullPageArchive || archiveFullPage) { - const fullPageArchiveAssetId = await archiveWebpage( - htmlContent, - browserUrl, - userId, - jobId, - ); + const { + assetId: fullPageArchiveAssetId, + size, + contentType, + } = await archiveWebpage(htmlContent, browserUrl, userId, jobId); await db.transaction(async (txn) => { await updateAsset( - fullPageArchiveAssetId, oldFullPageArchiveAssetId, - bookmarkId, - userId, - AssetTypes.LINK_FULL_PAGE_ARCHIVE, + { + id: fullPageArchiveAssetId, + bookmarkId, + userId, + assetType: AssetTypes.LINK_FULL_PAGE_ARCHIVE, + contentType, + size, + fileName: null, + }, txn, ); }); @@ -676,31 +717,13 @@ async function runCrawler(job: DequeuedJob<ZCrawlLinkRequest>) { await archivalLogic(); } -/** - * Removes the old asset and adds a new one instead - * @param newAssetId the new assetId to add - * @param oldAssetId the old assetId to remove (if it exists) - * @param bookmarkId the id of the bookmark the asset belongs to - * @param assetType the type of the asset - * @param txn the transaction where this update should happen in - */ async function updateAsset( - newAssetId: string | null, oldAssetId: string | undefined, - bookmarkId: string, - userId: string, - assetType: AssetTypes, + newAsset: DBAssetType, txn: HoarderDBTransaction, ) { - if (newAssetId) { - if (oldAssetId) { - await txn.delete(assets).where(eq(assets.id, oldAssetId)); - } - await txn.insert(assets).values({ - id: newAssetId, - assetType, - bookmarkId, - userId, - }); + if (oldAssetId) { + await txn.delete(assets).where(eq(assets.id, oldAssetId)); } + await txn.insert(assets).values(newAsset); } |
