diff --git a/components/Modal/Link/PreservedFormats.tsx b/components/Modal/Link/PreservedFormats.tsx
index 410d3a1..914accf 100644
--- a/components/Modal/Link/PreservedFormats.tsx
+++ b/components/Modal/Link/PreservedFormats.tsx
@@ -76,8 +76,7 @@ export default function PreservedFormats() {
// Create a temporary link and click it to trigger the download
const link = document.createElement("a");
link.href = path;
- link.download =
- format === ArchivedFormat.screenshot ? "Screenshot" : "PDF";
+ link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
link.click();
} else {
console.error("Failed to download file");
@@ -102,7 +101,7 @@ export default function PreservedFormats() {
handleDownload(ArchivedFormat.screenshot)}
+ onClick={() => handleDownload(ArchivedFormat.png)}
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
>
diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts
index b93eeef..c6755b4 100644
--- a/lib/api/controllers/links/postLink.ts
+++ b/lib/api/controllers/links/postLink.ts
@@ -1,17 +1,20 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "@/lib/api/getTitle";
-import archive from "@/lib/api/archive";
+import urlHandler from "@/lib/api/urlHandler";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import createFolder from "@/lib/api/storage/createFolder";
+import pdfHandler from "../../pdfHandler";
+import validateUrlSize from "../../validateUrlSize";
+import imageHandler from "../../imageHandler";
export default async function postLink(
link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
try {
- new URL(link.url);
+ if (link.url) new URL(link.url);
} catch (error) {
return {
response:
@@ -45,13 +48,33 @@ export default async function postLink(
const description =
link.description && link.description !== ""
? link.description
- : await getTitle(link.url);
+ : link.url
+ ? await getTitle(link.url)
+ : undefined;
+
+ const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
+
+ if (validatedUrl === null)
+ return { response: "File is too large to be stored.", status: 400 };
+
+ const contentType = validatedUrl?.get("content-type");
+ let linkType = "url";
+ let imageExtension = "png";
+
+ if (!link.url) linkType = link.type;
+ else if (contentType === "application/pdf") linkType = "pdf";
+ else if (contentType?.startsWith("image")) {
+ linkType = "image";
+ if (contentType === "image/jpeg") imageExtension = "jpeg";
+ else if (contentType === "image/png") imageExtension = "png";
+ }
const newLink = await prisma.link.create({
data: {
url: link.url,
name: link.name,
description,
+ type: linkType,
readabilityPath: "pending",
collection: {
connectOrCreate: {
@@ -91,7 +114,15 @@ export default async function postLink(
createFolder({ filePath: `archives/${newLink.collectionId}` });
- archive(newLink.id, newLink.url, userId);
+ newLink.url && linkType === "url"
+ ? urlHandler(newLink.id, newLink.url, userId)
+ : undefined;
+
+ linkType === "pdf" ? pdfHandler(newLink.id, newLink.url) : undefined;
+
+ linkType === "image"
+ ? imageHandler(newLink.id, newLink.url, imageExtension)
+ : undefined;
return { response: newLink, status: 200 };
}
diff --git a/lib/api/imageHandler.ts b/lib/api/imageHandler.ts
new file mode 100644
index 0000000..b9437dd
--- /dev/null
+++ b/lib/api/imageHandler.ts
@@ -0,0 +1,37 @@
+import { prisma } from "@/lib/api/db";
+import createFile from "@/lib/api/storage/createFile";
+import fs from "fs";
+import path from "path";
+
+export default async function imageHandler(
+ linkId: number,
+ url: string | null,
+ extension: string,
+ file?: string
+) {
+ 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: linkId },
+ });
+
+ linkExists
+ ? await createFile({
+ data: buffer,
+ filePath: `archives/${linkExists.collectionId}/${linkId}.${extension}`,
+ })
+ : undefined;
+
+ await prisma.link.update({
+ where: { id: linkId },
+ data: {
+ screenshotPath: linkExists
+ ? `archives/${linkExists.collectionId}/${linkId}.${extension}`
+ : null,
+ pdfPath: null,
+ readabilityPath: null,
+ },
+ });
+}
diff --git a/lib/api/pdfHandler.ts b/lib/api/pdfHandler.ts
new file mode 100644
index 0000000..37dbef7
--- /dev/null
+++ b/lib/api/pdfHandler.ts
@@ -0,0 +1,44 @@
+import { prisma } from "@/lib/api/db";
+import createFile from "@/lib/api/storage/createFile";
+import fs from "fs";
+import path from "path";
+
+export default async function pdfHandler(
+ linkId: number,
+ url: string | null,
+ file?: string
+) {
+ const targetLink = await prisma.link.update({
+ where: { id: linkId },
+ data: {
+ pdfPath: "pending",
+ lastPreserved: new Date().toISOString(),
+ },
+ });
+
+ 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: linkId },
+ });
+
+ linkExists
+ ? await createFile({
+ data: buffer,
+ filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
+ })
+ : undefined;
+
+ await prisma.link.update({
+ where: { id: linkId },
+ data: {
+ pdfPath: linkExists
+ ? `archives/${linkExists.collectionId}/${linkId}.pdf`
+ : null,
+ readabilityPath: null,
+ screenshotPath: null,
+ },
+ });
+}
diff --git a/lib/api/storage/readFile.ts b/lib/api/storage/readFile.ts
index 64ff8d7..350726d 100644
--- a/lib/api/storage/readFile.ts
+++ b/lib/api/storage/readFile.ts
@@ -97,7 +97,7 @@ export default async function readFile(filePath: string) {
return {
file: "File not found.",
contentType: "text/plain",
- status: 400,
+ status: 404,
};
else {
const file = fs.readFileSync(creationPath);
diff --git a/lib/api/archive.ts b/lib/api/urlHandler.ts
similarity index 99%
rename from lib/api/archive.ts
rename to lib/api/urlHandler.ts
index e55d86d..8795d76 100644
--- a/lib/api/archive.ts
+++ b/lib/api/urlHandler.ts
@@ -6,7 +6,7 @@ import { Readability } from "@mozilla/readability";
import { JSDOM } from "jsdom";
import DOMPurify from "dompurify";
-export default async function archive(
+export default async function urlHandler(
linkId: number,
url: string,
userId: number
diff --git a/lib/api/validateUrlSize.ts b/lib/api/validateUrlSize.ts
new file mode 100644
index 0000000..cde7e33
--- /dev/null
+++ b/lib/api/validateUrlSize.ts
@@ -0,0 +1,13 @@
+export default async function validateUrlSize(url: string) {
+ try {
+ const response = await fetch(url, { method: "HEAD" });
+
+ const totalSizeMB =
+ Number(response.headers.get("content-length")) / Math.pow(1024, 2);
+ if (totalSizeMB > 50) return null;
+ else return response.headers;
+ } catch (err) {
+ console.log(err);
+ return null;
+ }
+}
diff --git a/pages/api/v1/archives/[linkId].ts b/pages/api/v1/archives/[linkId].ts
index 74f7738..faade60 100644
--- a/pages/api/v1/archives/[linkId].ts
+++ b/pages/api/v1/archives/[linkId].ts
@@ -10,7 +10,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
let suffix;
- if (format === ArchivedFormat.screenshot) suffix = ".png";
+ if (format === ArchivedFormat.png) suffix = ".png";
+ else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
@@ -43,6 +44,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
+
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
diff --git a/pages/api/v1/links/[id]/archive/index.ts b/pages/api/v1/links/[id]/archive/index.ts
index 65da756..1fce3ff 100644
--- a/pages/api/v1/links/[id]/archive/index.ts
+++ b/pages/api/v1/links/[id]/archive/index.ts
@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
-import archive from "@/lib/api/archive";
+import urlHandler from "@/lib/api/urlHandler";
import { prisma } from "@/lib/api/db";
import verifyUser from "@/lib/api/verifyUser";
@@ -41,7 +41,7 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
} minutes or create a new one.`,
});
- archive(link.id, link.url, user.id);
+ urlHandler(link.id, link.url, user.id);
return res.status(200).json({
response: "Link is being archived.",
});
diff --git a/prisma/migrations/20231125043215_add_link_type_field/migration.sql b/prisma/migrations/20231125043215_add_link_type_field/migration.sql
new file mode 100644
index 0000000..7a90c1c
--- /dev/null
+++ b/prisma/migrations/20231125043215_add_link_type_field/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Link" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'url',
+ALTER COLUMN "url" DROP NOT NULL;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 70ef764..dd91892 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -103,12 +103,13 @@ model UsersAndCollections {
model Link {
id Int @id @default(autoincrement())
name String
- url String
+ type String @default("url")
description String @default("")
pinnedBy User[]
collection Collection @relation(fields: [collectionId], references: [id])
collectionId Int
tags Tag[]
+ url String?
textContent String?
screenshotPath String?
pdfPath String?
diff --git a/types/global.ts b/types/global.ts
index a423d45..c1d1893 100644
--- a/types/global.ts
+++ b/types/global.ts
@@ -117,7 +117,14 @@ export type DeleteUserBody = {
};
export enum ArchivedFormat {
- screenshot,
+ png,
+ jpeg,
pdf,
readability,
}
+
+export enum LinkType {
+ url,
+ pdf,
+ image,
+}