- ) : undefined}
+ ) : // : link.type === "monolith" ? (
+ //
+ // )
+ undefined}
>
);
}
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx
index 3e349ed..9a9f5fa 100644
--- a/components/ModalContent/EditCollectionSharingModal.tsx
+++ b/components/ModalContent/EditCollectionSharingModal.tsx
@@ -68,6 +68,7 @@ export default function EditCollectionSharingModal({
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
+ archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx
index e2fc181..0284687 100644
--- a/components/ModalContent/NewLinkModal.tsx
+++ b/components/ModalContent/NewLinkModal.tsx
@@ -30,6 +30,7 @@ export default function NewLinkModal({ onClose }: Props) {
image: "",
pdf: "",
readable: "",
+ monolith: "",
textContent: "",
collection: {
name: "",
diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx
index 66ae53c..87e84a7 100644
--- a/components/ModalContent/PreservedFormatsModal.tsx
+++ b/components/ModalContent/PreservedFormatsModal.tsx
@@ -12,12 +12,14 @@ import { useSession } from "next-auth/react";
import {
pdfAvailable,
readabilityAvailable,
+ monolithAvailable,
screenshotAvailable,
} from "@/lib/shared/getArchiveValidity";
import PreservedFormatRow from "@/components/PreserverdFormatRow";
import useAccountStore from "@/store/account";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
+import { BeatLoader } from "react-spinners";
type Props = {
onClose: Function;
@@ -41,6 +43,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
+ archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
@@ -58,6 +61,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
username: account.username as string,
image: account.image as string,
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
+ archiveAsMonolith: account.archiveAsScreenshot as boolean,
archiveAsPDF: account.archiveAsPDF as boolean,
});
}
@@ -72,6 +76,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
+ (collectionOwner.archiveAsMonolith === true
+ ? link.monolith && link.monolith !== "pending"
+ : true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
@@ -80,6 +87,15 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
);
};
+ const atLeastOneFormatAvailable = () => {
+ return (
+ screenshotAvailable(link) ||
+ pdfAvailable(link) ||
+ readabilityAvailable(link) ||
+ monolithAvailable(link)
+ );
+ };
+
useEffect(() => {
(async () => {
const data = await getLink(link.id as number, isPublic);
@@ -108,7 +124,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
clearInterval(interval);
}
};
- }, [link, getLink]);
+ }, [link?.monolith]);
const updateArchive = async () => {
const load = toast.loading(t("sending_request"));
@@ -133,56 +149,81 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
{t("preserved_formats")}
- {isReady() &&
- (screenshotAvailable(link) ||
- pdfAvailable(link) ||
- readabilityAvailable(link)) ? (
+ {screenshotAvailable(link) ||
+ pdfAvailable(link) ||
+ readabilityAvailable(link) ||
+ monolithAvailable(link) ? (
{t("available_formats")}
) : (
""
)}
-
- {isReady() ? (
- <>
- {screenshotAvailable(link) ? (
-
- ) : undefined}
- {pdfAvailable(link) ? (
-
- ) : undefined}
- {readabilityAvailable(link) ? (
-
- ) : undefined}
- >
- ) : (
-
-
+
+ {monolithAvailable(link) ? (
+
+ ) : undefined}
+
+ {screenshotAvailable(link) ? (
+
+ ) : undefined}
+
+ {pdfAvailable(link) ? (
+
+ ) : undefined}
+
+ {readabilityAvailable(link) ? (
+
+ ) : undefined}
+
+ {!isReady() && !atLeastOneFormatAvailable() ? (
+
+
+
{t("preservation_in_queue")}
{t("check_back_later")}
- )}
+ ) : !isReady() && atLeastOneFormatAvailable() ? (
+
+
+
{t("there_are_more_formats")}
+
{t("check_back_later")}
+
+ ) : undefined}
diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx
index e7100cd..ce0f21d 100644
--- a/components/PreserverdFormatRow.tsx
+++ b/components/PreserverdFormatRow.tsx
@@ -4,6 +4,7 @@ import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
+import toast from "react-hot-toast";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
@@ -60,7 +61,7 @@ export default function PreservedFormatRow({
clearInterval(interval);
}
};
- }, [link?.image, link?.pdf, link?.readable]);
+ }, [link?.image, link?.pdf, link?.readable, link?.monolith]);
const handleDownload = () => {
const path = `/api/v1/archives/${link?.id}?format=${format}`;
@@ -68,10 +69,15 @@ export default function PreservedFormatRow({
.then((response) => {
if (response.ok) {
// Create a temporary link and click it to trigger the download
- const link = document.createElement("a");
- link.href = path;
- link.download = format === ArchivedFormat.pdf ? "PDF" : "Screenshot";
- link.click();
+ const anchorElement = document.createElement("a");
+ anchorElement.href = path;
+ anchorElement.download =
+ format === ArchivedFormat.monolith
+ ? "Webpage"
+ : format === ArchivedFormat.pdf
+ ? "PDF"
+ : "Screenshot";
+ anchorElement.click();
} else {
console.error("Failed to download file");
}
diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx
index d4c5eb5..b600236 100644
--- a/components/ReadableView.tsx
+++ b/components/ReadableView.tsx
@@ -81,9 +81,11 @@ export default function ReadableView({ link }: Props) {
(link?.image === "pending" ||
link?.pdf === "pending" ||
link?.readable === "pending" ||
+ link?.monolith === "pending" ||
!link?.image ||
!link?.pdf ||
- !link?.readable)
+ !link?.readable ||
+ !link?.monolith)
) {
interval = setInterval(() => getLink(link.id as number), 5000);
} else {
@@ -97,7 +99,7 @@ export default function ReadableView({ link }: Props) {
clearInterval(interval);
}
};
- }, [link?.image, link?.pdf, link?.readable]);
+ }, [link?.image, link?.pdf, link?.readable, link?.monolith]);
const rgbToHex = (r: number, g: number, b: number): string =>
"#" +
diff --git a/lib/api/archiveHandler.ts b/lib/api/archiveHandler.ts
index 0cb514d..c5e5f09 100644
--- a/lib/api/archiveHandler.ts
+++ b/lib/api/archiveHandler.ts
@@ -1,15 +1,16 @@
import { LaunchOptions, chromium, devices } from "playwright";
import { prisma } from "./db";
-import createFile from "./storage/createFile";
-import sendToWayback from "./sendToWayback";
-import { Readability } from "@mozilla/readability";
-import { JSDOM } from "jsdom";
-import DOMPurify from "dompurify";
+import sendToWayback from "./preservationScheme/sendToWayback";
import { Collection, Link, User } from "@prisma/client";
-import validateUrlSize from "./validateUrlSize";
+import fetchHeaders from "./fetchHeaders";
import createFolder from "./storage/createFolder";
-import generatePreview from "./generatePreview";
import { removeFiles } from "./manageLinkFiles";
+import handleMonolith from "./preservationScheme/handleMonolith";
+import handleReadablility from "./preservationScheme/handleReadablility";
+import handleArchivePreview from "./preservationScheme/handleArchivePreview";
+import handleScreenshotAndPdf from "./preservationScheme/handleScreenshotAndPdf";
+import imageHandler from "./preservationScheme/imageHandler";
+import pdfHandler from "./preservationScheme/pdfHandler";
type LinksAndCollectionAndOwner = Link & {
collection: Collection & {
@@ -20,6 +21,18 @@ type LinksAndCollectionAndOwner = Link & {
const BROWSER_TIMEOUT = Number(process.env.BROWSER_TIMEOUT) || 5;
export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
+ const timeoutPromise = new Promise((_, reject) => {
+ setTimeout(
+ () =>
+ reject(
+ new Error(
+ `Browser has been open for more than ${BROWSER_TIMEOUT} minutes.`
+ )
+ ),
+ BROWSER_TIMEOUT * 60000
+ );
+ });
+
// allow user to configure a proxy
let browserOptions: LaunchOptions = {};
if (process.env.PROXY) {
@@ -39,18 +52,6 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
const page = await context.newPage();
- const timeoutPromise = new Promise((_, reject) => {
- setTimeout(
- () =>
- reject(
- new Error(
- `Browser has been open for more than ${BROWSER_TIMEOUT} minutes.`
- )
- ),
- BROWSER_TIMEOUT * 60000
- );
- });
-
createFolder({
filePath: `archives/preview/${link.collectionId}`,
});
@@ -62,17 +63,11 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
try {
await Promise.race([
(async () => {
- const validatedUrl = link.url
- ? await validateUrlSize(link.url)
- : undefined;
+ const user = link.collection?.owner;
- if (
- validatedUrl === null &&
- process.env.IGNORE_URL_SIZE_LIMIT !== "true"
- )
- throw "Something went wrong while retrieving the file size.";
+ const header = link.url ? await fetchHeaders(link.url) : undefined;
- const contentType = validatedUrl?.get("content-type");
+ const contentType = header?.get("content-type");
let linkType = "url";
let imageExtension = "png";
@@ -84,12 +79,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
else if (contentType.includes("image/png")) imageExtension = "png";
}
- const user = link.collection?.owner;
-
- // send to archive.org
- if (user.archiveAsWaybackMachine && link.url) sendToWayback(link.url);
-
- const targetLink = await prisma.link.update({
+ await prisma.link.update({
where: { id: link.id },
data: {
type: linkType,
@@ -104,6 +94,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
readable: !link.readable?.startsWith("archive")
? "pending"
: undefined,
+ monolith: !link.monolith?.startsWith("archive")
+ ? "pending"
+ : undefined,
preview: !link.readable?.startsWith("archive")
? "pending"
: undefined,
@@ -111,6 +104,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
},
});
+ // send to archive.org
+ if (user.archiveAsWaybackMachine && link.url) sendToWayback(link.url);
+
if (linkType === "image" && !link.image?.startsWith("archive")) {
await imageHandler(link, imageExtension); // archive image (jpeg/png)
return;
@@ -124,151 +120,37 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
const content = await page.content();
- // TODO single file
- // const session = await page.context().newCDPSession(page);
- // const doc = await session.send("Page.captureSnapshot", {
- // format: "mhtml",
- // });
- // const saveDocLocally = (doc: any) => {
- // console.log(doc);
- // return createFile({
- // data: doc,
- // filePath: `archives/${targetLink.collectionId}/${link.id}.mhtml`,
- // });
- // };
- // saveDocLocally(doc.data);
+ // Preview
+ if (
+ !link.preview?.startsWith("archives") &&
+ !link.preview?.startsWith("unavailable")
+ )
+ await handleArchivePreview(link, page);
// Readability
- const window = new JSDOM("").window;
- const purify = DOMPurify(window);
- const cleanedUpContent = purify.sanitize(content);
- const dom = new JSDOM(cleanedUpContent, { url: link.url || "" });
- const article = new Readability(dom.window.document).parse();
- const articleText = article?.textContent
- .replace(/ +(?= )/g, "") // strip out multiple spaces
- .replace(/(\r\n|\n|\r)/gm, " "); // strip out line breaks
if (
- articleText &&
- articleText !== "" &&
- !link.readable?.startsWith("archive")
- ) {
- await createFile({
- data: JSON.stringify(article),
- filePath: `archives/${targetLink.collectionId}/${link.id}_readability.json`,
- });
-
- await prisma.link.update({
- where: { id: link.id },
- data: {
- readable: `archives/${targetLink.collectionId}/${link.id}_readability.json`,
- textContent: articleText,
- },
- });
- }
-
- // Preview
-
- const ogImageUrl = await page.evaluate(() => {
- const metaTag = document.querySelector('meta[property="og:image"]');
- return metaTag ? (metaTag as any).content : null;
- });
-
- if (ogImageUrl) {
- console.log("Found og:image URL:", ogImageUrl);
-
- // Download the image
- const imageResponse = await page.goto(ogImageUrl);
-
- // Check if imageResponse is not null
- if (imageResponse && !link.preview?.startsWith("archive")) {
- const buffer = await imageResponse.body();
- await generatePreview(buffer, link.collectionId, link.id);
- }
-
- await page.goBack();
- } else if (!link.preview?.startsWith("archive")) {
- console.log("No og:image found");
- await page
- .screenshot({ type: "jpeg", quality: 20 })
- .then((screenshot) => {
- return createFile({
- data: screenshot,
- 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`,
- },
- });
- });
- }
+ !link.readable?.startsWith("archives") &&
+ !link.readable?.startsWith("unavailable")
+ )
+ await handleReadablility(content, link);
// Screenshot/PDF
- await page.evaluate(
- autoScroll,
- Number(process.env.AUTOSCROLL_TIMEOUT) || 30
- );
+ if (
+ (!link.image?.startsWith("archives") &&
+ !link.image?.startsWith("unavailable")) ||
+ (!link.pdf?.startsWith("archives") &&
+ !link.pdf?.startsWith("unavailable"))
+ )
+ await handleScreenshotAndPdf(link, page, user);
- // Check if the user hasn't deleted the link by the time we're done scrolling
- const linkExists = await prisma.link.findUnique({
- where: { id: link.id },
- });
- if (linkExists) {
- const processingPromises = [];
-
- if (
- user.archiveAsScreenshot &&
- !link.image?.startsWith("archive")
- ) {
- processingPromises.push(
- page.screenshot({ fullPage: true }).then((screenshot) => {
- return createFile({
- data: screenshot,
- filePath: `archives/${linkExists.collectionId}/${link.id}.png`,
- });
- })
- );
- }
-
- // apply administrator's defined pdf margins or default to 15px
- const margins = {
- top: process.env.PDF_MARGIN_TOP || "15px",
- bottom: process.env.PDF_MARGIN_BOTTOM || "15px",
- };
-
- if (user.archiveAsPDF && !link.pdf?.startsWith("archive")) {
- processingPromises.push(
- page
- .pdf({
- width: "1366px",
- height: "1931px",
- printBackground: true,
- margin: margins,
- })
- .then((pdf) => {
- return createFile({
- data: pdf,
- filePath: `archives/${linkExists.collectionId}/${link.id}.pdf`,
- });
- })
- );
- }
- await Promise.allSettled(processingPromises);
- await prisma.link.update({
- where: { id: link.id },
- data: {
- image: user.archiveAsScreenshot
- ? `archives/${linkExists.collectionId}/${link.id}.png`
- : undefined,
- pdf: user.archiveAsPDF
- ? `archives/${linkExists.collectionId}/${link.id}.pdf`
- : undefined,
- },
- });
- }
+ // Monolith
+ if (
+ !link.monolith?.startsWith("archive") &&
+ !link.monolith?.startsWith("unavailable") &&
+ user.archiveAsMonolith &&
+ link.url
+ )
+ await handleMonolith(link, content);
}
})(),
timeoutPromise,
@@ -293,6 +175,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
image: !finalLink.image?.startsWith("archives")
? "unavailable"
: undefined,
+ monolith: !finalLink.monolith?.startsWith("archives")
+ ? "unavailable"
+ : undefined,
pdf: !finalLink.pdf?.startsWith("archives")
? "unavailable"
: undefined,
@@ -308,76 +193,3 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
await browser.close();
}
}
-
-const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
- const timeoutPromise = new Promise
((_, reject) => {
- setTimeout(() => {
- reject(new Error(`Webpage was too long to be archived.`));
- }, AUTOSCROLL_TIMEOUT * 1000);
- });
-
- const scrollingPromise = new Promise((resolve) => {
- let totalHeight = 0;
- let distance = 100;
- let scrollDown = setInterval(() => {
- let scrollHeight = document.body.scrollHeight;
- window.scrollBy(0, distance);
- totalHeight += distance;
- if (totalHeight >= scrollHeight) {
- clearInterval(scrollDown);
- window.scroll(0, 0);
- resolve();
- }
- }, 100);
- });
-
- await Promise.race([scrollingPromise, timeoutPromise]);
-};
-
-const imageHandler = async ({ url, id }: Link, extension: string) => {
- const image = await fetch(url as string).then((res) => res.blob());
-
- const buffer = Buffer.from(await image.arrayBuffer());
-
- const linkExists = await prisma.link.findUnique({
- where: { id },
- });
-
- if (linkExists) {
- await createFile({
- data: buffer,
- filePath: `archives/${linkExists.collectionId}/${id}.${extension}`,
- });
-
- await prisma.link.update({
- where: { id },
- data: {
- image: `archives/${linkExists.collectionId}/${id}.${extension}`,
- },
- });
- }
-};
-
-const pdfHandler = async ({ url, id }: Link) => {
- const pdf = await fetch(url as string).then((res) => res.blob());
-
- const buffer = Buffer.from(await pdf.arrayBuffer());
-
- const linkExists = await prisma.link.findUnique({
- where: { id },
- });
-
- if (linkExists) {
- await createFile({
- data: buffer,
- filePath: `archives/${linkExists.collectionId}/${id}.pdf`,
- });
-
- await prisma.link.update({
- where: { id },
- data: {
- pdf: `archives/${linkExists.collectionId}/${id}.pdf`,
- },
- });
- }
-};
diff --git a/lib/api/controllers/collections/collectionId/deleteCollectionById.ts b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts
index e8fe51a..1e21809 100644
--- a/lib/api/controllers/collections/collectionId/deleteCollectionById.ts
+++ b/lib/api/controllers/collections/collectionId/deleteCollectionById.ts
@@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
import getPermission from "@/lib/api/getPermission";
-import { Collection, UsersAndCollections } from "@prisma/client";
+import { UsersAndCollections } from "@prisma/client";
import removeFolder from "@/lib/api/storage/removeFolder";
export default async function deleteCollection(
@@ -58,6 +58,7 @@ export default async function deleteCollection(
});
await removeFolder({ filePath: `archives/${collectionId}` });
+ await removeFolder({ filePath: `archives/preview/${collectionId}` });
await removeFromOrders(userId, collectionId);
@@ -100,6 +101,7 @@ async function deleteSubCollections(collectionId: number) {
});
await removeFolder({ filePath: `archives/${subCollection.id}` });
+ await removeFolder({ filePath: `archives/preview/${subCollection.id}` });
}
}
diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts
index 0086e93..976f1f1 100644
--- a/lib/api/controllers/links/getLinks.ts
+++ b/lib/api/controllers/links/getLinks.ts
@@ -2,7 +2,8 @@ import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
export default async function getLink(userId: number, query: LinkRequestQuery) {
- const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
+ const POSTGRES_IS_ENABLED =
+ process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
diff --git a/lib/api/controllers/links/linkId/deleteLinkById.ts b/lib/api/controllers/links/linkId/deleteLinkById.ts
index dba90cb..abff6f0 100644
--- a/lib/api/controllers/links/linkId/deleteLinkById.ts
+++ b/lib/api/controllers/links/linkId/deleteLinkById.ts
@@ -1,7 +1,6 @@
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) {
diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts
index 7f77aeb..8056c11 100644
--- a/lib/api/controllers/links/postLink.ts
+++ b/lib/api/controllers/links/postLink.ts
@@ -1,10 +1,8 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
-import getTitle from "@/lib/shared/getTitle";
-import { UsersAndCollections } from "@prisma/client";
-import getPermission from "@/lib/api/getPermission";
+import fetchTitleAndHeaders from "@/lib/shared/fetchTitleAndHeaders";
import createFolder from "@/lib/api/storage/createFolder";
-import validateUrlSize from "../../validateUrlSize";
+import setLinkCollection from "../../setLinkCollection";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
@@ -24,93 +22,10 @@ export default async function postLink(
}
}
- if (!link.collection.id && link.collection.name) {
- link.collection.name = link.collection.name.trim();
+ const linkCollection = await setLinkCollection(link, userId);
- // find the collection with the name and the user's id
- const findCollection = await prisma.collection.findFirst({
- where: {
- name: link.collection.name,
- ownerId: userId,
- parentId: link.collection.parentId,
- },
- });
-
- if (findCollection) {
- const collectionIsAccessible = await getPermission({
- userId,
- collectionId: findCollection.id,
- });
-
- const memberHasAccess = collectionIsAccessible?.members.some(
- (e: UsersAndCollections) => e.userId === userId && e.canCreate
- );
-
- if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
- return { response: "Collection is not accessible.", status: 401 };
-
- link.collection.id = findCollection.id;
- link.collection.ownerId = findCollection.ownerId;
- } else {
- const collection = await prisma.collection.create({
- data: {
- name: link.collection.name,
- ownerId: userId,
- },
- });
-
- link.collection.id = collection.id;
-
- await prisma.user.update({
- where: {
- id: userId,
- },
- data: {
- collectionOrder: {
- push: link.collection.id,
- },
- },
- });
- }
- } else if (link.collection.id) {
- const collectionIsAccessible = await getPermission({
- userId,
- collectionId: link.collection.id,
- });
-
- const memberHasAccess = collectionIsAccessible?.members.some(
- (e: UsersAndCollections) => e.userId === userId && e.canCreate
- );
-
- if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
- return { response: "Collection is not accessible.", status: 401 };
- } else if (!link.collection.id) {
- link.collection.name = "Unorganized";
- link.collection.parentId = null;
-
- // find the collection with the name "Unorganized" and the user's id
- const unorganizedCollection = await prisma.collection.findFirst({
- where: {
- name: "Unorganized",
- ownerId: userId,
- },
- });
-
- link.collection.id = unorganizedCollection?.id;
-
- await prisma.user.update({
- where: {
- id: userId,
- },
- data: {
- collectionOrder: {
- push: link.collection.id,
- },
- },
- });
- } else {
- return { response: "Uncaught error.", status: 500 };
- }
+ if (!linkCollection)
+ return { response: "Collection is not accessible.", status: 400 };
const user = await prisma.user.findUnique({
where: {
@@ -124,8 +39,6 @@ export default async function postLink(
const urlWithoutWww = hasWwwPrefix ? url?.replace(`://www.`, "://") : url;
const urlWithWww = hasWwwPrefix ? url : url?.replace("://", `://www.`);
- console.log(url, urlWithoutWww, urlWithWww);
-
const existingLink = await prisma.link.findFirst({
where: {
OR: [{ url: urlWithWww }, { url: urlWithoutWww }],
@@ -135,8 +48,6 @@ export default async function postLink(
},
});
- console.log(url, urlWithoutWww, urlWithWww, "DONE!");
-
if (existingLink)
return {
response: "Link already exists",
@@ -147,30 +58,23 @@ export default async function postLink(
const numberOfLinksTheUserHas = await prisma.link.count({
where: {
collection: {
- ownerId: userId,
+ ownerId: linkCollection.ownerId,
},
},
});
- if (numberOfLinksTheUserHas + 1 > MAX_LINKS_PER_USER)
+ if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
- response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
+ response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
- link.collection.name = link.collection.name.trim();
-
- const title =
- !(link.name && link.name !== "") && link.url
- ? await getTitle(link.url)
- : "";
+ const { title, headers } = await fetchTitleAndHeaders(link.url || "");
const name =
link.name && link.name !== "" ? link.name : link.url ? title : "";
- const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
-
- const contentType = validatedUrl?.get("content-type");
+ const contentType = headers?.get("content-type");
let linkType = "url";
let imageExtension = "png";
@@ -190,7 +94,7 @@ export default async function postLink(
type: linkType,
collection: {
connect: {
- id: link.collection.id,
+ id: linkCollection.id,
},
},
tags: {
@@ -198,14 +102,14 @@ export default async function postLink(
where: {
name_ownerId: {
name: tag.name.trim(),
- ownerId: link.collection.ownerId,
+ ownerId: linkCollection.ownerId,
},
},
create: {
name: tag.name.trim(),
owner: {
connect: {
- id: link.collection.ownerId,
+ id: linkCollection.ownerId,
},
},
},
diff --git a/lib/api/controllers/migration/importFromHTMLFile.ts b/lib/api/controllers/migration/importFromHTMLFile.ts
index 21fb3c4..6e6cf71 100644
--- a/lib/api/controllers/migration/importFromHTMLFile.ts
+++ b/lib/api/controllers/migration/importFromHTMLFile.ts
@@ -31,7 +31,7 @@ export default async function importFromHTMLFile(
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
- response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
+ response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
diff --git a/lib/api/controllers/migration/importFromLinkwarden.ts b/lib/api/controllers/migration/importFromLinkwarden.ts
index fb486cb..5e872e8 100644
--- a/lib/api/controllers/migration/importFromLinkwarden.ts
+++ b/lib/api/controllers/migration/importFromLinkwarden.ts
@@ -26,7 +26,7 @@ export default async function importFromLinkwarden(
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
- response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
+ response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
diff --git a/lib/api/controllers/migration/importFromWallabag.ts b/lib/api/controllers/migration/importFromWallabag.ts
index 6f9f404..568952a 100644
--- a/lib/api/controllers/migration/importFromWallabag.ts
+++ b/lib/api/controllers/migration/importFromWallabag.ts
@@ -47,7 +47,7 @@ export default async function importFromWallabag(
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return {
- response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
+ response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
status: 400,
};
diff --git a/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts b/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts
index e94a3e1..1b48984 100644
--- a/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts
+++ b/lib/api/controllers/public/links/getPublicLinksUnderCollection.ts
@@ -4,7 +4,8 @@ import { LinkRequestQuery, Sort } from "@/types/global";
export default async function getLink(
query: Omit
) {
- const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
+ const POSTGRES_IS_ENABLED =
+ process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
diff --git a/lib/api/controllers/public/users/getPublicUser.ts b/lib/api/controllers/public/users/getPublicUser.ts
index 04c7994..68b6fab 100644
--- a/lib/api/controllers/public/users/getPublicUser.ts
+++ b/lib/api/controllers/public/users/getPublicUser.ts
@@ -75,6 +75,7 @@ export default async function getPublicUser(
username: lessSensitiveInfo.username,
image: lessSensitiveInfo.image,
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
+ archiveAsMonolith: lessSensitiveInfo.archiveAsMonolith,
archiveAsPDF: lessSensitiveInfo.archiveAsPDF,
};
diff --git a/lib/api/controllers/session/createSession.ts b/lib/api/controllers/session/createSession.ts
index c2ff37e..c1a6aa7 100644
--- a/lib/api/controllers/session/createSession.ts
+++ b/lib/api/controllers/session/createSession.ts
@@ -21,12 +21,12 @@ export default async function createSession(
jti: crypto.randomUUID(),
},
maxAge: expiryDateSecond || 604800,
- secret: process.env.NEXTAUTH_SECRET,
+ secret: process.env.NEXTAUTH_SECRET as string,
});
const tokenBody = await decode({
token,
- secret: process.env.NEXTAUTH_SECRET,
+ secret: process.env.NEXTAUTH_SECRET as string,
});
const createToken = await prisma.accessToken.create({
diff --git a/lib/api/controllers/tokens/postToken.ts b/lib/api/controllers/tokens/postToken.ts
index f88030d..faedd8a 100644
--- a/lib/api/controllers/tokens/postToken.ts
+++ b/lib/api/controllers/tokens/postToken.ts
@@ -65,12 +65,12 @@ export default async function postToken(
jti: crypto.randomUUID(),
},
maxAge: expiryDateSecond || 604800,
- secret: process.env.NEXTAUTH_SECRET,
+ secret: process.env.NEXTAUTH_SECRET as string,
});
const tokenBody = await decode({
token,
- secret: process.env.NEXTAUTH_SECRET,
+ secret: process.env.NEXTAUTH_SECRET as string,
});
const createToken = await prisma.accessToken.create({
diff --git a/lib/api/controllers/users/userId/deleteUserById.ts b/lib/api/controllers/users/userId/deleteUserById.ts
index 9b65304..a6e6106 100644
--- a/lib/api/controllers/users/userId/deleteUserById.ts
+++ b/lib/api/controllers/users/userId/deleteUserById.ts
@@ -80,7 +80,7 @@ export default async function deleteUserById(
});
// Delete archive folders
- removeFolder({ filePath: `archives/${collection.id}` });
+ await removeFolder({ filePath: `archives/${collection.id}` });
await removeFolder({
filePath: `archives/preview/${collection.id}`,
diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts
index b6e8bea..ebae737 100644
--- a/lib/api/controllers/users/userId/updateUserById.ts
+++ b/lib/api/controllers/users/userId/updateUserById.ts
@@ -207,6 +207,7 @@ export default async function updateUserById(
),
locale: i18n.locales.includes(data.locale) ? data.locale : "en",
archiveAsScreenshot: data.archiveAsScreenshot,
+ archiveAsMonolith: data.archiveAsMonolith,
archiveAsPDF: data.archiveAsPDF,
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
linksRouteTo: data.linksRouteTo,
diff --git a/lib/api/validateUrlSize.ts b/lib/api/fetchHeaders.ts
similarity index 65%
rename from lib/api/validateUrlSize.ts
rename to lib/api/fetchHeaders.ts
index d5826b8..f423661 100644
--- a/lib/api/validateUrlSize.ts
+++ b/lib/api/fetchHeaders.ts
@@ -2,7 +2,7 @@ import fetch from "node-fetch";
import https from "https";
import { SocksProxyAgent } from "socks-proxy-agent";
-export default async function validateUrlSize(url: string) {
+export default async function fetchHeaders(url: string) {
if (process.env.IGNORE_URL_SIZE_LIMIT === "true") return null;
try {
@@ -29,13 +29,17 @@ export default async function validateUrlSize(url: string) {
};
}
- const response = await fetch(url, fetchOpts);
+ const responsePromise = fetch(url, fetchOpts);
- const totalSizeMB =
- Number(response.headers.get("content-length")) / Math.pow(1024, 2);
- if (totalSizeMB > (Number(process.env.NEXT_PUBLIC_MAX_FILE_SIZE) || 30))
- return null;
- else return response.headers;
+ const timeoutPromise = new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(new Error("Fetch header timeout"));
+ }, 10 * 1000); // Stop after 10 seconds
+ });
+
+ const response = await Promise.race([responsePromise, timeoutPromise]);
+
+ return (response as Response)?.headers || null;
} catch (err) {
console.log(err);
return null;
diff --git a/lib/api/generatePreview.ts b/lib/api/generatePreview.ts
index 6e81630..2693be2 100644
--- a/lib/api/generatePreview.ts
+++ b/lib/api/generatePreview.ts
@@ -1,7 +1,6 @@
import Jimp from "jimp";
import { prisma } from "./db";
import createFile from "./storage/createFile";
-import createFolder from "./storage/createFolder";
const generatePreview = async (
buffer: Buffer,
@@ -9,27 +8,39 @@ const generatePreview = async (
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);
+ try {
+ const image = await Jimp.read(buffer);
- createFile({
- data: processedBuffer,
- filePath: `archives/preview/${collectionId}/${linkId}.jpeg`,
- }).then(() => {
- return prisma.link.update({
- where: { id: linkId },
- data: {
- preview: `archives/preview/${collectionId}/${linkId}.jpeg`,
- },
- });
- });
+ if (!image) {
+ console.log("Error generating preview: Image not found");
+ return;
}
- }).catch((err) => {
+
+ image.resize(1280, Jimp.AUTO).quality(20);
+ const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
+
+ if (
+ Buffer.byteLength(processedBuffer) >
+ 1024 * 1024 * Number(process.env.PREVIEW_MAX_BUFFER || 0.1)
+ ) {
+ console.log("Error generating preview: Buffer size exceeded");
+ return;
+ }
+
+ await createFile({
+ data: processedBuffer,
+ filePath: `archives/preview/${collectionId}/${linkId}.jpeg`,
+ });
+
+ await prisma.link.update({
+ where: { id: linkId },
+ data: {
+ preview: `archives/preview/${collectionId}/${linkId}.jpeg`,
+ },
+ });
+ } catch (err) {
console.error("Error processing the image:", err);
- });
+ }
}
};
diff --git a/lib/api/getPermission.ts b/lib/api/getPermission.ts
index 93dd04c..61dc5c5 100644
--- a/lib/api/getPermission.ts
+++ b/lib/api/getPermission.ts
@@ -3,14 +3,12 @@ import { prisma } from "@/lib/api/db";
type Props = {
userId: number;
collectionId?: number;
- collectionName?: string;
linkId?: number;
};
export default async function getPermission({
userId,
collectionId,
- collectionName,
linkId,
}: Props) {
if (linkId) {
@@ -26,11 +24,10 @@ export default async function getPermission({
});
return check;
- } else if (collectionId || collectionName) {
+ } else if (collectionId) {
const check = await prisma.collection.findFirst({
where: {
- id: collectionId || undefined,
- name: collectionName || undefined,
+ id: collectionId,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
},
include: { members: true },
diff --git a/lib/api/manageLinkFiles.ts b/lib/api/manageLinkFiles.ts
index 7bacdab..7498772 100644
--- a/lib/api/manageLinkFiles.ts
+++ b/lib/api/manageLinkFiles.ts
@@ -16,6 +16,10 @@ const removeFiles = async (linkId: number, collectionId: number) => {
await removeFile({
filePath: `archives/${collectionId}/${linkId}.jpg`,
});
+ // HTML
+ await removeFile({
+ filePath: `archives/${collectionId}/${linkId}.html`,
+ });
// Preview
await removeFile({
filePath: `archives/preview/${collectionId}/${linkId}.jpeg`,
@@ -47,6 +51,11 @@ const moveFiles = async (linkId: number, from: number, to: number) => {
`archives/${to}/${linkId}.jpg`
);
+ await moveFile(
+ `archives/${from}/${linkId}.html`,
+ `archives/${to}/${linkId}.html`
+ );
+
await moveFile(
`archives/preview/${from}/${linkId}.jpeg`,
`archives/preview/${to}/${linkId}.jpeg`
diff --git a/lib/api/preservationScheme/handleArchivePreview.ts b/lib/api/preservationScheme/handleArchivePreview.ts
new file mode 100644
index 0000000..9714985
--- /dev/null
+++ b/lib/api/preservationScheme/handleArchivePreview.ts
@@ -0,0 +1,61 @@
+import { Collection, Link, User } from "@prisma/client";
+import { Page } from "playwright";
+import generatePreview from "../generatePreview";
+import createFile from "../storage/createFile";
+import { prisma } from "../db";
+
+type LinksAndCollectionAndOwner = Link & {
+ collection: Collection & {
+ owner: User;
+ };
+};
+
+const handleArchivePreview = async (
+ link: LinksAndCollectionAndOwner,
+ page: Page
+) => {
+ const ogImageUrl = await page.evaluate(() => {
+ const metaTag = document.querySelector('meta[property="og:image"]');
+ return metaTag ? (metaTag as any).content : null;
+ });
+
+ if (ogImageUrl) {
+ console.log("Found og:image URL:", ogImageUrl);
+
+ // Download the image
+ const imageResponse = await page.goto(ogImageUrl);
+
+ // Check if imageResponse is not null
+ if (imageResponse && !link.preview?.startsWith("archive")) {
+ const buffer = await imageResponse.body();
+ generatePreview(buffer, link.collectionId, link.id);
+ }
+
+ await page.goBack();
+ } else if (!link.preview?.startsWith("archive")) {
+ console.log("No og:image found");
+ await page
+ .screenshot({ type: "jpeg", quality: 20 })
+ .then(async (screenshot) => {
+ if (
+ Buffer.byteLength(screenshot) >
+ 1024 * 1024 * Number(process.env.PREVIEW_MAX_BUFFER || 0.1)
+ )
+ return console.log("Error generating preview: Buffer size exceeded");
+
+ await createFile({
+ data: screenshot,
+ filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
+ });
+
+ await prisma.link.update({
+ where: { id: link.id },
+ data: {
+ preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
+ },
+ });
+ });
+ }
+};
+
+export default handleArchivePreview;
diff --git a/lib/api/preservationScheme/handleMonolith.ts b/lib/api/preservationScheme/handleMonolith.ts
new file mode 100644
index 0000000..f7c08f5
--- /dev/null
+++ b/lib/api/preservationScheme/handleMonolith.ts
@@ -0,0 +1,46 @@
+import { execSync } from "child_process";
+import createFile from "../storage/createFile";
+import { prisma } from "../db";
+import { Link } from "@prisma/client";
+
+const handleMonolith = async (link: Link, content: string) => {
+ if (!link.url) return;
+
+ try {
+ let html = execSync(
+ `monolith - -I -b ${link.url} ${
+ process.env.MONOLITH_CUSTOM_OPTIONS || "-j -F -s"
+ } -o -`,
+ {
+ timeout: 120000,
+ maxBuffer: 1024 * 1024 * Number(process.env.MONOLITH_MAX_BUFFER || 5),
+ input: content,
+ }
+ );
+
+ if (!html?.length)
+ return console.error("Error archiving as Monolith: Empty buffer");
+
+ if (
+ Buffer.byteLength(html) >
+ 1024 * 1024 * Number(process.env.MONOLITH_MAX_BUFFER || 6)
+ )
+ return console.error("Error archiving as Monolith: Buffer size exceeded");
+
+ await createFile({
+ data: html,
+ filePath: `archives/${link.collectionId}/${link.id}.html`,
+ }).then(async () => {
+ await prisma.link.update({
+ where: { id: link.id },
+ data: {
+ monolith: `archives/${link.collectionId}/${link.id}.html`,
+ },
+ });
+ });
+ } catch (err) {
+ console.log("Error running MONOLITH:", err);
+ }
+};
+
+export default handleMonolith;
diff --git a/lib/api/preservationScheme/handleReadablility.ts b/lib/api/preservationScheme/handleReadablility.ts
new file mode 100644
index 0000000..e274fc2
--- /dev/null
+++ b/lib/api/preservationScheme/handleReadablility.ts
@@ -0,0 +1,51 @@
+import { Readability } from "@mozilla/readability";
+import { JSDOM } from "jsdom";
+import DOMPurify from "dompurify";
+import { prisma } from "../db";
+import createFile from "../storage/createFile";
+import { Link } from "@prisma/client";
+
+const handleReadablility = async (content: string, link: Link) => {
+ const window = new JSDOM("").window;
+ const purify = DOMPurify(window);
+ const cleanedUpContent = purify.sanitize(content);
+ const dom = new JSDOM(cleanedUpContent, { url: link.url || "" });
+ const article = new Readability(dom.window.document).parse();
+ const articleText = article?.textContent
+ .replace(/ +(?= )/g, "") // strip out multiple spaces
+ .replace(/(\r\n|\n|\r)/gm, " "); // strip out line breaks
+
+ if (articleText && articleText !== "") {
+ const collectionId = (
+ await prisma.link.findUnique({
+ where: { id: link.id },
+ select: { collectionId: true },
+ })
+ )?.collectionId;
+
+ const data = JSON.stringify(article);
+
+ if (
+ Buffer.byteLength(data, "utf8") >
+ 1024 * 1024 * Number(process.env.READABILITY_MAX_BUFFER || 1)
+ )
+ return console.error(
+ "Error archiving as Readability: Buffer size exceeded"
+ );
+
+ await createFile({
+ data,
+ filePath: `archives/${collectionId}/${link.id}_readability.json`,
+ });
+
+ await prisma.link.update({
+ where: { id: link.id },
+ data: {
+ readable: `archives/${collectionId}/${link.id}_readability.json`,
+ textContent: articleText,
+ },
+ });
+ }
+};
+
+export default handleReadablility;
diff --git a/lib/api/preservationScheme/handleScreenshotAndPdf.ts b/lib/api/preservationScheme/handleScreenshotAndPdf.ts
new file mode 100644
index 0000000..fee5126
--- /dev/null
+++ b/lib/api/preservationScheme/handleScreenshotAndPdf.ts
@@ -0,0 +1,122 @@
+import { Collection, Link, User } from "@prisma/client";
+import { Page } from "playwright";
+import createFile from "../storage/createFile";
+import { prisma } from "../db";
+
+type LinksAndCollectionAndOwner = Link & {
+ collection: Collection & {
+ owner: User;
+ };
+};
+const handleScreenshotAndPdf = async (
+ link: LinksAndCollectionAndOwner,
+ page: Page,
+ user: User
+) => {
+ await page.evaluate(autoScroll, Number(process.env.AUTOSCROLL_TIMEOUT) || 30);
+
+ // Check if the user hasn't deleted the link by the time we're done scrolling
+ const linkExists = await prisma.link.findUnique({
+ where: { id: link.id },
+ });
+ if (linkExists) {
+ const processingPromises = [];
+
+ if (user.archiveAsScreenshot && !link.image?.startsWith("archive")) {
+ processingPromises.push(
+ page
+ .screenshot({ fullPage: true, type: "jpeg" })
+ .then(async (screenshot) => {
+ if (
+ Buffer.byteLength(screenshot) >
+ 1024 * 1024 * Number(process.env.SCREENSHOT_MAX_BUFFER || 2)
+ )
+ return console.log(
+ "Error archiving as Screenshot: Buffer size exceeded"
+ );
+
+ await createFile({
+ data: screenshot,
+ filePath: `archives/${linkExists.collectionId}/${link.id}.jpeg`,
+ });
+ await prisma.link.update({
+ where: { id: link.id },
+ data: {
+ image: user.archiveAsScreenshot
+ ? `archives/${linkExists.collectionId}/${link.id}.jpeg`
+ : undefined,
+ },
+ });
+ })
+ );
+ }
+
+ const margins = {
+ top: process.env.PDF_MARGIN_TOP || "15px",
+ bottom: process.env.PDF_MARGIN_BOTTOM || "15px",
+ };
+
+ if (user.archiveAsPDF && !link.pdf?.startsWith("archive")) {
+ processingPromises.push(
+ page
+ .pdf({
+ width: "1366px",
+ height: "1931px",
+ printBackground: true,
+ margin: margins,
+ })
+ .then(async (pdf) => {
+ if (
+ Buffer.byteLength(pdf) >
+ 1024 * 1024 * Number(process.env.PDF_MAX_BUFFER || 2)
+ )
+ return console.log(
+ "Error archiving as PDF: Buffer size exceeded"
+ );
+
+ await createFile({
+ data: pdf,
+ filePath: `archives/${linkExists.collectionId}/${link.id}.pdf`,
+ });
+
+ await prisma.link.update({
+ where: { id: link.id },
+ data: {
+ pdf: user.archiveAsPDF
+ ? `archives/${linkExists.collectionId}/${link.id}.pdf`
+ : undefined,
+ },
+ });
+ })
+ );
+ }
+ await Promise.allSettled(processingPromises);
+ }
+};
+
+const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
+ const timeoutPromise = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, AUTOSCROLL_TIMEOUT * 1000);
+ });
+
+ const scrollingPromise = new Promise((resolve) => {
+ let totalHeight = 0;
+ let distance = 100;
+ let scrollDown = setInterval(() => {
+ let scrollHeight = document.body.scrollHeight;
+ window.scrollBy(0, distance);
+ totalHeight += distance;
+ if (totalHeight >= scrollHeight) {
+ clearInterval(scrollDown);
+ window.scroll(0, 0);
+ resolve();
+ }
+ }, 100);
+ });
+
+ await Promise.race([scrollingPromise, timeoutPromise]);
+};
+
+export default handleScreenshotAndPdf;
diff --git a/lib/api/preservationScheme/imageHandler.ts b/lib/api/preservationScheme/imageHandler.ts
new file mode 100644
index 0000000..273c010
--- /dev/null
+++ b/lib/api/preservationScheme/imageHandler.ts
@@ -0,0 +1,38 @@
+import { Link } from "@prisma/client";
+import { prisma } from "../db";
+import createFile from "../storage/createFile";
+import generatePreview from "../generatePreview";
+
+const imageHandler = async ({ url, id }: Link, extension: string) => {
+ const image = await fetch(url as string).then((res) => res.blob());
+
+ const buffer = Buffer.from(await image.arrayBuffer());
+
+ if (
+ Buffer.byteLength(buffer) >
+ 1024 * 1024 * Number(process.env.SCREENSHOT_MAX_BUFFER || 2)
+ )
+ return console.log("Error archiving as Screenshot: Buffer size exceeded");
+
+ const linkExists = await prisma.link.findUnique({
+ where: { id },
+ });
+
+ if (linkExists) {
+ await generatePreview(buffer, linkExists.collectionId, id);
+
+ await createFile({
+ data: buffer,
+ filePath: `archives/${linkExists.collectionId}/${id}.${extension}`,
+ });
+
+ await prisma.link.update({
+ where: { id },
+ data: {
+ image: `archives/${linkExists.collectionId}/${id}.${extension}`,
+ },
+ });
+ }
+};
+
+export default imageHandler;
diff --git a/lib/api/preservationScheme/pdfHandler.ts b/lib/api/preservationScheme/pdfHandler.ts
new file mode 100644
index 0000000..7216cef
--- /dev/null
+++ b/lib/api/preservationScheme/pdfHandler.ts
@@ -0,0 +1,35 @@
+import { Link } from "@prisma/client";
+import { prisma } from "../db";
+import createFile from "../storage/createFile";
+
+const pdfHandler = async ({ url, id }: Link) => {
+ const pdf = await fetch(url as string).then((res) => res.blob());
+
+ const buffer = Buffer.from(await pdf.arrayBuffer());
+
+ if (
+ Buffer.byteLength(buffer) >
+ 1024 * 1024 * Number(process.env.PDF_MAX_BUFFER || 2)
+ )
+ return console.log("Error archiving as PDF: Buffer size exceeded");
+
+ const linkExists = await prisma.link.findUnique({
+ where: { id },
+ });
+
+ if (linkExists) {
+ await createFile({
+ data: buffer,
+ filePath: `archives/${linkExists.collectionId}/${id}.pdf`,
+ });
+
+ await prisma.link.update({
+ where: { id },
+ data: {
+ pdf: `archives/${linkExists.collectionId}/${id}.pdf`,
+ },
+ });
+ }
+};
+
+export default pdfHandler;
diff --git a/lib/api/sendToWayback.ts b/lib/api/preservationScheme/sendToWayback.ts
similarity index 100%
rename from lib/api/sendToWayback.ts
rename to lib/api/preservationScheme/sendToWayback.ts
diff --git a/lib/api/setLinkCollection.ts b/lib/api/setLinkCollection.ts
new file mode 100644
index 0000000..901e585
--- /dev/null
+++ b/lib/api/setLinkCollection.ts
@@ -0,0 +1,89 @@
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
+import { prisma } from "./db";
+import getPermission from "./getPermission";
+import { UsersAndCollections } from "@prisma/client";
+
+const setLinkCollection = async (
+ link: LinkIncludingShortenedCollectionAndTags,
+ userId: number
+) => {
+ if (link?.collection?.id && typeof link?.collection?.id === "number") {
+ const existingCollection = await prisma.collection.findUnique({
+ where: {
+ id: link.collection.id,
+ },
+ });
+
+ if (!existingCollection) return null;
+
+ const collectionIsAccessible = await getPermission({
+ userId,
+ collectionId: existingCollection.id,
+ });
+
+ const memberHasAccess = collectionIsAccessible?.members.some(
+ (e: UsersAndCollections) => e.userId === userId && e.canCreate
+ );
+
+ if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess))
+ return null;
+
+ return existingCollection;
+ } else if (link?.collection?.name) {
+ if (link.collection.name === "Unorganized") {
+ const firstTopLevelUnorganizedCollection =
+ await prisma.collection.findFirst({
+ where: {
+ name: "Unorganized",
+ ownerId: userId,
+ parentId: null,
+ },
+ });
+
+ if (firstTopLevelUnorganizedCollection)
+ return firstTopLevelUnorganizedCollection;
+ }
+
+ const newCollection = await prisma.collection.create({
+ data: {
+ name: link.collection.name.trim(),
+ ownerId: userId,
+ },
+ });
+
+ await prisma.user.update({
+ where: {
+ id: userId,
+ },
+ data: {
+ collectionOrder: {
+ push: newCollection.id,
+ },
+ },
+ });
+
+ return newCollection;
+ } else {
+ const firstTopLevelUnorganizedCollection =
+ await prisma.collection.findFirst({
+ where: {
+ name: "Unorganized",
+ ownerId: userId,
+ parentId: null,
+ },
+ });
+
+ if (firstTopLevelUnorganizedCollection)
+ return firstTopLevelUnorganizedCollection;
+ else
+ return await prisma.collection.create({
+ data: {
+ name: "Unorganized",
+ ownerId: userId,
+ parentId: null,
+ },
+ });
+ }
+};
+
+export default setLinkCollection;
diff --git a/lib/api/storage/readFile.ts b/lib/api/storage/readFile.ts
index 350726d..fc9d2e7 100644
--- a/lib/api/storage/readFile.ts
+++ b/lib/api/storage/readFile.ts
@@ -10,6 +10,7 @@ import util from "util";
type ReturnContentTypes =
| "text/plain"
+ | "text/html"
| "image/jpeg"
| "image/png"
| "application/pdf"
@@ -61,6 +62,8 @@ export default async function readFile(filePath: string) {
contentType = "image/png";
} else if (filePath.endsWith("_readability.json")) {
contentType = "application/json";
+ } else if (filePath.endsWith(".html")) {
+ contentType = "text/html";
} else {
// if (filePath.endsWith(".jpg"))
contentType = "image/jpeg";
@@ -88,6 +91,8 @@ export default async function readFile(filePath: string) {
contentType = "image/png";
} else if (filePath.endsWith("_readability.json")) {
contentType = "application/json";
+ } else if (filePath.endsWith(".html")) {
+ contentType = "text/html";
} else {
// if (filePath.endsWith(".jpg"))
contentType = "image/jpeg";
diff --git a/lib/client/generateLinkHref.ts b/lib/client/generateLinkHref.ts
index f012ab9..721c671 100644
--- a/lib/client/generateLinkHref.ts
+++ b/lib/client/generateLinkHref.ts
@@ -8,6 +8,7 @@ import {
pdfAvailable,
readabilityAvailable,
screenshotAvailable,
+ monolithAvailable,
} from "../shared/getArchiveValidity";
export const generateLinkHref = (
@@ -33,12 +34,15 @@ export const generateLinkHref = (
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
}`;
+ } else if (account.linksRouteTo === LinksRouteTo.MONOLITH) {
+ if (!monolithAvailable(link)) return link.url || "";
+
+ return `/preserved/${link?.id}?format=${ArchivedFormat.monolith}`;
} else {
return link.url || "";
}
diff --git a/lib/shared/getTitle.ts b/lib/shared/fetchTitleAndHeaders.ts
similarity index 83%
rename from lib/shared/getTitle.ts
rename to lib/shared/fetchTitleAndHeaders.ts
index 01488fd..a777964 100644
--- a/lib/shared/getTitle.ts
+++ b/lib/shared/fetchTitleAndHeaders.ts
@@ -2,7 +2,7 @@ import fetch from "node-fetch";
import https from "https";
import { SocksProxyAgent } from "socks-proxy-agent";
-export default async function getTitle(url: string) {
+export default async function fetchTitleAndHeaders(url: string) {
try {
const httpsAgent = new https.Agent({
rejectUnauthorized:
@@ -41,12 +41,16 @@ export default async function getTitle(url: string) {
// regular expression to find the tag
let match = text.match(/([^<]*)<\/title>/);
- if (match) return match[1];
- else return "";
+
+ const title = match[1] || "";
+ const headers = (response as Response)?.headers || null;
+
+ return { title, headers };
} else {
- return "";
+ return { title: "", headers: null };
}
} catch (err) {
console.log(err);
+ return { title: "", headers: null };
}
}
diff --git a/lib/shared/getArchiveValidity.ts b/lib/shared/getArchiveValidity.ts
index 0da5504..8ec8c44 100644
--- a/lib/shared/getArchiveValidity.ts
+++ b/lib/shared/getArchiveValidity.ts
@@ -28,6 +28,17 @@ export function readabilityAvailable(
);
}
+export function monolithAvailable(
+ link: LinkIncludingShortenedCollectionAndTags
+) {
+ return (
+ link &&
+ link.monolith &&
+ link.monolith !== "pending" &&
+ link.monolith !== "unavailable"
+ );
+}
+
export function previewAvailable(link: any) {
return (
link &&
diff --git a/package.json b/package.json
index 0045802..db8cf5d 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"next-i18next": "^15.3.0",
"node-fetch": "^2.7.0",
"nodemailer": "^6.9.3",
- "playwright": "^1.43.1",
+ "playwright": "^1.45.0",
"react": "18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "18.2.0",
@@ -77,7 +77,7 @@
"zustand": "^4.3.8"
},
"devDependencies": {
- "@playwright/test": "^1.43.1",
+ "@playwright/test": "^1.45.0",
"@types/bcrypt": "^5.0.0",
"@types/dompurify": "^3.0.4",
"@types/jsdom": "^21.1.3",
diff --git a/pages/api/v1/archives/[linkId].ts b/pages/api/v1/archives/[linkId].ts
index 17e7518..faa3dd2 100644
--- a/pages/api/v1/archives/[linkId].ts
+++ b/pages/api/v1/archives/[linkId].ts
@@ -29,6 +29,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
+ else if (format === ArchivedFormat.monolith) suffix = ".html";
//@ts-ignore
if (!linkId || !suffix)
@@ -84,21 +85,42 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
linkId,
});
- const memberHasAccess = collectionPermissions?.members.some(
+ if (!collectionPermissions)
+ return { response: "Collection is not accessible.", status: 400 };
+
+ 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 };
+ if (!(collectionPermissions.ownerId === user.id || memberHasAccess))
+ return { response: "Collection is not accessible.", status: 400 };
// await uploadHandler(linkId, )
- const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_FILE_SIZE);
+ const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER || 30000);
+
+ const numberOfLinksTheUserHas = await prisma.link.count({
+ where: {
+ collection: {
+ ownerId: user.id,
+ },
+ },
+ });
+
+ if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
+ return {
+ response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
+ status: 400,
+ };
+
+ const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
+ process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
+ );
const form = formidable({
maxFields: 1,
maxFiles: 1,
- maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576,
+ maxFileSize: NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024,
});
form.parse(req, async (err, fields, files) => {
@@ -116,18 +138,26 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
!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.`,
+ return res.status(400).json({
+ response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
});
} else {
const fileBuffer = fs.readFileSync(files.file[0].filepath);
+ if (
+ Buffer.byteLength(fileBuffer) >
+ 1024 * 1024 * Number(NEXT_PUBLIC_MAX_FILE_BUFFER)
+ )
+ return res.status(400).json({
+ response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
+ });
+
const linkStillExists = await prisma.link.findUnique({
where: { id: linkId },
});
if (linkStillExists && files.file[0].mimetype?.includes("image")) {
- const collectionId = collectionPermissions?.id as number;
+ const collectionId = collectionPermissions.id as number;
createFolder({
filePath: `archives/preview/${collectionId}`,
});
@@ -137,9 +167,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (linkStillExists) {
await createFile({
- filePath: `archives/${collectionPermissions?.id}/${
- linkId + suffix
- }`,
+ filePath: `archives/${collectionPermissions.id}/${linkId + suffix}`,
data: fileBuffer,
});
@@ -150,10 +178,10 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
? "unavailable"
: undefined,
image: files.file[0].mimetype?.includes("image")
- ? `archives/${collectionPermissions?.id}/${linkId + suffix}`
+ ? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
pdf: files.file[0].mimetype?.includes("pdf")
- ? `archives/${collectionPermissions?.id}/${linkId + suffix}`
+ ? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
lastPreserved: new Date().toISOString(),
},
diff --git a/pages/api/v1/links/[id]/archive/index.ts b/pages/api/v1/links/[id]/archive/index.ts
index 78353bb..ef91a4e 100644
--- a/pages/api/v1/links/[id]/archive/index.ts
+++ b/pages/api/v1/links/[id]/archive/index.ts
@@ -76,6 +76,7 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
image: null,
pdf: null,
readable: null,
+ monolith: null,
preview: null,
},
});
diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx
index 58d474d..bdfbfe9 100644
--- a/pages/collections/[id].tsx
+++ b/pages/collections/[id].tsx
@@ -59,6 +59,7 @@ export default function Index() {
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
+ archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
@@ -76,6 +77,7 @@ export default function Index() {
username: account.username as string,
image: account.image as string,
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
+ archiveAsMonolith: account.archiveAsScreenshot as boolean,
archiveAsPDF: account.archiveAsPDF as boolean,
});
}
diff --git a/pages/preserved/[id].tsx b/pages/preserved/[id].tsx
index 84e614a..99fe033 100644
--- a/pages/preserved/[id].tsx
+++ b/pages/preserved/[id].tsx
@@ -6,6 +6,7 @@ import {
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import ReadableView from "@/components/ReadableView";
+import getServerSideProps from "@/lib/client/getServerSideProps";
export default function Index() {
const { links, getLink } = useLinkStore();
@@ -36,6 +37,12 @@ export default function Index() {
{link && Number(router.query.format) === ArchivedFormat.readability && (
)}
+ {link && Number(router.query.format) === ArchivedFormat.monolith && (
+
+ )}
{link && Number(router.query.format) === ArchivedFormat.pdf && (
);
}
+
+export { getServerSideProps };
diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx
index 7579c57..55c0167 100644
--- a/pages/settings/preference.tsx
+++ b/pages/settings/preference.tsx
@@ -25,14 +25,21 @@ export default function Appearance() {
const [archiveAsPDF, setArchiveAsPDF] = useState
(
account.archiveAsPDF
);
+
+ const [archiveAsMonolith, setArchiveAsMonolith] = useState(
+ account.archiveAsMonolith
+ );
+
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
useState(account.archiveAsWaybackMachine);
+
const [linksRouteTo, setLinksRouteTo] = useState(account.linksRouteTo);
useEffect(() => {
setUser({
...account,
archiveAsScreenshot,
+ archiveAsMonolith,
archiveAsPDF,
archiveAsWaybackMachine,
linksRouteTo,
@@ -41,6 +48,7 @@ export default function Appearance() {
}, [
account,
archiveAsScreenshot,
+ archiveAsMonolith,
archiveAsPDF,
archiveAsWaybackMachine,
linksRouteTo,
@@ -54,6 +62,7 @@ export default function Appearance() {
useEffect(() => {
if (!objectIsEmpty(account)) {
setArchiveAsScreenshot(account.archiveAsScreenshot);
+ setArchiveAsMonolith(account.archiveAsMonolith);
setArchiveAsPDF(account.archiveAsPDF);
setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
setLinksRouteTo(account.linksRouteTo);
@@ -125,6 +134,13 @@ export default function Appearance() {
state={archiveAsScreenshot}
onClick={() => setArchiveAsScreenshot(!archiveAsScreenshot)}
/>
+
+ setArchiveAsMonolith(!archiveAsMonolith)}
+ />
+
+
+