rearchive protection
This commit is contained in:
parent
ccafc997fc
commit
56a281ae3d
|
@ -9,6 +9,7 @@ STORAGE_FOLDER=
|
||||||
AUTOSCROLL_TIMEOUT=
|
AUTOSCROLL_TIMEOUT=
|
||||||
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
||||||
IMPORT_SIZE_LIMIT=
|
IMPORT_SIZE_LIMIT=
|
||||||
|
RE_ARCHIVE_LIMIT=
|
||||||
|
|
||||||
# AWS S3 Settings
|
# AWS S3 Settings
|
||||||
SPACES_KEY=
|
SPACES_KEY=
|
||||||
|
|
|
@ -21,7 +21,6 @@ type Props = {
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
points?: { x: number; y: number };
|
points?: { x: number; y: number };
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
width?: number; // in rem
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Dropdown({
|
export default function Dropdown({
|
||||||
|
@ -29,7 +28,6 @@ export default function Dropdown({
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
width,
|
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [pos, setPos] = useState<{ x: number; y: number }>();
|
const [pos, setPos] = useState<{ x: number; y: number }>();
|
||||||
const [dropdownHeight, setDropdownHeight] = useState<number>();
|
const [dropdownHeight, setDropdownHeight] = useState<number>();
|
||||||
|
@ -60,7 +58,7 @@ export default function Dropdown({
|
||||||
|
|
||||||
setPos({ x: finalX, y: finalY });
|
setPos({ x: finalX, y: finalY });
|
||||||
}
|
}
|
||||||
}, [points, width, dropdownHeight]);
|
}, [points, dropdownHeight]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(!points || pos) && (
|
(!points || pos) && (
|
||||||
|
|
|
@ -254,7 +254,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
: undefined,
|
: undefined,
|
||||||
permissions === true
|
permissions === true
|
||||||
? {
|
? {
|
||||||
name: "Update Archive",
|
name: "Refresh Formats",
|
||||||
onClick: updateArchive,
|
onClick: updateArchive,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default function PreservedFormats() {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(`Link is being archived...`);
|
toast.success(`Link is being archived...`);
|
||||||
getLink(link?.id as number);
|
getLink(link?.id as number);
|
||||||
} else toast.error(data);
|
} else toast.error(data.response);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (format: "png" | "pdf") => {
|
const handleDownload = (format: "png" | "pdf") => {
|
||||||
|
@ -152,11 +152,18 @@ export default function PreservedFormats() {
|
||||||
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
||||||
{link?.collection.ownerId === session.data?.user.id ? (
|
{link?.collection.ownerId === session.data?.user.id ? (
|
||||||
<div
|
<div
|
||||||
className="w-full text-center bg-sky-600 p-1 rounded-md cursor-pointer select-none mt-3"
|
className={`w-full text-center bg-sky-700 p-1 rounded-md cursor-pointer select-none hover:bg-sky-600 duration-100 ${
|
||||||
|
link?.pdfPath &&
|
||||||
|
link?.screenshotPath &&
|
||||||
|
link?.pdfPath !== "pending" &&
|
||||||
|
link?.screenshotPath !== "pending"
|
||||||
|
? "mt-3"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
onClick={() => updateArchive()}
|
onClick={() => updateArchive()}
|
||||||
>
|
>
|
||||||
<p>Update Preserved Formats</p>
|
<p>Update Preserved Formats</p>
|
||||||
<p className="text-xs">(re-fetch)</p>
|
<p className="text-xs">(Refresh Formats)</p>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Link
|
<Link
|
||||||
|
@ -165,7 +172,14 @@ export default function PreservedFormats() {
|
||||||
""
|
""
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="sm:mt-3 text-gray-500 dark:text-gray-300 duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm"
|
className={`text-gray-500 dark:text-gray-300 duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
|
||||||
|
link?.pdfPath &&
|
||||||
|
link?.screenshotPath &&
|
||||||
|
link?.pdfPath !== "pending" &&
|
||||||
|
link?.screenshotPath !== "pending"
|
||||||
|
? "sm:mt-3"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
|
|
|
@ -30,11 +30,15 @@ export default function LinkModal({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{/* {method === "CREATE" && (
|
{method === "CREATE" ? (
|
||||||
<p className="text-xl text-black dark:text-white text-center">
|
<>
|
||||||
New Link
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
||||||
</p>
|
Create a New Link
|
||||||
)} */}
|
</p>
|
||||||
|
<AddOrEditLink toggleLinkModal={toggleLinkModal} method="CREATE" />
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
{activeLink && method === "UPDATE" ? (
|
{activeLink && method === "UPDATE" ? (
|
||||||
<>
|
<>
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">Edit Link</p>
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">Edit Link</p>
|
||||||
|
@ -46,19 +50,10 @@ export default function LinkModal({
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{method === "CREATE" ? (
|
{method === "FORMATS" ? (
|
||||||
<>
|
<>
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
||||||
Create a New Link
|
Preserved Formats
|
||||||
</p>
|
|
||||||
<AddOrEditLink toggleLinkModal={toggleLinkModal} method="CREATE" />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{activeLink && method === "FORMATS" ? (
|
|
||||||
<>
|
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
|
||||||
Manage Preserved Formats
|
|
||||||
</p>
|
</p>
|
||||||
<PreservedFormats />
|
<PreservedFormats />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -8,6 +8,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
|
import {
|
||||||
|
faPen,
|
||||||
|
faBoxesStacked,
|
||||||
|
faTrashCan,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import {
|
||||||
|
CollectionIncludingMembersAndLinkCount,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -40,6 +52,28 @@ export default function LinkLayout({ children }: Props) {
|
||||||
setSidebar(!sidebar);
|
setSidebar(!sidebar);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const session = useSession();
|
||||||
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const { links, removeLink } = useLinkStore();
|
||||||
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
const [linkCollection, setLinkCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>();
|
||||||
|
|
||||||
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (link)
|
||||||
|
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
|
||||||
|
}, [link]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
<ModalManagement />
|
||||||
|
@ -50,21 +84,91 @@ export default function LinkLayout({ children }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
|
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
|
||||||
<div className="flex gap-3 mb-5 duration-100">
|
<div className="flex gap-3 mb-5 duration-100 items-center justify-between">
|
||||||
<div
|
{/* <div
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
className="inline-flex gap-1 lg:hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700 lg:hover:bg-transparent lg:dark:hover:bg-transparent"
|
className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||||
Back
|
Back
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<div className="flex gap-5">
|
||||||
|
{link?.collection.ownerId === userId ||
|
||||||
|
linkCollection?.members.some(
|
||||||
|
(e) => e.userId === userId && e.canUpdate
|
||||||
|
) ? (
|
||||||
|
<div
|
||||||
|
title="Edit"
|
||||||
|
onClick={() => {
|
||||||
|
link
|
||||||
|
? setModal({
|
||||||
|
modal: "LINK",
|
||||||
|
state: true,
|
||||||
|
active: link,
|
||||||
|
method: "UPDATE",
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
}}
|
||||||
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPen}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
link
|
||||||
|
? setModal({
|
||||||
|
modal: "LINK",
|
||||||
|
state: true,
|
||||||
|
active: link,
|
||||||
|
method: "FORMATS",
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
}}
|
||||||
|
title="Preserved Formats"
|
||||||
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faBoxesStacked}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{link?.collection.ownerId === userId ||
|
||||||
|
linkCollection?.members.some(
|
||||||
|
(e) => e.userId === userId && e.canDelete
|
||||||
|
) ? (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (link?.id) {
|
||||||
|
removeLink(link.id);
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Delete"
|
||||||
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrashCan}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -11,36 +11,15 @@ export default async function archive(
|
||||||
url: string,
|
url: string,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// const checkExistingLink = await prisma.link.findFirst({
|
|
||||||
// where: {
|
|
||||||
// id: linkId,
|
|
||||||
// OR: [
|
|
||||||
// {
|
|
||||||
// screenshotPath: "pending",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// pdfPath: "pending",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (checkExistingLink) return "A request has already been made.";
|
|
||||||
|
|
||||||
const targetLink = await prisma.link.update({
|
const targetLink = await prisma.link.update({
|
||||||
where: {
|
where: { id: linkId },
|
||||||
id: linkId,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
screenshotPath: user?.archiveAsScreenshot ? "pending" : null,
|
screenshotPath: user?.archiveAsScreenshot ? "pending" : null,
|
||||||
pdfPath: user?.archiveAsPDF ? "pending" : null,
|
pdfPath: user?.archiveAsPDF ? "pending" : null,
|
||||||
readabilityPath: "pending",
|
readabilityPath: "pending",
|
||||||
|
lastPreserved: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,7 +35,6 @@ export default async function archive(
|
||||||
try {
|
try {
|
||||||
await page.goto(url, { waitUntil: "domcontentloaded" });
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
await page.goto(url);
|
|
||||||
const content = await page.content();
|
const content = await page.content();
|
||||||
|
|
||||||
// Readability
|
// Readability
|
||||||
|
@ -64,46 +42,35 @@ export default async function archive(
|
||||||
const window = new JSDOM("").window;
|
const window = new JSDOM("").window;
|
||||||
const purify = DOMPurify(window);
|
const purify = DOMPurify(window);
|
||||||
const cleanedUpContent = purify.sanitize(content);
|
const cleanedUpContent = purify.sanitize(content);
|
||||||
|
const dom = new JSDOM(cleanedUpContent, { url: url });
|
||||||
const dom = new JSDOM(cleanedUpContent, {
|
|
||||||
url: url,
|
|
||||||
});
|
|
||||||
|
|
||||||
const article = new Readability(dom.window.document).parse();
|
const article = new Readability(dom.window.document).parse();
|
||||||
|
|
||||||
await prisma.link.update({
|
|
||||||
where: {
|
|
||||||
id: linkId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
readabilityPath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await createFile({
|
await createFile({
|
||||||
data: JSON.stringify(article),
|
data: JSON.stringify(article),
|
||||||
filePath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
filePath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Screenshot/PDF
|
await prisma.link.update({
|
||||||
|
where: { id: linkId },
|
||||||
let faulty = true;
|
data: {
|
||||||
await page
|
readabilityPath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
||||||
.evaluate(autoScroll, Number(process.env.AUTOSCROLL_TIMEOUT) || 30)
|
|
||||||
.catch((e) => (faulty = false));
|
|
||||||
|
|
||||||
const linkExists = await prisma.link.findUnique({
|
|
||||||
where: {
|
|
||||||
id: linkId,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkExists && faulty) {
|
// Screenshot/PDF
|
||||||
if (user.archiveAsScreenshot) {
|
|
||||||
const screenshot = await page.screenshot({
|
|
||||||
fullPage: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let faulty = false;
|
||||||
|
await page
|
||||||
|
.evaluate(autoScroll, Number(process.env.AUTOSCROLL_TIMEOUT) || 30)
|
||||||
|
.catch((e) => (faulty = true));
|
||||||
|
|
||||||
|
const linkExists = await prisma.link.findUnique({
|
||||||
|
where: { id: linkId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (linkExists && !faulty) {
|
||||||
|
if (user.archiveAsScreenshot) {
|
||||||
|
const screenshot = await page.screenshot({ fullPage: true });
|
||||||
await createFile({
|
await createFile({
|
||||||
data: screenshot,
|
data: screenshot,
|
||||||
filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
|
filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
|
||||||
|
@ -124,10 +91,8 @@ export default async function archive(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLink = await prisma.link.update({
|
await prisma.link.update({
|
||||||
where: {
|
where: { id: linkId },
|
||||||
id: linkId,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
screenshotPath: user.archiveAsScreenshot
|
screenshotPath: user.archiveAsScreenshot
|
||||||
? `archives/${linkExists.collectionId}/${linkId}.png`
|
? `archives/${linkExists.collectionId}/${linkId}.png`
|
||||||
|
@ -139,21 +104,18 @@ export default async function archive(
|
||||||
});
|
});
|
||||||
} else if (faulty) {
|
} else if (faulty) {
|
||||||
await prisma.link.update({
|
await prisma.link.update({
|
||||||
where: {
|
where: { id: linkId },
|
||||||
id: linkId,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
screenshotPath: null,
|
screenshotPath: null,
|
||||||
pdfPath: null,
|
pdfPath: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await browser.close();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
return err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,11 +123,7 @@ export default async function archive(
|
||||||
const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
|
const autoScroll = async (AUTOSCROLL_TIMEOUT: number) => {
|
||||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(
|
reject(new Error(`Webpage was too long to be archived.`));
|
||||||
new Error(
|
|
||||||
`Auto scroll took too long (more than ${AUTOSCROLL_TIMEOUT} seconds).`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, AUTOSCROLL_TIMEOUT * 1000);
|
}, AUTOSCROLL_TIMEOUT * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ export default async function postLink(
|
||||||
url: link.url,
|
url: link.url,
|
||||||
name: link.name,
|
name: link.name,
|
||||||
description,
|
description,
|
||||||
|
readabilityPath: "pending",
|
||||||
collection: {
|
collection: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
|
||||||
import archive from "@/lib/api/archive";
|
import archive from "@/lib/api/archive";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
|
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
|
||||||
|
|
||||||
export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
|
@ -33,6 +35,20 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.method === "PUT") {
|
if (req.method === "PUT") {
|
||||||
|
if (
|
||||||
|
link?.lastPreserved &&
|
||||||
|
getTimezoneDifferenceInMinutes(new Date(), link?.lastPreserved) <
|
||||||
|
RE_ARCHIVE_LIMIT
|
||||||
|
)
|
||||||
|
return res.status(400).json({
|
||||||
|
response: `This link is currently being saved or has already been preserved. Please retry in ${
|
||||||
|
RE_ARCHIVE_LIMIT -
|
||||||
|
Math.floor(
|
||||||
|
getTimezoneDifferenceInMinutes(new Date(), link?.lastPreserved)
|
||||||
|
)
|
||||||
|
} minutes or create a new one.`,
|
||||||
|
});
|
||||||
|
|
||||||
archive(link.id, link.url, session.user.id);
|
archive(link.id, link.url, session.user.id);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
response: "Link is being archived.",
|
response: "Link is being archived.",
|
||||||
|
@ -42,3 +58,14 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
||||||
// Later?
|
// Later?
|
||||||
// else if (req.method === "DELETE") {}
|
// else if (req.method === "DELETE") {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTimezoneDifferenceInMinutes = (future: Date, past: Date) => {
|
||||||
|
const date1 = new Date(future);
|
||||||
|
const date2 = new Date(past);
|
||||||
|
|
||||||
|
const diffInMilliseconds = Math.abs(date1.getTime() - date2.getTime());
|
||||||
|
|
||||||
|
const diffInMinutes = diffInMilliseconds / (1000 * 60);
|
||||||
|
|
||||||
|
return diffInMinutes;
|
||||||
|
};
|
||||||
|
|
|
@ -220,7 +220,7 @@ export default function Index() {
|
||||||
if (target.id !== "expand-dropdown")
|
if (target.id !== "expand-dropdown")
|
||||||
setExpandDropdown(false);
|
setExpandDropdown(false);
|
||||||
}}
|
}}
|
||||||
className="absolute top-8 right-0 z-10 w-40"
|
className="absolute top-8 right-0 z-10 w-44"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -76,7 +76,11 @@ export default function Index() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: NodeJS.Timer | undefined;
|
let interval: NodeJS.Timer | undefined;
|
||||||
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
if (
|
||||||
|
link?.screenshotPath === "pending" ||
|
||||||
|
link?.pdfPath === "pending" ||
|
||||||
|
link?.readabilityPath === "pending"
|
||||||
|
) {
|
||||||
interval = setInterval(() => getLink(link.id as number), 5000);
|
interval = setInterval(() => getLink(link.id as number), 5000);
|
||||||
} else {
|
} else {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
|
@ -190,6 +194,7 @@ export default function Index() {
|
||||||
<p>•</p>
|
<p>•</p>
|
||||||
<Link
|
<Link
|
||||||
href={link?.url || ""}
|
href={link?.url || ""}
|
||||||
|
title={link?.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:opacity-60 duration-100 break-all"
|
className="hover:opacity-60 duration-100 break-all"
|
||||||
>
|
>
|
||||||
|
@ -240,46 +245,52 @@ export default function Index() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5 h-full">
|
<div className="flex flex-col gap-5 h-full">
|
||||||
{link &&
|
{link?.readabilityPath?.startsWith("archives") ? (
|
||||||
link?.readabilityPath &&
|
|
||||||
link?.readabilityPath !== "pending" ? (
|
|
||||||
<div
|
<div
|
||||||
className="line-break px-5"
|
className="line-break px-3"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
|
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
|
||||||
<p className="text-center text-2xl text-black dark:text-white">
|
{link?.readabilityPath === "pending" ? (
|
||||||
There is no reader view for this webpage
|
<p className="text-center">
|
||||||
</p>
|
Generating readable format, please wait...
|
||||||
<p className="text-center text-sm text-black dark:text-white">
|
</p>
|
||||||
{link?.collection.ownerId === userId
|
) : (
|
||||||
? "You can update (refetch) the preserved formats by managing them below"
|
<>
|
||||||
: "The collections owners can refetch the preserved formats"}
|
<p className="text-center text-2xl text-black dark:text-white">
|
||||||
</p>
|
There is no reader view for this webpage
|
||||||
{link?.collection.ownerId === userId ? (
|
</p>
|
||||||
<div
|
<p className="text-center text-sm text-black dark:text-white">
|
||||||
onClick={() =>
|
{link?.collection.ownerId === userId
|
||||||
link
|
? "You can update (refetch) the preserved formats by managing them below"
|
||||||
? setModal({
|
: "The collections owners can refetch the preserved formats"}
|
||||||
modal: "LINK",
|
</p>
|
||||||
state: true,
|
{link?.collection.ownerId === userId ? (
|
||||||
active: link,
|
<div
|
||||||
method: "FORMATS",
|
onClick={() =>
|
||||||
})
|
link
|
||||||
: undefined
|
? setModal({
|
||||||
}
|
modal: "LINK",
|
||||||
className="mt-4 flex gap-2 w-fit mx-auto relative items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100"
|
state: true,
|
||||||
>
|
active: link,
|
||||||
<FontAwesomeIcon
|
method: "FORMATS",
|
||||||
icon={faBoxesStacked}
|
})
|
||||||
className="w-5 h-5 duration-100"
|
: undefined
|
||||||
/>
|
}
|
||||||
<p>Manage preserved formats</p>
|
className="mt-4 flex gap-2 w-fit mx-auto relative items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100"
|
||||||
</div>
|
>
|
||||||
) : undefined}
|
<FontAwesomeIcon
|
||||||
|
icon={faBoxesStacked}
|
||||||
|
className="w-5 h-5 duration-100"
|
||||||
|
/>
|
||||||
|
<p>Manage preserved formats</p>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Link" ADD COLUMN "lastPreserved" TIMESTAMP(3);
|
|
@ -107,6 +107,8 @@ model Link {
|
||||||
pdfPath String?
|
pdfPath String?
|
||||||
readabilityPath String?
|
readabilityPath String?
|
||||||
|
|
||||||
|
lastPreserved DateTime?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt @default(now())
|
updatedAt DateTime @updatedAt @default(now())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ declare global {
|
||||||
STORAGE_FOLDER?: string;
|
STORAGE_FOLDER?: string;
|
||||||
AUTOSCROLL_TIMEOUT?: string;
|
AUTOSCROLL_TIMEOUT?: string;
|
||||||
IMPORT_SIZE_LIMIT?: string;
|
IMPORT_SIZE_LIMIT?: string;
|
||||||
|
RE_ARCHIVE_LIMIT?: string;
|
||||||
|
|
||||||
SPACES_KEY?: string;
|
SPACES_KEY?: string;
|
||||||
SPACES_SECRET?: string;
|
SPACES_SECRET?: string;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue