Skip to content

Commit e1d7955

Browse files
authored
Attachment Download (#970)
* feat: attachment api * feat: download attachment * feat: change status code * refactor: change api fetching * fix: create task with attachment
1 parent 71ba7ed commit e1d7955

File tree

6 files changed

+137
-1
lines changed

6 files changed

+137
-1
lines changed

Diff for: src/app/api/tasks/[id]/attachment/route.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { notFound } from 'next/navigation'
2+
import { NextRequest, NextResponse } from 'next/server'
3+
4+
import { GetObjectCommand } from '@aws-sdk/client-s3'
5+
6+
import checkUserPermissionOnTask from '@/lib/api/queries/checkUserPermissionOnTask'
7+
import prisma from '@/lib/prisma'
8+
import { s3Client } from '@/lib/s3Client'
9+
import { getServerUser } from '@/lib/session'
10+
import { forbidden, unauthorized } from '@/utils/apiResponse'
11+
12+
export async function GET(
13+
_: NextRequest,
14+
{ params }: { params: { id: string } }
15+
) {
16+
const id = params.id
17+
18+
const task = await prisma.task.findUnique({
19+
where: { id },
20+
select: {
21+
id: true,
22+
private: true,
23+
statement: true
24+
}
25+
})
26+
27+
if (!task) return notFound()
28+
29+
if (task.private) {
30+
const user = await getServerUser()
31+
32+
if (!user) {
33+
return unauthorized()
34+
}
35+
36+
if (!(await checkUserPermissionOnTask(user, task.id))) {
37+
return forbidden()
38+
}
39+
}
40+
41+
try {
42+
const response = await s3Client.send(
43+
new GetObjectCommand({
44+
Bucket: process.env.BUCKET_NAME,
45+
Key: `statements/attachments/${id}.zip`
46+
})
47+
)
48+
49+
return new NextResponse(response.Body as ReadableStream, {
50+
headers: {
51+
'Content-Type': 'application/zip',
52+
'Content-Disposition': `inline; attachment; filename=${id}.zip`
53+
}
54+
})
55+
} catch {
56+
return new NextResponse(null, { status: 204 })
57+
}
58+
}

Diff for: src/app/api/tasks/route.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export async function POST(req: NextRequest) {
5454
const Key =
5555
file.type === 'application/pdf'
5656
? `statements/pdf/${task.id}.pdf`
57-
: `testcases/${task.id}/${file.path}`
57+
: file.type === 'application/zip'
58+
? `statements/attachments/${task.id}.zip`
59+
: `testcases/${task.id}/${file.path}`
5860
const url = await getSignedUrl(
5961
s3Client,
6062
new PutObjectCommand({

Diff for: src/components/Task/Attachment.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client'
2+
3+
import useSWR from 'swr'
4+
5+
import zipFetcher from '@/lib/zipFetcher'
6+
import { DownloadIcon } from '@/svg/Illustrations/DownloadIcon'
7+
8+
export const Attachment = ({ id }: { id: string }) => {
9+
const { data: attachment } = useSWR(`/api/tasks/${id}/attachment`, zipFetcher)
10+
11+
if (!(attachment instanceof Blob)) {
12+
return null
13+
}
14+
15+
const downloadAttachment = () => {
16+
const url = URL.createObjectURL(attachment as Blob)
17+
const a = document.createElement('a')
18+
a.href = url
19+
a.download = `${id}.zip`
20+
document.body.appendChild(a)
21+
a.click()
22+
document.body.removeChild(a)
23+
URL.revokeObjectURL(url)
24+
}
25+
26+
return (
27+
<div className="my-3 text-gray-500 dark:text-gray-100">
28+
<p className="mb-1">Attachment</p>
29+
<button
30+
className="flex w-full items-center justify-between rounded-md border p-3"
31+
onClick={downloadAttachment}
32+
>
33+
<p className="w-2/3 truncate">{id}.zip</p>
34+
<DownloadIcon />
35+
</button>
36+
<hr className="my-4 hidden md:block" />
37+
</div>
38+
)
39+
}

Diff for: src/components/Task/SideBar.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { PieChart } from '@/components/common/PieChart'
1414
import fetcher from '@/lib/fetcher'
1515
import { IListSubmission } from '@/types/submissions'
1616

17+
import { Attachment } from './Attachment'
18+
1719
const NormalTabs = [
1820
{
1921
label: 'Statement',
@@ -153,6 +155,8 @@ export const SideBar = ({
153155

154156
<hr className="my-4 hidden md:block" />
155157

158+
<Attachment id={task.id} />
159+
156160
<div className="hidden flex-col items-center justify-center md:flex">
157161
<a
158162
target="_blank"

Diff for: src/lib/zipFetcher.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default async function zipFetcher<JSON = unknown>(
2+
input: RequestInfo,
3+
init?: RequestInit
4+
): Promise<JSON | Blob> {
5+
try {
6+
const res = await fetch(input, init)
7+
8+
if (res.headers.get('Content-Type')?.includes('application/zip')) {
9+
return res.blob()
10+
}
11+
return res.json()
12+
} catch (e) {
13+
return Promise.reject(e)
14+
}
15+
}

Diff for: src/svg/Illustrations/DownloadIcon.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export const DownloadIcon = () => {
2+
return (
3+
<svg
4+
width="14"
5+
height="14"
6+
viewBox="0 0 14 14"
7+
fill="none"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<path
11+
fill-rule="evenodd"
12+
clip-rule="evenodd"
13+
d="M2.01465 11.7202C2.01465 11.3612 2.30566 11.0702 2.66465 11.0702H10.4646C10.8236 11.0702 11.1146 11.3612 11.1146 11.7202C11.1146 12.0792 10.8236 12.3702 10.4646 12.3702H2.66465C2.30566 12.3702 2.01465 12.0792 2.01465 11.7202ZM4.15503 6.7106C4.40887 6.45675 4.82043 6.45675 5.07427 6.7106L5.91465 7.55098L5.91465 2.62021C5.91465 2.26123 6.20566 1.97021 6.56465 1.97021C6.92363 1.97021 7.21465 2.26123 7.21465 2.62021L7.21465 7.55098L8.05503 6.7106C8.30887 6.45675 8.72043 6.45675 8.97427 6.7106C9.22811 6.96444 9.22811 7.37599 8.97427 7.62983L7.02427 9.57983C6.90237 9.70173 6.73704 9.77022 6.56465 9.77022C6.39226 9.77022 6.22693 9.70173 6.10503 9.57983L4.15503 7.62983C3.90119 7.37599 3.90119 6.96444 4.15503 6.7106Z"
14+
fill="#3B82F6"
15+
/>
16+
</svg>
17+
)
18+
}

0 commit comments

Comments
 (0)