Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add download file button #279

Merged
merged 1 commit into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/NotionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const NotionPage = ({
return (
<>
<div className="bg-white">
<Nav brand={brand} />
<Nav brand={brand} type="notion" />

<div>
<NotionRenderer
Expand Down
3 changes: 2 additions & 1 deletion components/view/PDFViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ export default function PDFViewer(props: any) {
numPages={numPages}
allowDownload={props.allowDownload}
assistantEnabled={props.assistantEnabled}
file={{ name: props.name, url: props.file }}
viewId={props.viewId}
linkId={props.linkId}
/>
<div
hidden={loading}
Expand Down
5 changes: 5 additions & 0 deletions components/view/PagesViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function PagesViewer({
documentId,
viewId,
assistantEnabled,
allowDownload,
feedbackEnabled,
versionNumber,
brand,
Expand All @@ -24,6 +25,7 @@ export default function PagesViewer({
documentId: string;
viewId: string;
assistantEnabled: boolean;
allowDownload: boolean;
feedbackEnabled: boolean;
versionNumber: number;
brand?: Brand;
Expand Down Expand Up @@ -148,7 +150,10 @@ export default function PagesViewer({
pageNumber={pageNumber}
numPages={numPages}
assistantEnabled={assistantEnabled}
allowDownload={allowDownload}
brand={brand}
viewId={viewId}
linkId={linkId}
/>
<div
style={{ height: "calc(100vh - 64px)" }}
Expand Down
57 changes: 31 additions & 26 deletions components/view/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,52 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { Button } from "../ui/button";
import PapermarkSparkle from "../shared/icons/papermark-sparkle";
import { Download } from "lucide-react";
import { Brand } from "@prisma/client";
import Image from "next/image";
import { toast } from "sonner";

export default function Nav({
pageNumber,
numPages,
allowDownload,
assistantEnabled,
file,
brand,
viewId,
linkId,
type,
}: {
pageNumber?: number;
numPages?: number;
allowDownload?: boolean;
assistantEnabled?: boolean;
file?: { name: string; url: string };
brand?: Brand;
viewId?: string;
linkId?: string;
type?: "pdf" | "notion";
}) {
const router = useRouter();
const { linkId } = router.query as { linkId: string };

async function downloadFile(e: React.MouseEvent<HTMLButtonElement>) {
if (!allowDownload || !file) return;
const downloadFile = async () => {
if (!allowDownload || type === "notion") return;
try {
//get file data
const response = await fetch(file.url);
const fileData = await response.blob();
const response = await fetch(`/api/links/download`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ linkId, viewId }),
});

//create <a/> to download the file
const a = document.createElement("a");
a.href = window.URL.createObjectURL(fileData);
a.download = file.name;
document.body.appendChild(a);
a.click();
if (!response.ok) {
toast.error("Error downloading file");
return;
}

//clean up used resources
document.body.removeChild(a);
window.URL.revokeObjectURL(a.href);
const { downloadUrl } = await response.json();
window.open(downloadUrl, "_blank");
} catch (error) {
console.error("Error downloading file:", error);
}
}
};

return (
<nav
Expand Down Expand Up @@ -93,11 +95,14 @@ export default function Nav({
</Link>
) : null}
{allowDownload ? (
<div className="bg-gray-900 text-white rounded-md px-2 py-1 text-sm m-1">
<Button onClick={downloadFile} size="icon">
<Download className="w-6 h-6" />
</Button>
</div>
<Button
onClick={downloadFile}
className="text-white bg-gray-900 hover:bg-gray-900/80 m-1"
size="icon"
title="Download document"
>
<Download className="w-5 h-5" />
</Button>
) : null}
{pageNumber && numPages ? (
<div className="bg-gray-900 text-white rounded-md h-10 px-4 py-2 items-center flex text-sm font-medium">
Expand Down
1 change: 1 addition & 0 deletions components/view/view-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function ViewData({
linkId={link.id}
documentId={document.id}
assistantEnabled={document.assistantEnabled}
allowDownload={link.allowDownload!}
feedbackEnabled={link.enableFeedback!}
versionNumber={document.versions[0].versionNumber}
brand={brand}
Expand Down
97 changes: 97 additions & 0 deletions pages/api/links/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/lib/prisma";
import { getDownloadUrl } from "@vercel/blob";

export default async function handle(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "POST") {
// GET /api/links/download
const { linkId, viewId } = req.body as { linkId: string; viewId: string };

try {
const view = await prisma.view.findUnique({
where: {
id: viewId,
linkId: linkId,
},
select: {
id: true,
viewedAt: true,
link: {
select: {
allowDownload: true,
expiresAt: true,
isArchived: true,
},
},
document: {
select: {
versions: {
where: { isPrimary: true },
select: {
type: true,
file: true,
},
take: 1,
},
},
},
},
});

// if view does not exist, we should not allow the download
if (!view) {
return res.status(404).json({ error: "Error downloading" });
}

// if link does not allow download, we should not allow the download
if (!view.link.allowDownload) {
return res.status(403).json({ error: "Error downloading" });
}

// if link is archived, we should not allow the download
if (view.link.isArchived) {
return res.status(403).json({ error: "Error downloading" });
}

// if link is expired, we should not allow the download
if (view.link.expiresAt && view.link.expiresAt < new Date()) {
return res.status(403).json({ error: "Error downloading" });
}

// if document is a Notion document, we should not allow the download
if (view.document.versions[0].type === "notion") {
return res.status(403).json({ error: "Error downloading" });
}

// if viewedAt is longer than 30 mins ago, we should not allow the download
if (
view.viewedAt &&
view.viewedAt < new Date(Date.now() - 30 * 60 * 1000)
) {
return res.status(403).json({ error: "Error downloading" });
}

// update the view with the downloadedAt timestamp
await prisma.view.update({
where: { id: viewId },
data: { downloadedAt: new Date() },
});

const downloadUrl = getDownloadUrl(view.document.versions[0].file);

return res.status(200).json({ downloadUrl });
} catch (error) {
return res.status(500).json({
message: "Internal Server Error",
error: (error as Error).message,
});
}
}

// We only allow POST requests
res.setHeader("Allow", ["POST"]);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "View" ADD COLUMN "downloadedAt" TIMESTAMP(3);
37 changes: 19 additions & 18 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ datasource db {
}

generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}

Expand Down Expand Up @@ -76,15 +76,15 @@ model Team {
}

model Brand {
id String @id @default(cuid())
id String @id @default(cuid())
logo String? // This should be a reference to where the file is stored (S3, Google Cloud Storage, etc.)
brandColor String? // This should be a reference to the brand color
accentColor String? // This should be a reference to the accent color
teamId String @unique
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId String @unique
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

enum Role {
Expand Down Expand Up @@ -191,9 +191,9 @@ model Link {
enableFeedback Boolean? @default(true) // Optional give user a option to enable the feedback toolbar

// custom metatags
metaTitle String? // This will be the meta title of the link
metaDescription String? // This will be the meta description of the link
metaImage String? // This will be the meta image of the link
metaTitle String? // This will be the meta title of the link
metaDescription String? // This will be the meta description of the link
metaImage String? // This will be the meta image of the link
enableCustomMetatag Boolean? @default(false) // Optional give user a option to enable the custom metatag

@@unique([domainSlug, slug])
Expand All @@ -218,15 +218,16 @@ model Domain {
}

model View {
id String @id @default(cuid())
link Link @relation(fields: [linkId], references: [id])
linkId String
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
documentId String
viewerEmail String? // Email of the viewer if known
verified Boolean @default(false) // Whether the viewer email has been verified
viewedAt DateTime @default(now())
reactions Reaction[]
id String @id @default(cuid())
link Link @relation(fields: [linkId], references: [id])
linkId String
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
documentId String
viewerEmail String? // Email of the viewer if known
verified Boolean @default(false) // Whether the viewer email has been verified
viewedAt DateTime @default(now())
downloadedAt DateTime? // This is the time the document was downloaded
reactions Reaction[]

@@index([linkId])
@@index([documentId])
Expand Down