Add Single file archive method.
This commit is contained in:
parent
5990d4ce2d
commit
5fe6a5b19a
|
@ -45,6 +45,10 @@ PROXY_BYPASS=
|
||||||
PDF_MARGIN_TOP=
|
PDF_MARGIN_TOP=
|
||||||
PDF_MARGIN_BOTTOM=
|
PDF_MARGIN_BOTTOM=
|
||||||
|
|
||||||
|
# Singlefile archive settings
|
||||||
|
SINGLEFILE_ARCHIVE_COMMAND= # single-file "{{URL}}" --dump-content
|
||||||
|
SINGLEFILE_ARCHIVE_HTTP_API= # http://singlefile:3000/
|
||||||
|
|
||||||
#
|
#
|
||||||
# SSO Providers
|
# SSO Providers
|
||||||
#
|
#
|
||||||
|
|
|
@ -57,7 +57,7 @@ We've forked the old version from the current repository into [this repo](https:
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 📸 Auto capture a screenshot, PDF, and readable view of each webpage.
|
- 📸 Auto capture a screenshot, PDF, single html file, and readable view of each webpage.
|
||||||
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional)
|
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional)
|
||||||
- 📂 Organize links by collection, sub-collection, name, description and multiple tags.
|
- 📂 Organize links by collection, sub-collection, name, description and multiple tags.
|
||||||
- 👥 Collaborate on gathering links in a collection.
|
- 👥 Collaborate on gathering links in a collection.
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsSinglefile: undefined as unknown as boolean,
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
username: account.username as string,
|
username: account.username as string,
|
||||||
image: account.image as string,
|
image: account.image as string,
|
||||||
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsSinglefile: account.archiveAsSinglefile as boolean,
|
||||||
archiveAsPDF: account.archiveAsPDF as boolean,
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,8 @@ export default function LinkGroupedIconURL({
|
||||||
<i className={`bi-file-earmark-pdf`}></i>
|
<i className={`bi-file-earmark-pdf`}></i>
|
||||||
) : link.type === "image" ? (
|
) : link.type === "image" ? (
|
||||||
<i className={`bi-file-earmark-image`}></i>
|
<i className={`bi-file-earmark-image`}></i>
|
||||||
|
) : link.type === "singlefile" ? (
|
||||||
|
<i className={`bi-filetype-html`}></i>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<p className="truncate bg-white text-black mr-1">
|
<p className="truncate bg-white text-black mr-1">
|
||||||
<p className="text-sm">{shortendURL}</p>
|
<p className="text-sm">{shortendURL}</p>
|
||||||
|
|
|
@ -42,6 +42,8 @@ export default function LinkIcon({
|
||||||
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
|
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
|
||||||
) : link.type === "image" ? (
|
) : link.type === "image" ? (
|
||||||
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
|
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
|
||||||
|
) : link.type === "singlefile" ? (
|
||||||
|
<i className={`bi-filetype-html ${iconClasses}`}></i>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -65,6 +65,7 @@ export default function EditCollectionSharingModal({
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsSinglefile: undefined as unknown as boolean,
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||||
image: "",
|
image: "",
|
||||||
pdf: "",
|
pdf: "",
|
||||||
readable: "",
|
readable: "",
|
||||||
|
singlefile: "",
|
||||||
textContent: "",
|
textContent: "",
|
||||||
collection: {
|
collection: {
|
||||||
name: "",
|
name: "",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { useSession } from "next-auth/react";
|
||||||
import {
|
import {
|
||||||
pdfAvailable,
|
pdfAvailable,
|
||||||
readabilityAvailable,
|
readabilityAvailable,
|
||||||
|
singlefileAvailable,
|
||||||
screenshotAvailable,
|
screenshotAvailable,
|
||||||
} from "@/lib/shared/getArchiveValidity";
|
} from "@/lib/shared/getArchiveValidity";
|
||||||
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
||||||
|
@ -42,6 +43,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsSinglefile: undefined as unknown as boolean,
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,6 +61,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
username: account.username as string,
|
username: account.username as string,
|
||||||
image: account.image as string,
|
image: account.image as string,
|
||||||
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsSinglefile: account.archiveAsScreenshot as boolean,
|
||||||
archiveAsPDF: account.archiveAsPDF as boolean,
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -73,6 +76,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
(collectionOwner.archiveAsScreenshot === true
|
(collectionOwner.archiveAsScreenshot === true
|
||||||
? link.pdf && link.pdf !== "pending"
|
? link.pdf && link.pdf !== "pending"
|
||||||
: true) &&
|
: true) &&
|
||||||
|
(collectionOwner.archiveAsSinglefile === true
|
||||||
|
? link.singlefile && link.singlefile !== "pending"
|
||||||
|
: true) &&
|
||||||
(collectionOwner.archiveAsPDF === true
|
(collectionOwner.archiveAsPDF === true
|
||||||
? link.pdf && link.pdf !== "pending"
|
? link.pdf && link.pdf !== "pending"
|
||||||
: true) &&
|
: true) &&
|
||||||
|
@ -109,7 +115,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [link?.image, link?.pdf, link?.readable]);
|
}, [link?.image, link?.pdf, link?.readable, link?.singlefile]);
|
||||||
|
|
||||||
const updateArchive = async () => {
|
const updateArchive = async () => {
|
||||||
const load = toast.loading("Sending request...");
|
const load = toast.loading("Sending request...");
|
||||||
|
@ -140,7 +146,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
{isReady() &&
|
{isReady() &&
|
||||||
(screenshotAvailable(link) ||
|
(screenshotAvailable(link) ||
|
||||||
pdfAvailable(link) ||
|
pdfAvailable(link) ||
|
||||||
readabilityAvailable(link)) ? (
|
readabilityAvailable(link) ||
|
||||||
|
singlefileAvailable(link)) ? (
|
||||||
<p className="mb-3">
|
<p className="mb-3">
|
||||||
The following formats are available for this link:
|
The following formats are available for this link:
|
||||||
</p>
|
</p>
|
||||||
|
@ -183,6 +190,16 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
activeLink={link}
|
activeLink={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
{singlefileAvailable(link) ? (
|
||||||
|
<PreservedFormatRow
|
||||||
|
name={"Singlefile"}
|
||||||
|
icon={"bi-filetype-html"}
|
||||||
|
format={ArchivedFormat.singlefile}
|
||||||
|
activeLink={link}
|
||||||
|
downloadable={true}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
image: "",
|
image: "",
|
||||||
pdf: "",
|
pdf: "",
|
||||||
readable: "",
|
readable: "",
|
||||||
|
singlefile: "",
|
||||||
textContent: "",
|
textContent: "",
|
||||||
collection: {
|
collection: {
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -101,7 +102,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!submitLoader && file) {
|
if (!submitLoader && file) {
|
||||||
let fileType: ArchivedFormat | null = null;
|
let fileType: ArchivedFormat | null = null;
|
||||||
let linkType: "url" | "image" | "pdf" | null = null;
|
let linkType: "url" | "image" | "singlefile" | "pdf" | null = null;
|
||||||
|
|
||||||
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
|
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
|
||||||
fileType = ArchivedFormat.jpeg;
|
fileType = ArchivedFormat.jpeg;
|
||||||
|
@ -109,6 +110,9 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
} else if (file.type === "image/png") {
|
} else if (file.type === "image/png") {
|
||||||
fileType = ArchivedFormat.png;
|
fileType = ArchivedFormat.png;
|
||||||
linkType = "image";
|
linkType = "image";
|
||||||
|
} else if (file.type === "text/html") {
|
||||||
|
fileType = ArchivedFormat.singlefile;
|
||||||
|
linkType = "singlefile";
|
||||||
} else if (file.type === "application/pdf") {
|
} else if (file.type === "application/pdf") {
|
||||||
fileType = ArchivedFormat.pdf;
|
fileType = ArchivedFormat.pdf;
|
||||||
linkType = "pdf";
|
linkType = "pdf";
|
||||||
|
@ -165,13 +169,13 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.png,.jpg,.jpeg"
|
accept=".pdf,.png,.jpg,.jpeg,.html"
|
||||||
className="cursor-pointer custom-file-input"
|
className="cursor-pointer custom-file-input"
|
||||||
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs font-semibold mt-2">
|
<p className="text-xs font-semibold mt-2">
|
||||||
PDF, PNG, JPG (Up to {process.env.NEXT_PUBLIC_MAX_FILE_SIZE || 30}
|
PDF, PNG, JPG, HTML (Up to {process.env.NEXT_PUBLIC_MAX_FILE_SIZE || 30}
|
||||||
MB)
|
MB)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import {
|
import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
ArchivedFormat,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
@ -61,7 +57,7 @@ export default function PreservedFormatRow({
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [link?.image, link?.pdf, link?.readable]);
|
}, [link?.image, link?.pdf, link?.readable, link?.singlefile]);
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
||||||
|
@ -69,10 +65,10 @@ export default function PreservedFormatRow({
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Create a temporary link and click it to trigger the download
|
// Create a temporary link and click it to trigger the download
|
||||||
const link = document.createElement("a");
|
const anchorElement = document.createElement("a");
|
||||||
link.href = path;
|
anchorElement.href = path;
|
||||||
link.download = format === ArchivedFormat.pdf ? "PDF" : "Screenshot";
|
anchorElement.download = format === ArchivedFormat.singlefile ? (link.name ?? 'index') : format === ArchivedFormat.pdf ? "PDF" : "Screenshot";
|
||||||
link.click();
|
anchorElement.click();
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to download file");
|
console.error("Failed to download file");
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,9 +65,11 @@ export default function ReadableView({ link }: Props) {
|
||||||
(link?.image === "pending" ||
|
(link?.image === "pending" ||
|
||||||
link?.pdf === "pending" ||
|
link?.pdf === "pending" ||
|
||||||
link?.readable === "pending" ||
|
link?.readable === "pending" ||
|
||||||
|
link?.singlefile === "pending" ||
|
||||||
!link?.image ||
|
!link?.image ||
|
||||||
!link?.pdf ||
|
!link?.pdf ||
|
||||||
!link?.readable)
|
!link?.readable ||
|
||||||
|
!link?.singlefile)
|
||||||
) {
|
) {
|
||||||
interval = setInterval(() => getLink(link.id as number), 5000);
|
interval = setInterval(() => getLink(link.id as number), 5000);
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,7 +83,7 @@ export default function ReadableView({ link }: Props) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [link?.image, link?.pdf, link?.readable]);
|
}, [link?.image, link?.pdf, link?.readable, link?.singlefile]);
|
||||||
|
|
||||||
const rgbToHex = (r: number, g: number, b: number): string =>
|
const rgbToHex = (r: number, g: number, b: number): string =>
|
||||||
"#" +
|
"#" +
|
||||||
|
|
|
@ -19,3 +19,6 @@ services:
|
||||||
- ./data:/data/data
|
- ./data:/data/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
singlefile:
|
||||||
|
image: rutkai/single-file-web:latest
|
||||||
|
container_name: singlefile
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { Collection, Link, User } from "@prisma/client";
|
||||||
import validateUrlSize from "./validateUrlSize";
|
import validateUrlSize from "./validateUrlSize";
|
||||||
import removeFile from "./storage/removeFile";
|
import removeFile from "./storage/removeFile";
|
||||||
import Jimp from "jimp";
|
import Jimp from "jimp";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import axios from "axios";
|
||||||
|
import { Agent } from "http";
|
||||||
import createFolder from "./storage/createFolder";
|
import createFolder from "./storage/createFolder";
|
||||||
|
|
||||||
type LinksAndCollectionAndOwner = Link & {
|
type LinksAndCollectionAndOwner = Link & {
|
||||||
|
@ -93,6 +96,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
readable: !link.readable?.startsWith("archive")
|
readable: !link.readable?.startsWith("archive")
|
||||||
? "pending"
|
? "pending"
|
||||||
: undefined,
|
: undefined,
|
||||||
|
singlefile: !link.singlefile?.startsWith("archive")
|
||||||
|
? "pending"
|
||||||
|
: undefined,
|
||||||
preview: !link.readable?.startsWith("archive")
|
preview: !link.readable?.startsWith("archive")
|
||||||
? "pending"
|
? "pending"
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -113,19 +119,46 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
|
|
||||||
const content = await page.content();
|
const content = await page.content();
|
||||||
|
|
||||||
// TODO single file
|
// Singlefile
|
||||||
// const session = await page.context().newCDPSession(page);
|
if (user.archiveAsSinglefile && !link.singlefile?.startsWith("archive")) {
|
||||||
// const doc = await session.send("Page.captureSnapshot", {
|
let command = process.env.SINGLEFILE_ARCHIVE_COMMAND;
|
||||||
// format: "mhtml",
|
let httpApi = process.env.SINGLEFILE_ARCHIVE_HTTP_API;
|
||||||
// });
|
if (command) {
|
||||||
// const saveDocLocally = (doc: any) => {
|
if (command.includes("{{URL}}")) {
|
||||||
// console.log(doc);
|
try {
|
||||||
// return createFile({
|
let html = execSync(command.replace("{{URL}}", link.url), {
|
||||||
// data: doc,
|
timeout: 60000,
|
||||||
// filePath: `archives/${targetLink.collectionId}/${link.id}.mhtml`,
|
maxBuffer: 1024 * 1024 * 100,
|
||||||
// });
|
});
|
||||||
// };
|
await createFile({
|
||||||
// saveDocLocally(doc.data);
|
data: html,
|
||||||
|
filePath: `archives/${targetLink.collectionId}/${link.id}.html`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error running SINGLEFILE_ARCHIVE_COMMAND:", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Invalid SINGLEFILE_ARCHIVE_COMMAND. Missing {{URL}}");
|
||||||
|
}
|
||||||
|
} else if (httpApi) {
|
||||||
|
try {
|
||||||
|
let html = await axios.post(httpApi, { url: link.url }, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
httpAgent: new Agent({ keepAlive: false }),
|
||||||
|
});
|
||||||
|
await createFile({
|
||||||
|
data: html.data,
|
||||||
|
filePath: `archives/${targetLink.collectionId}/${link.id}.html`,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching Singlefile using SINGLEFILE_ARCHIVE_HTTP_API:", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("No SINGLEFILE_ARCHIVE_COMMAND or SINGLEFILE_ARCHIVE_HTTP_API defined.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Readability
|
// Readability
|
||||||
const window = new JSDOM("").window;
|
const window = new JSDOM("").window;
|
||||||
|
@ -284,6 +317,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
image: user.archiveAsScreenshot
|
image: user.archiveAsScreenshot
|
||||||
? `archives/${linkExists.collectionId}/${link.id}.png`
|
? `archives/${linkExists.collectionId}/${link.id}.png`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
singlefile: user.archiveAsSinglefile
|
||||||
|
? `archives/${linkExists.collectionId}/${link.id}.html`
|
||||||
|
: undefined,
|
||||||
pdf: user.archiveAsPDF
|
pdf: user.archiveAsPDF
|
||||||
? `archives/${linkExists.collectionId}/${link.id}.pdf`
|
? `archives/${linkExists.collectionId}/${link.id}.pdf`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -314,6 +350,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
image: !finalLink.image?.startsWith("archives")
|
image: !finalLink.image?.startsWith("archives")
|
||||||
? "unavailable"
|
? "unavailable"
|
||||||
: undefined,
|
: undefined,
|
||||||
|
singlefile: !finalLink.singlefile?.startsWith("archives")
|
||||||
|
? "unavailable"
|
||||||
|
: undefined,
|
||||||
pdf: !finalLink.pdf?.startsWith("archives")
|
pdf: !finalLink.pdf?.startsWith("archives")
|
||||||
? "unavailable"
|
? "unavailable"
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -324,6 +363,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
});
|
});
|
||||||
else {
|
else {
|
||||||
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` });
|
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` });
|
||||||
|
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.html` });
|
||||||
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` });
|
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` });
|
||||||
removeFile({
|
removeFile({
|
||||||
filePath: `archives/${link.collectionId}/${link.id}_readability.json`,
|
filePath: `archives/${link.collectionId}/${link.id}_readability.json`,
|
||||||
|
|
|
@ -52,6 +52,9 @@ export default async function deleteLinksById(
|
||||||
removeFile({
|
removeFile({
|
||||||
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
||||||
});
|
});
|
||||||
|
removeFile({
|
||||||
|
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.html`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { response: deletedLinks, status: 200 };
|
return { response: deletedLinks, status: 200 };
|
||||||
|
|
|
@ -30,6 +30,9 @@ export default async function deleteLink(userId: number, linkId: number) {
|
||||||
removeFile({
|
removeFile({
|
||||||
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
||||||
});
|
});
|
||||||
|
removeFile({
|
||||||
|
filePath: `archives/${collectionIsAccessible?.id}/${linkId}.html`,
|
||||||
|
});
|
||||||
|
|
||||||
return { response: deleteLink, status: 200 };
|
return { response: deleteLink, status: 200 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,6 +160,11 @@ export default async function updateLinkById(
|
||||||
`archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
`archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
|
||||||
`archives/${data.collection.id}/${linkId}_readability.json`
|
`archives/${data.collection.id}/${linkId}_readability.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await moveFile(
|
||||||
|
`archives/${collectionIsAccessible?.id}/${linkId}.html`,
|
||||||
|
`archives/${data.collection.id}/${linkId}.html`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { response: updatedLink, status: 200 };
|
return { response: updatedLink, status: 200 };
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default async function getPublicUser(
|
||||||
username: lessSensitiveInfo.username,
|
username: lessSensitiveInfo.username,
|
||||||
image: lessSensitiveInfo.image,
|
image: lessSensitiveInfo.image,
|
||||||
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
||||||
|
archiveAsSinglefile: lessSensitiveInfo.archiveAsSinglefile,
|
||||||
archiveAsPDF: lessSensitiveInfo.archiveAsPDF,
|
archiveAsPDF: lessSensitiveInfo.archiveAsPDF,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,7 @@ export default async function updateUserById(
|
||||||
(value, index, self) => self.indexOf(value) === index
|
(value, index, self) => self.indexOf(value) === index
|
||||||
),
|
),
|
||||||
archiveAsScreenshot: data.archiveAsScreenshot,
|
archiveAsScreenshot: data.archiveAsScreenshot,
|
||||||
|
archiveAsSinglefile: data.archiveAsSinglefile,
|
||||||
archiveAsPDF: data.archiveAsPDF,
|
archiveAsPDF: data.archiveAsPDF,
|
||||||
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
|
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
|
||||||
linksRouteTo: data.linksRouteTo,
|
linksRouteTo: data.linksRouteTo,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import util from "util";
|
||||||
|
|
||||||
type ReturnContentTypes =
|
type ReturnContentTypes =
|
||||||
| "text/plain"
|
| "text/plain"
|
||||||
|
| "text/html"
|
||||||
| "image/jpeg"
|
| "image/jpeg"
|
||||||
| "image/png"
|
| "image/png"
|
||||||
| "application/pdf"
|
| "application/pdf"
|
||||||
|
@ -61,6 +62,8 @@ export default async function readFile(filePath: string) {
|
||||||
contentType = "image/png";
|
contentType = "image/png";
|
||||||
} else if (filePath.endsWith("_readability.json")) {
|
} else if (filePath.endsWith("_readability.json")) {
|
||||||
contentType = "application/json";
|
contentType = "application/json";
|
||||||
|
} else if (filePath.endsWith(".html")) {
|
||||||
|
contentType = "text/html";
|
||||||
} else {
|
} else {
|
||||||
// if (filePath.endsWith(".jpg"))
|
// if (filePath.endsWith(".jpg"))
|
||||||
contentType = "image/jpeg";
|
contentType = "image/jpeg";
|
||||||
|
@ -88,6 +91,8 @@ export default async function readFile(filePath: string) {
|
||||||
contentType = "image/png";
|
contentType = "image/png";
|
||||||
} else if (filePath.endsWith("_readability.json")) {
|
} else if (filePath.endsWith("_readability.json")) {
|
||||||
contentType = "application/json";
|
contentType = "application/json";
|
||||||
|
} else if (filePath.endsWith(".html")) {
|
||||||
|
contentType = "text/html";
|
||||||
} else {
|
} else {
|
||||||
// if (filePath.endsWith(".jpg"))
|
// if (filePath.endsWith(".jpg"))
|
||||||
contentType = "image/jpeg";
|
contentType = "image/jpeg";
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { LinksRouteTo } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
pdfAvailable,
|
pdfAvailable,
|
||||||
readabilityAvailable,
|
readabilityAvailable,
|
||||||
|
singlefileAvailable,
|
||||||
screenshotAvailable,
|
screenshotAvailable,
|
||||||
} from "../shared/getArchiveValidity";
|
} from "../shared/getArchiveValidity";
|
||||||
|
|
||||||
|
@ -27,6 +28,10 @@ export const generateLinkHref = (
|
||||||
if (!readabilityAvailable(link)) return link.url || "";
|
if (!readabilityAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
|
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
|
||||||
|
case LinksRouteTo.SINGLEFILE:
|
||||||
|
if (!singlefileAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
|
return `/preserved/${link?.id}?format=${ArchivedFormat.singlefile}`;
|
||||||
case LinksRouteTo.SCREENSHOT:
|
case LinksRouteTo.SCREENSHOT:
|
||||||
if (!screenshotAvailable(link)) return link.url || "";
|
if (!screenshotAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,17 @@ export function readabilityAvailable(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function singlefileAvailable(
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
link &&
|
||||||
|
link.singlefile &&
|
||||||
|
link.singlefile !== "pending" &&
|
||||||
|
link.singlefile !== "unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function previewAvailable(link: any) {
|
export function previewAvailable(link: any) {
|
||||||
return (
|
return (
|
||||||
link &&
|
link &&
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
|
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
|
||||||
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
|
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
|
||||||
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
|
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
|
||||||
|
else if (format === ArchivedFormat.singlefile) suffix = ".html";
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (!linkId || !suffix)
|
if (!linkId || !suffix)
|
||||||
|
|
|
@ -76,6 +76,7 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
|
||||||
image: null,
|
image: null,
|
||||||
pdf: null,
|
pdf: null,
|
||||||
readable: null,
|
readable: null,
|
||||||
|
singlefile: null,
|
||||||
preview: null,
|
preview: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -89,6 +90,9 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
|
||||||
await removeFile({
|
await removeFile({
|
||||||
filePath: `archives/${link.collection.id}/${link.id}_readability.json`,
|
filePath: `archives/${link.collection.id}/${link.id}_readability.json`,
|
||||||
});
|
});
|
||||||
|
await removeFile({
|
||||||
|
filePath: `archives/${link.collection.id}/${link.id}.html`,
|
||||||
|
});
|
||||||
await removeFile({
|
await removeFile({
|
||||||
filePath: `archives/preview/${link.collection.id}/${link.id}.png`,
|
filePath: `archives/preview/${link.collection.id}/${link.id}.png`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,6 +61,7 @@ export default function Index() {
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsSinglefile: undefined as unknown as boolean,
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -78,6 +79,7 @@ export default function Index() {
|
||||||
username: account.username as string,
|
username: account.username as string,
|
||||||
image: account.image as string,
|
image: account.image as string,
|
||||||
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsSinglefile: account.archiveAsScreenshot as boolean,
|
||||||
archiveAsPDF: account.archiveAsPDF as boolean,
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,12 @@ export default function Index() {
|
||||||
{link && Number(router.query.format) === ArchivedFormat.readability && (
|
{link && Number(router.query.format) === ArchivedFormat.readability && (
|
||||||
<ReadableView link={link} />
|
<ReadableView link={link} />
|
||||||
)}
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.singlefile && (
|
||||||
|
<iframe
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.singlefile}`}
|
||||||
|
className="w-full h-screen border-none"
|
||||||
|
></iframe>
|
||||||
|
)}
|
||||||
{link && Number(router.query.format) === ArchivedFormat.pdf && (
|
{link && Number(router.query.format) === ArchivedFormat.pdf && (
|
||||||
<iframe
|
<iframe
|
||||||
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
|
||||||
|
|
|
@ -53,6 +53,7 @@ export default function PublicCollections() {
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsSinglefile: undefined as unknown as boolean,
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ export default function Appearance() {
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [archiveAsScreenshot, setArchiveAsScreenshot] =
|
const [archiveAsScreenshot, setArchiveAsScreenshot] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
const [archiveAsSinglefile, setArchiveAsSinglefile] =
|
||||||
|
useState<boolean>(false);
|
||||||
const [archiveAsPDF, setArchiveAsPDF] = useState<boolean>(false);
|
const [archiveAsPDF, setArchiveAsPDF] = useState<boolean>(false);
|
||||||
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
|
const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
@ -31,6 +33,7 @@ export default function Appearance() {
|
||||||
setUser({
|
setUser({
|
||||||
...account,
|
...account,
|
||||||
archiveAsScreenshot,
|
archiveAsScreenshot,
|
||||||
|
archiveAsSinglefile,
|
||||||
archiveAsPDF,
|
archiveAsPDF,
|
||||||
archiveAsWaybackMachine,
|
archiveAsWaybackMachine,
|
||||||
linksRouteTo,
|
linksRouteTo,
|
||||||
|
@ -39,6 +42,7 @@ export default function Appearance() {
|
||||||
}, [
|
}, [
|
||||||
account,
|
account,
|
||||||
archiveAsScreenshot,
|
archiveAsScreenshot,
|
||||||
|
archiveAsSinglefile,
|
||||||
archiveAsPDF,
|
archiveAsPDF,
|
||||||
archiveAsWaybackMachine,
|
archiveAsWaybackMachine,
|
||||||
linksRouteTo,
|
linksRouteTo,
|
||||||
|
@ -52,6 +56,7 @@ export default function Appearance() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!objectIsEmpty(account)) {
|
if (!objectIsEmpty(account)) {
|
||||||
setArchiveAsScreenshot(account.archiveAsScreenshot);
|
setArchiveAsScreenshot(account.archiveAsScreenshot);
|
||||||
|
setArchiveAsSinglefile(account.archiveAsSinglefile);
|
||||||
setArchiveAsPDF(account.archiveAsPDF);
|
setArchiveAsPDF(account.archiveAsPDF);
|
||||||
setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
|
setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
|
||||||
setLinksRouteTo(account.linksRouteTo);
|
setLinksRouteTo(account.linksRouteTo);
|
||||||
|
@ -129,6 +134,12 @@ export default function Appearance() {
|
||||||
onClick={() => setArchiveAsScreenshot(!archiveAsScreenshot)}
|
onClick={() => setArchiveAsScreenshot(!archiveAsScreenshot)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Singlefile"
|
||||||
|
state={archiveAsSinglefile}
|
||||||
|
onClick={() => setArchiveAsSinglefile(!archiveAsSinglefile)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="PDF"
|
label="PDF"
|
||||||
state={archiveAsPDF}
|
state={archiveAsPDF}
|
||||||
|
@ -207,6 +218,22 @@ export default function Appearance() {
|
||||||
<span className="label-text">Open Readable, if available</span>
|
<span className="label-text">Open Readable, if available</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex gap-2 justify-start w-fit"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="link-preference-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Singlefile"
|
||||||
|
checked={linksRouteTo === LinksRouteTo.SINGLEFILE}
|
||||||
|
onChange={() => setLinksRouteTo(LinksRouteTo.SINGLEFILE)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Open Singlefile, if available</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className="label cursor-pointer flex gap-2 justify-start w-fit"
|
className="label cursor-pointer flex gap-2 justify-start w-fit"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "LinksRouteTo" ADD VALUE 'SINGLEFILE';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "archiveAsSinglefile" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Link" ADD COLUMN "singlefile" text;
|
|
@ -45,6 +45,7 @@ model User {
|
||||||
linksRouteTo LinksRouteTo @default(ORIGINAL)
|
linksRouteTo LinksRouteTo @default(ORIGINAL)
|
||||||
preventDuplicateLinks Boolean @default(false)
|
preventDuplicateLinks Boolean @default(false)
|
||||||
archiveAsScreenshot Boolean @default(true)
|
archiveAsScreenshot Boolean @default(true)
|
||||||
|
archiveAsSinglefile Boolean @default(true)
|
||||||
archiveAsPDF Boolean @default(true)
|
archiveAsPDF Boolean @default(true)
|
||||||
archiveAsWaybackMachine Boolean @default(false)
|
archiveAsWaybackMachine Boolean @default(false)
|
||||||
isPrivate Boolean @default(false)
|
isPrivate Boolean @default(false)
|
||||||
|
@ -56,6 +57,7 @@ enum LinksRouteTo {
|
||||||
ORIGINAL
|
ORIGINAL
|
||||||
PDF
|
PDF
|
||||||
READABLE
|
READABLE
|
||||||
|
SINGLEFILE
|
||||||
SCREENSHOT
|
SCREENSHOT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +129,7 @@ model Link {
|
||||||
image String?
|
image String?
|
||||||
pdf String?
|
pdf String?
|
||||||
readable String?
|
readable String?
|
||||||
|
singlefile String?
|
||||||
lastPreserved DateTime?
|
lastPreserved DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
|
@ -38,6 +38,13 @@ async function processBatch() {
|
||||||
{
|
{
|
||||||
readable: "pending",
|
readable: "pending",
|
||||||
},
|
},
|
||||||
|
///////////////////////
|
||||||
|
{
|
||||||
|
singlefile: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
singlefile: "pending",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
take: archiveTakeCount,
|
take: archiveTakeCount,
|
||||||
|
@ -75,6 +82,13 @@ async function processBatch() {
|
||||||
{
|
{
|
||||||
readable: "pending",
|
readable: "pending",
|
||||||
},
|
},
|
||||||
|
///////////////////////
|
||||||
|
{
|
||||||
|
singlefile: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
singlefile: "pending",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
take: archiveTakeCount,
|
take: archiveTakeCount,
|
||||||
|
|
|
@ -46,6 +46,10 @@ declare global {
|
||||||
PDF_MARGIN_TOP?: string;
|
PDF_MARGIN_TOP?: string;
|
||||||
PDF_MARGIN_BOTTOM?: string;
|
PDF_MARGIN_BOTTOM?: string;
|
||||||
|
|
||||||
|
// PDF archive settings
|
||||||
|
SINGLEFILE_ARCHIVE_COMMAND?: string;
|
||||||
|
SINGLEFILE_ARCHIVE_HTTP_API?: string;
|
||||||
|
|
||||||
//
|
//
|
||||||
// SSO Providers
|
// SSO Providers
|
||||||
//
|
//
|
||||||
|
|
|
@ -128,12 +128,14 @@ export enum ArchivedFormat {
|
||||||
jpeg,
|
jpeg,
|
||||||
pdf,
|
pdf,
|
||||||
readability,
|
readability,
|
||||||
|
singlefile,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LinkType {
|
export enum LinkType {
|
||||||
url,
|
url,
|
||||||
pdf,
|
pdf,
|
||||||
image,
|
image,
|
||||||
|
singlefile,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TokenExpiry {
|
export enum TokenExpiry {
|
||||||
|
|
Ŝarĝante…
Reference in New Issue