diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index d93904a..9299442 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -19,6 +19,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref"; import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; +import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -53,7 +54,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { let shortendURL; try { - shortendURL = new URL(link.url || "").host.toLowerCase(); + if (link.url) { + shortendURL = new URL(link.url).host.toLowerCase(); + } } catch (error) { console.log(error); } @@ -109,7 +112,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { editMode && (permissions === true || permissions?.canCreate || permissions?.canDelete); - // window.open ('www.yourdomain.com', '_ blank'); return (
- { - e.stopPropagation(); - }} - className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" - > - -

{shortendURL}

- +

diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index d809dfc..6c25093 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -122,18 +122,20 @@ export default function LinkActions({ ) : undefined} -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setPreservedFormatsModal(true); - }} - > - Preserved Formats -
    -
  • + {link.type === "url" && ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setPreservedFormatsModal(true); + }} + > + Preserved Formats +
    +
  • + )} {permissions === true || permissions?.canDelete ? (
  • (true); return ( <> - {link.url && url && showFavicon ? ( - { - setShowFavicon(false); - }} - /> - ) : showFavicon === false ? ( -
    - -
    + {link.type === "url" && url ? ( + showFavicon ? ( + { + setShowFavicon(false); + }} + /> + ) : ( + + ) ) : link.type === "pdf" ? ( - + ) : link.type === "image" ? ( - + ) : undefined} ); } + +const LinkPlaceholderIcon = ({ + iconClasses, + icon, +}: { + iconClasses: string; + icon: string; +}) => { + return ( +
    + +
    + ); +}; diff --git a/components/LinkViews/LinkComponents/LinkTypeBadge.tsx b/components/LinkViews/LinkComponents/LinkTypeBadge.tsx new file mode 100644 index 0000000..e491330 --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkTypeBadge.tsx @@ -0,0 +1,38 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import Link from "next/link"; +import React from "react"; + +export default function LinkTypeBadge({ + link, +}: { + link: LinkIncludingShortenedCollectionAndTags; +}) { + let shortendURL; + + if (link.type === "url" && link.url) { + try { + shortendURL = new URL(link.url).host.toLowerCase(); + } catch (error) { + console.log(error); + } + } + + return link.url && shortendURL ? ( + { + e.stopPropagation(); + }} + className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" + > + +

    {shortendURL}

    + + ) : ( +
    + {link.type} +
    + ); +} diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index a535adf..e5bd8bc 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -16,6 +16,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref"; import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; +import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -56,14 +57,6 @@ export default function LinkCardCompact({ } }; - let shortendURL; - - try { - shortendURL = new URL(link.url || "").host.toLowerCase(); - } catch (error) { - console.log(error); - } - const [collection, setCollection] = useState( collections.find( @@ -130,7 +123,11 @@ export default function LinkCardCompact({ } >
    - +
    @@ -143,24 +140,7 @@ export default function LinkCardCompact({ {collection ? ( ) : undefined} - {link.url ? ( - { - e.stopPropagation(); - }} - className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" - > - -

    {shortendURL}

    - - ) : ( -
    - {link.type} -
    - )} +
    diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 497bc91..49524aa 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -43,7 +43,7 @@ export default function UploadFileModal({ onClose }: Props) { const [file, setFile] = useState(); - const { addLink } = useLinkStore(); + const { uploadFile } = useLinkStore(); const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); @@ -100,56 +100,22 @@ export default function UploadFileModal({ onClose }: Props) { const submit = async () => { if (!submitLoader && file) { - let fileType: ArchivedFormat | null = null; - let linkType: "url" | "image" | "pdf" | null = null; + setSubmitLoader(true); - if (file?.type === "image/jpg" || file.type === "image/jpeg") { - fileType = ArchivedFormat.jpeg; - linkType = "image"; - } else if (file.type === "image/png") { - fileType = ArchivedFormat.png; - linkType = "image"; - } else if (file.type === "application/pdf") { - fileType = ArchivedFormat.pdf; - linkType = "pdf"; - } + const load = toast.loading("Creating..."); - if (fileType !== null && linkType !== null) { - setSubmitLoader(true); + const response = await uploadFile(link, file); - let response; + toast.dismiss(load); - const load = toast.loading("Creating..."); + if (response.ok) { + toast.success(`Created!`); + onClose(); + } else toast.error(response.data as string); - response = await addLink({ - ...link, - type: linkType, - name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""), - }); + setSubmitLoader(false); - toast.dismiss(load); - - if (response.ok) { - const formBody = new FormData(); - file && formBody.append("file", file); - - await fetch( - `/api/v1/archives/${ - (response.data as LinkIncludingShortenedCollectionAndTags).id - }?format=${fileType}`, - { - body: formBody, - method: "POST", - } - ); - toast.success(`Created!`); - onClose(); - } else toast.error(response.data as string); - - setSubmitLoader(false); - - return response; - } + return response; } }; @@ -238,7 +204,7 @@ export default function UploadFileModal({ onClose }: Props) { className="btn btn-accent dark:border-violet-400 text-white" onClick={submit} > - Create Link + Upload File diff --git a/components/Navbar.tsx b/components/Navbar.tsx index cb38c93..df672c1 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -93,7 +93,7 @@ export default function Navbar() { New Link
  • - {/*
  • +
  • { (document?.activeElement as HTMLElement)?.blur(); @@ -104,7 +104,7 @@ export default function Navbar() { > Upload File
    -
  • */} +
  • { diff --git a/lib/api/archiveHandler.ts b/lib/api/archiveHandler.ts index 08a35b5..a32498f 100644 --- a/lib/api/archiveHandler.ts +++ b/lib/api/archiveHandler.ts @@ -7,9 +7,9 @@ import { JSDOM } from "jsdom"; import DOMPurify from "dompurify"; import { Collection, Link, User } from "@prisma/client"; import validateUrlSize from "./validateUrlSize"; -import removeFile from "./storage/removeFile"; -import Jimp from "jimp"; import createFolder from "./storage/createFolder"; +import generatePreview from "./generatePreview"; +import { removeFiles } from "./manageLinkFiles"; type LinksAndCollectionAndOwner = Link & { collection: Collection & { @@ -51,6 +51,14 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { ); }); + createFolder({ + filePath: `archives/preview/${link.collectionId}`, + }); + + createFolder({ + filePath: `archives/${link.collectionId}`, + }); + try { await Promise.race([ (async () => { @@ -162,10 +170,6 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { return metaTag ? (metaTag as any).content : null; }); - createFolder({ - filePath: `archives/preview/${link.collectionId}`, - }); - if (ogImageUrl) { console.log("Found og:image URL:", ogImageUrl); @@ -175,35 +179,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { // Check if imageResponse is not null if (imageResponse && !link.preview?.startsWith("archive")) { const buffer = await imageResponse.body(); - - // Check if buffer is not null - if (buffer) { - // Load the image using Jimp - Jimp.read(buffer, async (err, image) => { - if (image && !err) { - image?.resize(1280, Jimp.AUTO).quality(20); - const processedBuffer = await image?.getBufferAsync( - Jimp.MIME_JPEG - ); - - createFile({ - data: processedBuffer, - filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`, - }).then(() => { - return prisma.link.update({ - where: { id: link.id }, - data: { - preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`, - }, - }); - }); - } - }).catch((err) => { - console.error("Error processing the image:", err); - }); - } else { - console.log("No image data found."); - } + await generatePreview(buffer, link.collectionId, link.id); } await page.goBack(); @@ -323,14 +299,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { }, }); else { - removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` }); - removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` }); - removeFile({ - filePath: `archives/${link.collectionId}/${link.id}_readability.json`, - }); - removeFile({ - filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`, - }); + await removeFiles(link.id, link.collectionId); } await browser.close(); diff --git a/lib/api/controllers/links/bulk/deleteLinksById.ts b/lib/api/controllers/links/bulk/deleteLinksById.ts index 466db98..2db3896 100644 --- a/lib/api/controllers/links/bulk/deleteLinksById.ts +++ b/lib/api/controllers/links/bulk/deleteLinksById.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/api/db"; import { UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; import removeFile from "@/lib/api/storage/removeFile"; +import { removeFiles } from "@/lib/api/manageLinkFiles"; export default async function deleteLinksById( userId: number, @@ -43,15 +44,7 @@ export default async function deleteLinksById( const linkId = linkIds[i]; const collectionIsAccessible = collectionIsAccessibleArray[i]; - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`, - }); - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`, - }); - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`, - }); + if (collectionIsAccessible) removeFiles(linkId, collectionIsAccessible.id); } return { response: deletedLinks, status: 200 }; diff --git a/lib/api/controllers/links/linkId/deleteLinkById.ts b/lib/api/controllers/links/linkId/deleteLinkById.ts index db68ee7..dba90cb 100644 --- a/lib/api/controllers/links/linkId/deleteLinkById.ts +++ b/lib/api/controllers/links/linkId/deleteLinkById.ts @@ -2,6 +2,7 @@ import { prisma } from "@/lib/api/db"; import { Link, UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; import removeFile from "@/lib/api/storage/removeFile"; +import { removeFiles } from "@/lib/api/manageLinkFiles"; export default async function deleteLink(userId: number, linkId: number) { if (!linkId) return { response: "Please choose a valid link.", status: 401 }; @@ -12,7 +13,10 @@ export default async function deleteLink(userId: number, linkId: number) { (e: UsersAndCollections) => e.userId === userId && e.canDelete ); - if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) + if ( + !collectionIsAccessible || + !(collectionIsAccessible?.ownerId === userId || memberHasAccess) + ) return { response: "Collection is not accessible.", status: 401 }; const deleteLink: Link = await prisma.link.delete({ @@ -21,15 +25,7 @@ export default async function deleteLink(userId: number, linkId: number) { }, }); - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`, - }); - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`, - }); - removeFile({ - filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`, - }); + removeFiles(linkId, collectionIsAccessible.id); return { response: deleteLink, status: 200 }; } diff --git a/lib/api/controllers/links/linkId/updateLinkById.ts b/lib/api/controllers/links/linkId/updateLinkById.ts index e6f7f0d..4a24f4a 100644 --- a/lib/api/controllers/links/linkId/updateLinkById.ts +++ b/lib/api/controllers/links/linkId/updateLinkById.ts @@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { UsersAndCollections } from "@prisma/client"; import getPermission from "@/lib/api/getPermission"; -import moveFile from "@/lib/api/storage/moveFile"; +import { moveFiles } from "@/lib/api/manageLinkFiles"; export default async function updateLinkById( userId: number, @@ -146,20 +146,7 @@ export default async function updateLinkById( }); if (collectionIsAccessible?.id !== data.collection.id) { - await moveFile( - `archives/${collectionIsAccessible?.id}/${linkId}.pdf`, - `archives/${data.collection.id}/${linkId}.pdf` - ); - - await moveFile( - `archives/${collectionIsAccessible?.id}/${linkId}.png`, - `archives/${data.collection.id}/${linkId}.png` - ); - - await moveFile( - `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`, - `archives/${data.collection.id}/${linkId}_readability.json` - ); + await moveFiles(linkId, collectionIsAccessible?.id, data.collection.id); } return { response: updatedLink, status: 200 }; diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index ba4e513..85fd537 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -12,14 +12,16 @@ export default async function postLink( link: LinkIncludingShortenedCollectionAndTags, userId: number ) { - try { - new URL(link.url || ""); - } catch (error) { - return { - response: - "Please enter a valid Address for the Link. (It should start with http/https)", - status: 400, - }; + if (link.url || link.type === "url") { + try { + new URL(link.url || ""); + } catch (error) { + return { + response: + "Please enter a valid Address for the Link. (It should start with http/https)", + status: 400, + }; + } } if (!link.collection.id && link.collection.name) { @@ -172,7 +174,7 @@ export default async function postLink( const newLink = await prisma.link.create({ data: { - url: link.url?.trim(), + url: link.url?.trim() || null, name: link.name, description, type: linkType, diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts index 2a5a083..976bd71 100644 --- a/lib/api/controllers/users/userId/deleteUserById.ts +++ b/lib/api/controllers/users/userId/deleteUserById.ts @@ -71,6 +71,10 @@ export default async function deleteUserById( // Delete archive folders removeFolder({ filePath: `archives/${collection.id}` }); + + await removeFolder({ + filePath: `archives/preview/${collection.id}`, + }); } // Delete collections after cleaning up related data diff --git a/lib/api/generatePreview.ts b/lib/api/generatePreview.ts new file mode 100644 index 0000000..6e81630 --- /dev/null +++ b/lib/api/generatePreview.ts @@ -0,0 +1,36 @@ +import Jimp from "jimp"; +import { prisma } from "./db"; +import createFile from "./storage/createFile"; +import createFolder from "./storage/createFolder"; + +const generatePreview = async ( + buffer: Buffer, + collectionId: number, + linkId: number +) => { + if (buffer && collectionId && linkId) { + // Load the image using Jimp + await Jimp.read(buffer, async (err, image) => { + if (image && !err) { + image?.resize(1280, Jimp.AUTO).quality(20); + const processedBuffer = await image?.getBufferAsync(Jimp.MIME_JPEG); + + createFile({ + data: processedBuffer, + filePath: `archives/preview/${collectionId}/${linkId}.jpeg`, + }).then(() => { + return prisma.link.update({ + where: { id: linkId }, + data: { + preview: `archives/preview/${collectionId}/${linkId}.jpeg`, + }, + }); + }); + } + }).catch((err) => { + console.error("Error processing the image:", err); + }); + } +}; + +export default generatePreview; diff --git a/lib/api/manageLinkFiles.ts b/lib/api/manageLinkFiles.ts new file mode 100644 index 0000000..7bacdab --- /dev/null +++ b/lib/api/manageLinkFiles.ts @@ -0,0 +1,61 @@ +import moveFile from "./storage/moveFile"; +import removeFile from "./storage/removeFile"; + +const removeFiles = async (linkId: number, collectionId: number) => { + // PDF + await removeFile({ + filePath: `archives/${collectionId}/${linkId}.pdf`, + }); + // Images + await removeFile({ + filePath: `archives/${collectionId}/${linkId}.png`, + }); + await removeFile({ + filePath: `archives/${collectionId}/${linkId}.jpeg`, + }); + await removeFile({ + filePath: `archives/${collectionId}/${linkId}.jpg`, + }); + // Preview + await removeFile({ + filePath: `archives/preview/${collectionId}/${linkId}.jpeg`, + }); + // Readability + await removeFile({ + filePath: `archives/${collectionId}/${linkId}_readability.json`, + }); +}; + +const moveFiles = async (linkId: number, from: number, to: number) => { + await moveFile( + `archives/${from}/${linkId}.pdf`, + `archives/${to}/${linkId}.pdf` + ); + + await moveFile( + `archives/${from}/${linkId}.png`, + `archives/${to}/${linkId}.png` + ); + + await moveFile( + `archives/${from}/${linkId}.jpeg`, + `archives/${to}/${linkId}.jpeg` + ); + + await moveFile( + `archives/${from}/${linkId}.jpg`, + `archives/${to}/${linkId}.jpg` + ); + + await moveFile( + `archives/preview/${from}/${linkId}.jpeg`, + `archives/preview/${to}/${linkId}.jpeg` + ); + + await moveFile( + `archives/${from}/${linkId}_readability.json`, + `archives/${to}/${linkId}_readability.json` + ); +}; + +export { removeFiles, moveFiles }; diff --git a/lib/client/generateLinkHref.ts b/lib/client/generateLinkHref.ts index 47c1888..f012ab9 100644 --- a/lib/client/generateLinkHref.ts +++ b/lib/client/generateLinkHref.ts @@ -16,24 +16,30 @@ export const generateLinkHref = ( ): string => { // Return the links href based on the account's preference // If the user's preference is not available, return the original link - switch (account.linksRouteTo) { - case LinksRouteTo.ORIGINAL: - return link.url || ""; - case LinksRouteTo.PDF: - if (!pdfAvailable(link)) return link.url || ""; + if (account.linksRouteTo === LinksRouteTo.ORIGINAL && link.type === "url") { + return link.url || ""; + } else if (account.linksRouteTo === LinksRouteTo.PDF || link.type === "pdf") { + if (!pdfAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`; - case LinksRouteTo.READABLE: - if (!readabilityAvailable(link)) return link.url || ""; + return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`; + } else if ( + account.linksRouteTo === LinksRouteTo.READABLE && + link.type === "url" + ) { + if (!readabilityAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`; - case LinksRouteTo.SCREENSHOT: - if (!screenshotAvailable(link)) return link.url || ""; + return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`; + } else if ( + account.linksRouteTo === LinksRouteTo.SCREENSHOT || + link.type === "image" + ) { + console.log(link); + if (!screenshotAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ - link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg - }`; - default: - return link.url || ""; + return `/preserved/${link?.id}?format=${ + link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg + }`; + } else { + return link.url || ""; } }; diff --git a/package.json b/package.json index 4568588..6b03cf3 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "nodemon": "^3.0.2", "postcss": "^8.4.26", "prettier": "3.1.1", - "prisma": "^5.1.0", + "prisma": "^4.16.2", "tailwindcss": "^3.3.3", "ts-node": "^10.9.2", "typescript": "4.9.4" diff --git a/pages/api/v1/archives/[linkId].ts b/pages/api/v1/archives/[linkId].ts index b13e690..9a439ec 100644 --- a/pages/api/v1/archives/[linkId].ts +++ b/pages/api/v1/archives/[linkId].ts @@ -9,6 +9,9 @@ import formidable from "formidable"; import createFile from "@/lib/api/storage/createFile"; import fs from "fs"; import verifyToken from "@/lib/api/verifyToken"; +import Jimp from "jimp"; +import generatePreview from "@/lib/api/generatePreview"; +import createFolder from "@/lib/api/storage/createFolder"; export const config = { api: { @@ -73,83 +76,97 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { return res.send(file); } + } else if (req.method === "POST") { + const user = await verifyUser({ req, res }); + if (!user) return; + + const collectionPermissions = await getPermission({ + userId: user.id, + linkId, + }); + + const memberHasAccess = collectionPermissions?.members.some( + (e: UsersAndCollections) => e.userId === user.id && e.canCreate + ); + + if (!(collectionPermissions?.ownerId === user.id || memberHasAccess)) + return { response: "Collection is not accessible.", status: 401 }; + + // await uploadHandler(linkId, ) + + const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_FILE_SIZE); + + const form = formidable({ + maxFields: 1, + maxFiles: 1, + maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576, + }); + + form.parse(req, async (err, fields, files) => { + const allowedMIMETypes = [ + "application/pdf", + "image/png", + "image/jpg", + "image/jpeg", + ]; + + if ( + err || + !files.file || + !files.file[0] || + !allowedMIMETypes.includes(files.file[0].mimetype || "") + ) { + // Handle parsing error + return res.status(500).json({ + response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${MAX_UPLOAD_SIZE}MB.`, + }); + } else { + const fileBuffer = fs.readFileSync(files.file[0].filepath); + + const linkStillExists = await prisma.link.findUnique({ + where: { id: linkId }, + }); + + if (linkStillExists && files.file[0].mimetype?.includes("image")) { + const collectionId = collectionPermissions?.id as number; + createFolder({ + filePath: `archives/preview/${collectionId}`, + }); + + generatePreview(fileBuffer, collectionId, linkId); + } + + if (linkStillExists) { + await createFile({ + filePath: `archives/${collectionPermissions?.id}/${ + linkId + suffix + }`, + data: fileBuffer, + }); + + await prisma.link.update({ + where: { id: linkId }, + data: { + preview: files.file[0].mimetype?.includes("pdf") + ? "unavailable" + : undefined, + image: files.file[0].mimetype?.includes("image") + ? `archives/${collectionPermissions?.id}/${linkId + suffix}` + : null, + pdf: files.file[0].mimetype?.includes("pdf") + ? `archives/${collectionPermissions?.id}/${linkId + suffix}` + : null, + lastPreserved: new Date().toISOString(), + }, + }); + } + + fs.unlinkSync(files.file[0].filepath); + } + + return res.status(200).json({ + response: files, + }); + }); } - // else if (req.method === "POST") { - // const user = await verifyUser({ req, res }); - // if (!user) return; - - // const collectionPermissions = await getPermission({ - // userId: user.id, - // linkId, - // }); - - // const memberHasAccess = collectionPermissions?.members.some( - // (e: UsersAndCollections) => e.userId === user.id && e.canCreate - // ); - - // if (!(collectionPermissions?.ownerId === user.id || memberHasAccess)) - // return { response: "Collection is not accessible.", status: 401 }; - - // // await uploadHandler(linkId, ) - - // const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_FILE_SIZE); - - // const form = formidable({ - // maxFields: 1, - // maxFiles: 1, - // maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576, - // }); - - // form.parse(req, async (err, fields, files) => { - // const allowedMIMETypes = [ - // "application/pdf", - // "image/png", - // "image/jpg", - // "image/jpeg", - // ]; - - // if ( - // err || - // !files.file || - // !files.file[0] || - // !allowedMIMETypes.includes(files.file[0].mimetype || "") - // ) { - // // Handle parsing error - // return res.status(500).json({ - // response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${MAX_UPLOAD_SIZE}MB.`, - // }); - // } else { - // const fileBuffer = fs.readFileSync(files.file[0].filepath); - - // const linkStillExists = await prisma.link.findUnique({ - // where: { id: linkId }, - // }); - - // if (linkStillExists) { - // await createFile({ - // filePath: `archives/${collectionPermissions?.id}/${ - // linkId + suffix - // }`, - // data: fileBuffer, - // }); - - // await prisma.link.update({ - // where: { id: linkId }, - // data: { - // image: `archives/${collectionPermissions?.id}/${ - // linkId + suffix - // }`, - // lastPreserved: new Date().toISOString(), - // }, - // }); - // } - - // fs.unlinkSync(files.file[0].filepath); - // } - - // return res.status(200).json({ - // response: files, - // }); - // }); - // } } diff --git a/pages/api/v1/links/[id]/archive/index.ts b/pages/api/v1/links/[id]/archive/index.ts index 4693fac..78353bb 100644 --- a/pages/api/v1/links/[id]/archive/index.ts +++ b/pages/api/v1/links/[id]/archive/index.ts @@ -2,8 +2,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "@/lib/api/db"; import verifyUser from "@/lib/api/verifyUser"; import isValidUrl from "@/lib/shared/isValidUrl"; -import removeFile from "@/lib/api/storage/removeFile"; import { Collection, Link } from "@prisma/client"; +import { removeFiles } from "@/lib/api/manageLinkFiles"; const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5; @@ -80,16 +80,5 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => { }, }); - await removeFile({ - filePath: `archives/${link.collection.id}/${link.id}.pdf`, - }); - await removeFile({ - filePath: `archives/${link.collection.id}/${link.id}.png`, - }); - await removeFile({ - filePath: `archives/${link.collection.id}/${link.id}_readability.json`, - }); - await removeFile({ - filePath: `archives/preview/${link.collection.id}/${link.id}.png`, - }); + await removeFiles(link.id, link.collection.id); }; diff --git a/store/links.ts b/store/links.ts index 408a3ee..c2c3a8a 100644 --- a/store/links.ts +++ b/store/links.ts @@ -1,5 +1,8 @@ import { create } from "zustand"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { + ArchivedFormat, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; import useTagStore from "./tags"; import useCollectionStore from "./collections"; @@ -19,6 +22,10 @@ type LinkStore = { addLink: ( body: LinkIncludingShortenedCollectionAndTags ) => Promise; + uploadFile: ( + link: LinkIncludingShortenedCollectionAndTags, + file: File + ) => Promise; getLink: (linkId: number, publicRoute?: boolean) => Promise; updateLink: ( link: LinkIncludingShortenedCollectionAndTags @@ -79,6 +86,82 @@ const useLinkStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, + uploadFile: async (link, file) => { + let fileType: ArchivedFormat | null = null; + let linkType: "url" | "image" | "pdf" | null = null; + + if (file?.type === "image/jpg" || file.type === "image/jpeg") { + fileType = ArchivedFormat.jpeg; + linkType = "image"; + } else if (file.type === "image/png") { + fileType = ArchivedFormat.png; + linkType = "image"; + } else if (file.type === "application/pdf") { + fileType = ArchivedFormat.pdf; + linkType = "pdf"; + } else { + return { ok: false, data: "Invalid file type." }; + } + + const response = await fetch("/api/v1/links", { + body: JSON.stringify({ + ...link, + type: linkType, + name: link.name ? link.name : file.name, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + const createdLink: LinkIncludingShortenedCollectionAndTags = data.response; + + console.log(data); + + if (response.ok) { + const formBody = new FormData(); + file && formBody.append("file", file); + + await fetch( + `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, + { + body: formBody, + method: "POST", + } + ); + + // get file extension + const extension = file.name.split(".").pop() || ""; + + set((state) => ({ + links: [ + { + ...createdLink, + image: + linkType === "image" + ? `archives/${createdLink.collectionId}/${ + createdLink.id + extension + }` + : null, + pdf: + linkType === "pdf" + ? `archives/${createdLink.collectionId}/${ + createdLink.id + ".pdf" + }` + : null, + }, + ...state.links, + ], + })); + useTagStore.getState().setTags(); + useCollectionStore.getState().setCollections(); + } + + return { ok: response.ok, data: data.response }; + }, getLink: async (linkId, publicRoute) => { const path = publicRoute ? `/api/v1/public/links/${linkId}` diff --git a/yarn.lock b/yarn.lock index 37c6839..d016ae9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1301,10 +1301,10 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14" integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg== -"@prisma/engines@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.1.0.tgz#4ccf7f344eaeee08ca1e4a1bb2dc14e36ff1d5ec" - integrity sha512-HqaFsnPmZOdMWkPq6tT2eTVTQyaAXEDdKszcZ4yc7DGMBIYRP6j/zAJTtZUG9SsMV8FaucdL5vRyxY/p5Ni28g== +"@prisma/engines@4.16.2": + version "4.16.2" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f" + integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw== "@radix-ui/primitive@1.0.1": version "1.0.1" @@ -5038,12 +5038,12 @@ pretty-format@^3.8.0: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== -prisma@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.1.0.tgz#29e316b54844f5694a83017a9781a6d6f7cb99ea" - integrity sha512-wkXvh+6wxk03G8qwpZMOed4Y3j+EQ+bMTlvbDZHeal6k1E8QuGKzRO7DRXlE1NV0WNgOAas8kwZqcLETQ2+BiQ== +prisma@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e" + integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g== dependencies: - "@prisma/engines" "5.1.0" + "@prisma/engines" "4.16.2" process@^0.11.10: version "0.11.10"