Skip to content

Commit ce20e02

Browse files
authored
Merge pull request #51 from t1np0t/feature/fileupload
Add File Upload API (#36)
2 parents a35c08f + 2d6491b commit ce20e02

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed

src/app/api/files/upload/route.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { NextResponse } from 'next/server';
2+
import { db } from '@src/server/db';
3+
import { file } from '@src/server/db/schema/file';
4+
import { and, eq } from 'drizzle-orm';
5+
import { getServerAuthSession } from '@src/server/auth';
6+
import fs from 'fs';
7+
import path from 'path';
8+
9+
import { z } from 'zod';
10+
11+
const allowedTypes = ['image/png', 'image/jpeg', 'application/pdf'];
12+
13+
const uploadFormSchema = z.object({
14+
file: z.any(),
15+
prefix: z.string().min(1, { message: 'Prefix is missing' }),
16+
courseNumber: z.string().min(1, { message: 'Course number is missing' }),
17+
sectionCode: z.string().min(1, { message: 'Section code is missing' }),
18+
professor: z.string().min(1 , { message: 'Professor is missing' }),
19+
term: z.enum(['Spring', 'Summer', 'Fall']),
20+
year: z.coerce.number({ message: 'Year is missing'}),
21+
})
22+
23+
// Upload file to database w/ file metadata (Local)
24+
export async function POST(req: Request) {
25+
const session = await getServerAuthSession();
26+
27+
if (!session?.user?.id) {
28+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
29+
}
30+
31+
const formData = await req.formData();
32+
const result = uploadFormSchema.safeParse(Object.fromEntries(formData));
33+
34+
if (!result.success) {
35+
return NextResponse.json(
36+
{ success: false, errors: result.error },
37+
{ status: 400 },
38+
);
39+
}
40+
41+
const data = result.data;
42+
const newFile = data.file as File;
43+
44+
if (!(newFile instanceof File)) {
45+
return NextResponse.json(
46+
{ error: 'Invalid file upload' },
47+
{ status: 400 },
48+
);
49+
}
50+
51+
if (newFile.size === 0) {
52+
return NextResponse.json(
53+
{ error: 'File is empty' },
54+
{ status: 400 },
55+
);
56+
}
57+
58+
if (!allowedTypes.includes(newFile.type)) {
59+
return NextResponse.json(
60+
{ error: 'Unsupported file type' },
61+
{ status: 400 },
62+
);
63+
}
64+
65+
// Find section
66+
const sectionData = await db.query.section.findFirst({
67+
where: (sec) =>
68+
and(
69+
eq(sec.prefix, data.prefix),
70+
eq(sec.number, data.courseNumber),
71+
eq(sec.sectionCode, data.sectionCode),
72+
eq(sec.professor, data.professor),
73+
eq(sec.term, data.term),
74+
eq(sec.year, data.year),
75+
),
76+
});
77+
78+
if (!sectionData) {
79+
return NextResponse.json(
80+
{ error: 'Section not found' },
81+
{ status: 404 },
82+
);
83+
}
84+
85+
try {
86+
// Local file upload
87+
const uploadDir = path.join(process.cwd(), 'public', 'uploads');
88+
await fs.promises.mkdir(uploadDir, { recursive: true });
89+
90+
const filePath = path.join(uploadDir, newFile.name);
91+
92+
const buffer = Buffer.from(await newFile.arrayBuffer());
93+
await fs.promises.writeFile(filePath, buffer);
94+
95+
// File metadata
96+
const fileMetadata = {
97+
authorId: session.user.id,
98+
sectionId: sectionData.id,
99+
fileTitle: newFile.name, // required by schema
100+
fileName: newFile.name, // required by schema
101+
};
102+
103+
const result = await db.insert(file).values(fileMetadata).returning();
104+
105+
return NextResponse.json(
106+
{ message: 'File uploaded successfully', data: result },
107+
{ status: 201 },
108+
);
109+
} catch (err) {
110+
console.error('File upload error:', err);
111+
return NextResponse.json(
112+
{ error: 'File upload failed' },
113+
{ status: 500 },
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)