Skip to content

Commit 2fdd60b

Browse files
authored
Merge pull request #301 from mfts/feat/s3
feat: add aws s3 as an alternative storage
2 parents c049fbd + 70d0862 commit 2fdd60b

File tree

27 files changed

+4349
-2112
lines changed

27 files changed

+4349
-2112
lines changed

components/documents/add-document-modal.tsx

Lines changed: 32 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ import { Label } from "@/components/ui/label";
1010
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
1111
import { FormEvent, useState } from "react";
1212
import { useRouter } from "next/router";
13-
import { type PutBlobResult } from "@vercel/blob";
14-
import { upload } from "@vercel/blob/client";
1513
import DocumentUpload from "@/components/document-upload";
16-
import { pdfjs } from "react-pdf";
17-
import { copyToClipboard, getExtension } from "@/lib/utils";
14+
import { copyToClipboard } from "@/lib/utils";
1815
import { Button } from "@/components/ui/button";
1916
import { usePlausible } from "next-plausible";
2017
import { toast } from "sonner";
2118
import { useTeam } from "@/context/team-context";
2219
import { parsePageId } from "notion-utils";
23-
24-
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
20+
import { putFile } from "@/lib/files/put-file";
21+
import {
22+
DocumentData,
23+
createDocument,
24+
createNewDocumentVersion,
25+
} from "@/lib/documents/create-document";
2526

2627
export function AddDocumentModal({
2728
newVersion,
@@ -37,7 +38,9 @@ export function AddDocumentModal({
3738
const [notionLink, setNotionLink] = useState<string | null>(null);
3839
const teamInfo = useTeam();
3940

40-
const handleBrowserUpload = async (
41+
const teamId = teamInfo?.currentTeam?.id as string;
42+
43+
const handleFileUpload = async (
4144
event: FormEvent<HTMLFormElement>,
4245
): Promise<void> => {
4346
event.preventDefault();
@@ -51,28 +54,30 @@ export function AddDocumentModal({
5154
try {
5255
setUploading(true);
5356

54-
const newBlob = await upload(currentFile.name, currentFile, {
55-
access: "public",
56-
handleUploadUrl: "/api/file/browser-upload",
57+
const { type, data, numPages } = await putFile({
58+
file: currentFile,
59+
teamId,
5760
});
5861

62+
const documentData: DocumentData = {
63+
name: currentFile.name,
64+
key: data!,
65+
storageType: type!,
66+
};
5967
let response: Response | undefined;
60-
let numPages: number | undefined;
61-
// create a document or new version in the database if the document is a pdf
62-
if (getExtension(newBlob.pathname).includes("pdf")) {
63-
numPages = await getTotalPages(newBlob.url);
64-
if (!newVersion) {
65-
// create a document in the database
66-
response = await saveDocumentToDatabase(newBlob, numPages);
67-
} else {
68-
// create a new version for existing document in the database
69-
const documentId = router.query.id;
70-
response = await saveNewVersionToDatabase(
71-
newBlob,
72-
documentId as string,
73-
numPages,
74-
);
75-
}
68+
// create a document or new version in the database
69+
if (!newVersion) {
70+
// create a document in the database
71+
response = await createDocument({ documentData, teamId, numPages });
72+
} else {
73+
// create a new version for existing document in the database
74+
const documentId = router.query.id as string;
75+
response = await createNewDocumentVersion({
76+
documentData,
77+
documentId,
78+
numPages,
79+
teamId,
80+
});
7681
}
7782

7883
if (response) {
@@ -114,67 +119,6 @@ export function AddDocumentModal({
114119
}
115120
};
116121

117-
async function saveDocumentToDatabase(
118-
blob: PutBlobResult,
119-
numPages?: number,
120-
) {
121-
// create a document in the database with the blob url
122-
const response = await fetch(
123-
`/api/teams/${teamInfo?.currentTeam?.id}/documents`,
124-
{
125-
method: "POST",
126-
headers: {
127-
"Content-Type": "application/json",
128-
},
129-
body: JSON.stringify({
130-
name: blob.pathname,
131-
url: blob.url,
132-
numPages: numPages,
133-
}),
134-
},
135-
);
136-
137-
if (!response.ok) {
138-
throw new Error(`HTTP error! status: ${response.status}`);
139-
}
140-
141-
return response;
142-
}
143-
144-
// create a new version in the database
145-
async function saveNewVersionToDatabase(
146-
blob: PutBlobResult,
147-
documentId: string,
148-
numPages?: number,
149-
) {
150-
const response = await fetch(
151-
`/api/teams/${teamInfo?.currentTeam?.id}/documents/${documentId}/versions`,
152-
{
153-
method: "POST",
154-
headers: {
155-
"Content-Type": "application/json",
156-
},
157-
body: JSON.stringify({
158-
url: blob.url,
159-
numPages: numPages,
160-
type: "pdf",
161-
}),
162-
},
163-
);
164-
165-
if (!response.ok) {
166-
throw new Error(`HTTP error! status: ${response.status}`);
167-
}
168-
169-
return response;
170-
}
171-
172-
// get the number of pages in the pdf
173-
async function getTotalPages(url: string): Promise<number> {
174-
const pdf = await pdfjs.getDocument(url).promise;
175-
return pdf.numPages;
176-
}
177-
178122
const createNotionFileName = () => {
179123
// Extract Notion file name from the URL
180124
const urlSegments = (notionLink as string).split("/")[3];
@@ -291,7 +235,7 @@ export function AddDocumentModal({
291235
<CardContent className="space-y-2">
292236
<form
293237
encType="multipart/form-data"
294-
onSubmit={handleBrowserUpload}
238+
onSubmit={handleFileUpload}
295239
className="flex flex-col"
296240
>
297241
<div className="space-y-1">

components/documents/document-card.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -105,29 +105,26 @@ export default function DocumentsCard({
105105
return;
106106
}
107107

108-
const response = await fetch(
109-
`/api/teams/${teamInfo?.currentTeam?.id}/documents/${documentId}`,
110-
{
108+
toast.promise(
109+
fetch(`/api/teams/${teamInfo?.currentTeam?.id}/documents/${documentId}`, {
111110
method: "DELETE",
111+
}).then(() => {
112+
mutate(`/api/teams/${teamInfo?.currentTeam?.id}/documents`, null, {
113+
populateCache: (_, docs) => {
114+
return docs.filter(
115+
(doc: DocumentWithLinksAndLinkCountAndViewCount) =>
116+
doc.id !== documentId,
117+
);
118+
},
119+
revalidate: false,
120+
});
121+
}),
122+
{
123+
loading: "Deleting document...",
124+
success: "Document deleted successfully.",
125+
error: "Failed to delete document. Try again.",
112126
},
113127
);
114-
115-
if (response.ok) {
116-
// remove the document from the cache
117-
mutate(`/api/teams/${teamInfo?.currentTeam?.id}/documents`, null, {
118-
populateCache: (_, docs) => {
119-
return docs.filter(
120-
(doc: DocumentWithLinksAndLinkCountAndViewCount) =>
121-
doc.id !== documentId,
122-
);
123-
},
124-
revalidate: false,
125-
});
126-
toast.success("Document deleted successfully.");
127-
} else {
128-
const { message } = await response.json();
129-
toast.error(message);
130-
}
131128
};
132129

133130
const handleMenuStateChange = (open: boolean) => {

components/view/PagesViewer.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,41 @@ export default function PagesViewer({
191191

192192
<div className="flex justify-center mx-auto relative h-full w-full">
193193
{pages && loadedImages[pageNumber - 1] ? (
194-
pages.map((page, index) => (
195-
<Image
196-
key={index}
197-
className={`object-contain mx-auto ${
198-
pageNumber - 1 === index ? "block" : "hidden"
199-
}`}
200-
src={loadedImages[index] ? page.file : BlankImg}
201-
alt={`Page ${index + 1}`}
202-
priority={loadedImages[index] ? true : false}
203-
fill
204-
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw"
205-
quality={100}
206-
/>
207-
))
194+
pages.map((page, index) => {
195+
// contains cloudfront.net in the file path, then use img tag otherwise use next/image
196+
if (page.file.toLowerCase().includes("cloudfront.net")) {
197+
return (
198+
<img
199+
key={index}
200+
className={`object-contain mx-auto ${
201+
pageNumber - 1 === index ? "block" : "hidden"
202+
}`}
203+
src={
204+
loadedImages[index]
205+
? page.file
206+
: "https://www.papermark.io/_static/blank.gif"
207+
}
208+
alt={`Page ${index + 1}`}
209+
fetchPriority={loadedImages[index] ? "high" : "auto"}
210+
/>
211+
);
212+
}
213+
214+
return (
215+
<Image
216+
key={index}
217+
className={`object-contain mx-auto ${
218+
pageNumber - 1 === index ? "block" : "hidden"
219+
}`}
220+
src={loadedImages[index] ? page.file : BlankImg}
221+
alt={`Page ${index + 1}`}
222+
priority={loadedImages[index] ? true : false}
223+
fill
224+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw"
225+
quality={100}
226+
/>
227+
);
228+
})
208229
) : (
209230
<LoadingSpinner className="h-20 w-20 text-foreground" />
210231
)}

jobs/convert-pdf-to-image.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { client } from "@/trigger";
22
import { eventTrigger, retry } from "@trigger.dev/sdk";
33
import { z } from "zod";
44
import prisma from "@/lib/prisma";
5+
import { getFile } from "@/lib/files/get-file";
56

67
client.defineJob({
78
id: "convert-pdf-to-image",
@@ -13,19 +14,22 @@ client.defineJob({
1314
documentVersionId: z.string(),
1415
versionNumber: z.number().int().optional(),
1516
documentId: z.string().optional(),
17+
teamId: z.string().optional(),
1618
}),
1719
}),
1820
run: async (payload, io, ctx) => {
1921
const { documentVersionId } = payload;
2022

21-
// get file url from document version
23+
// 1. get file url from document version
2224
const documentUrl = await io.runTask("get-document-url", async () => {
2325
return prisma.documentVersion.findUnique({
2426
where: {
2527
id: documentVersionId,
2628
},
2729
select: {
2830
file: true,
31+
storageType: true,
32+
numPages: true,
2933
},
3034
});
3135
});
@@ -36,32 +40,52 @@ client.defineJob({
3640
return;
3741
}
3842

39-
// send file to api/convert endpoint in a task and get back number of pages
40-
const muDocument = await io.runTask("get-number-of-pages", async () => {
41-
const response = await fetch(
42-
`${process.env.NEXT_PUBLIC_BASE_URL}/api/mupdf/get-pages`,
43-
{
44-
method: "POST",
45-
body: JSON.stringify({ url: documentUrl.file }),
46-
headers: {
47-
"Content-Type": "application/json",
43+
let numPages = documentUrl.numPages;
44+
45+
// skip if the numPages are already defined
46+
if (!numPages) {
47+
// 2. send file to api/convert endpoint in a task and get back number of pages
48+
const muDocument = await io.runTask("get-number-of-pages", async () => {
49+
const response = await fetch(
50+
`${process.env.NEXT_PUBLIC_BASE_URL}/api/mupdf/get-pages`,
51+
{
52+
method: "POST",
53+
body: JSON.stringify({ url: documentUrl.file }),
54+
headers: {
55+
"Content-Type": "application/json",
56+
},
4857
},
49-
},
50-
);
51-
await io.logger.info("log response", { response });
58+
);
59+
await io.logger.info("log response", { response });
5260

53-
const { numPages } = (await response.json()) as { numPages: number };
54-
return { numPages };
61+
const { numPages } = (await response.json()) as { numPages: number };
62+
return { numPages };
63+
});
64+
65+
if (!muDocument || muDocument.numPages < 1) {
66+
await io.logger.error("Failed to get number of pages", { payload });
67+
return;
68+
}
69+
70+
numPages = muDocument.numPages;
71+
}
72+
73+
// 3. get signed url from file
74+
const signedUrl = await io.runTask("get-signed-url", async () => {
75+
return await getFile({
76+
type: documentUrl.storageType,
77+
data: documentUrl.file,
78+
});
5579
});
5680

57-
if (!muDocument || muDocument.numPages < 1) {
58-
await io.logger.error("Failed to get number of pages", { payload });
81+
if (!signedUrl) {
82+
await io.logger.error("Failed to get signed url", { payload });
5983
return;
6084
}
6185

62-
// iterate through pages and upload to blob in a task
86+
// 4. iterate through pages and upload to blob in a task
6387
let currentPage = 0;
64-
for (var i = 0; i < muDocument.numPages; ++i) {
88+
for (var i = 0; i < numPages; ++i) {
6589
currentPage = i + 1;
6690
await io.runTask(
6791
`upload-page-${currentPage}`,
@@ -74,10 +98,12 @@ client.defineJob({
7498
body: JSON.stringify({
7599
documentVersionId: documentVersionId,
76100
pageNumber: currentPage,
77-
url: documentUrl.file,
101+
url: signedUrl,
102+
teamId: payload.teamId,
78103
}),
79104
headers: {
80105
"Content-Type": "application/json",
106+
Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`,
81107
},
82108
},
83109
);
@@ -104,7 +130,7 @@ client.defineJob({
104130
);
105131
}
106132

107-
// after all pages are uploaded, update document version to hasPages = true
133+
// 5. after all pages are uploaded, update document version to hasPages = true
108134
await io.runTask("enable-pages", async () => {
109135
return prisma.documentVersion.update({
110136
where: {

0 commit comments

Comments
 (0)