Skip to content

Commit c79242a

Browse files
committed
feat: file upload / download
1 parent edb9888 commit c79242a

File tree

14 files changed

+2925
-450
lines changed

14 files changed

+2925
-450
lines changed

apps/api/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@
3030
"typescript": "^5.3.2"
3131
},
3232
"dependencies": {
33+
"@aws-sdk/client-s3": "^3.749.0",
34+
"@aws-sdk/s3-request-presigner": "^3.749.0",
3335
"@azure/identity": "^4.5.0",
3436
"@fastify/cookie": "^9.0.4",
3537
"@fastify/cors": "^10.0.1",
36-
"@fastify/multipart": "^8.2.0",
38+
"@fastify/multipart": "^8.3.1",
3739
"@fastify/rate-limit": "^9.0.0",
3840
"@fastify/session": "^10.4.0",
3941
"@fastify/swagger": "^9.2.0",
4042
"@fastify/swagger-ui": "^5.1.0",
4143
"@prisma/client": "5.6.0",
44+
"@types/multer": "^1.4.12",
4245
"add": "^2.0.6",
4346
"axios": "^1.5.0",
4447
"bcrypt": "^5.0.1",
Lines changed: 299 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,320 @@
11
//@ts-nocheck
22
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
33
import multer from "fastify-multer";
4+
import fs from "fs";
5+
import { Readable } from "stream";
6+
import { StorageService } from "../lib/services/storage.service";
47
import { prisma } from "../prisma";
5-
const upload = multer({ dest: "uploads/" });
8+
9+
// Configure multer with storage settings
10+
const storage = multer.diskStorage({
11+
destination: function (req, file, cb) {
12+
cb(null, 'uploads/');
13+
},
14+
filename: function (req, file, cb) {
15+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
16+
cb(null, uniqueSuffix + '-' + file.originalname);
17+
}
18+
});
19+
20+
const upload = multer({
21+
storage: storage,
22+
limits: {
23+
fileSize: 10 * 1024 * 1024 // 10MB limit
24+
}
25+
});
626

727
export function objectStoreRoutes(fastify: FastifyInstance) {
8-
//
28+
// Get storage configuration
29+
fastify.get(
30+
"/api/v1/storage/config",
31+
async (request: FastifyRequest, reply: FastifyReply) => {
32+
try {
33+
const config = await prisma.storageConfig.findFirst({
34+
where: { active: true },
35+
});
36+
37+
reply.send({
38+
success: true,
39+
config,
40+
});
41+
} catch (error) {
42+
console.error("Error fetching storage config:", error);
43+
reply.status(500).send({
44+
success: false,
45+
error: "Failed to fetch storage configuration",
46+
});
47+
}
48+
}
49+
);
50+
51+
// Update storage configuration
52+
fastify.post(
53+
"/api/v1/storage/config",
54+
async (request: FastifyRequest, reply: FastifyReply) => {
55+
try {
56+
// Deactivate current active config if exists
57+
await prisma.storageConfig.updateMany({
58+
where: { active: true },
59+
data: { active: false },
60+
});
61+
62+
// Create new config
63+
const config = await prisma.storageConfig.create({
64+
data: request.body,
65+
});
66+
67+
// Reset storage provider to use new config
68+
await StorageService.resetProvider();
69+
70+
reply.send({
71+
success: true,
72+
config,
73+
});
74+
} catch (error) {
75+
console.error("Error updating storage config:", error);
76+
reply.status(500).send({
77+
success: false,
78+
error: "Failed to update storage configuration",
79+
});
80+
}
81+
}
82+
);
83+
84+
// Upload a single file
985
fastify.post(
1086
"/api/v1/storage/ticket/:id/upload/single",
1187
{ preHandler: upload.single("file") },
12-
1388
async (request: FastifyRequest, reply: FastifyReply) => {
14-
console.log(request.file);
15-
console.log(request.body);
89+
try {
90+
console.log('Upload request received:', {
91+
file: request.file,
92+
body: request.body,
93+
params: request.params
94+
});
1695

17-
const uploadedFile = await prisma.ticketFile.create({
18-
data: {
19-
ticketId: request.params.id,
20-
filename: request.file.originalname,
96+
if (!request.file) {
97+
return reply.status(400).send({
98+
success: false,
99+
error: "No file uploaded",
100+
});
101+
}
102+
103+
if (!request.params.id) {
104+
return reply.status(400).send({
105+
success: false,
106+
error: "No ticket ID provided",
107+
});
108+
}
109+
110+
if (!request.body.user) {
111+
return reply.status(400).send({
112+
success: false,
113+
error: "No user ID provided",
114+
});
115+
}
116+
117+
// Verify ticket exists
118+
const ticket = await prisma.ticket.findUnique({
119+
where: { id: request.params.id }
120+
});
121+
122+
if (!ticket) {
123+
return reply.status(404).send({
124+
success: false,
125+
error: "Ticket not found",
126+
});
127+
}
128+
129+
// Verify user exists
130+
const user = await prisma.user.findUnique({
131+
where: { id: request.body.user }
132+
});
133+
134+
if (!user) {
135+
return reply.status(404).send({
136+
success: false,
137+
error: "User not found",
138+
});
139+
}
140+
141+
console.log('Getting storage provider...');
142+
const storageProvider = await StorageService.getProvider();
143+
144+
console.log('Uploading file to storage...', {
21145
path: request.file.path,
22-
mime: request.file.mimetype,
23-
size: request.file.size,
24-
encoding: request.file.encoding,
25-
userId: request.body.user,
26-
},
27-
});
28-
29-
console.log(uploadedFile);
30-
31-
reply.send({
32-
success: true,
33-
});
146+
filename: request.file.filename,
147+
mimetype: request.file.mimetype,
148+
size: request.file.size
149+
});
150+
151+
const filePath = await storageProvider.upload(request.file);
152+
console.log('File uploaded to storage at path:', filePath);
153+
154+
console.log('Creating database record...');
155+
const uploadedFile = await prisma.ticketFile.create({
156+
data: {
157+
ticketId: request.params.id,
158+
filename: request.file.originalname,
159+
path: filePath,
160+
mime: request.file.mimetype || 'application/octet-stream',
161+
size: request.file.size,
162+
encoding: request.file.encoding || 'utf-8',
163+
userId: request.body.user,
164+
},
165+
});
166+
console.log('Database record created:', uploadedFile);
167+
168+
reply.send({
169+
success: true,
170+
file: uploadedFile,
171+
});
172+
} catch (error) {
173+
console.error("File upload error details:", {
174+
error: error.message,
175+
stack: error.stack,
176+
file: request.file ? {
177+
path: request.file.path,
178+
filename: request.file.filename,
179+
mimetype: request.file.mimetype,
180+
size: request.file.size
181+
} : null,
182+
ticketId: request.params.id,
183+
userId: request.body?.user
184+
});
185+
186+
// Clean up the uploaded file if it exists
187+
if (request.file && request.file.path) {
188+
try {
189+
await fs.promises.unlink(request.file.path);
190+
} catch (unlinkError) {
191+
console.error('Failed to clean up uploaded file:', unlinkError);
192+
}
193+
}
194+
195+
reply.status(500).send({
196+
success: false,
197+
error: "Failed to upload file",
198+
details: error.message
199+
});
200+
}
34201
}
35202
);
36203

37204
// Get all ticket attachments
205+
fastify.get(
206+
"/api/v1/storage/ticket/:id/files",
207+
async (request: FastifyRequest, reply: FastifyReply) => {
208+
try {
209+
const files = await prisma.ticketFile.findMany({
210+
where: {
211+
ticketId: request.params.id,
212+
},
213+
});
214+
215+
const storageProvider = await StorageService.getProvider();
216+
const filesWithUrls = await Promise.all(
217+
files.map(async (file) => ({
218+
...file,
219+
url: await storageProvider.getSignedUrl(file.path),
220+
}))
221+
);
222+
223+
reply.send({
224+
success: true,
225+
files: filesWithUrls,
226+
});
227+
} catch (error) {
228+
console.error("Error fetching files:", error);
229+
reply.status(500).send({
230+
success: false,
231+
error: "Failed to fetch files",
232+
});
233+
}
234+
}
235+
);
38236

39237
// Delete an attachment
238+
fastify.delete(
239+
"/api/v1/storage/ticket/file/:fileId",
240+
async (request: FastifyRequest, reply: FastifyReply) => {
241+
try {
242+
const file = await prisma.ticketFile.findUnique({
243+
where: {
244+
id: request.params.fileId,
245+
},
246+
});
247+
248+
if (!file) {
249+
return reply.status(404).send({
250+
success: false,
251+
error: "File not found",
252+
});
253+
}
254+
255+
const storageProvider = await StorageService.getProvider();
256+
await storageProvider.delete(file.path);
257+
258+
await prisma.ticketFile.delete({
259+
where: {
260+
id: file.id,
261+
},
262+
});
263+
264+
reply.send({
265+
success: true,
266+
});
267+
} catch (error) {
268+
console.error("Error deleting file:", error);
269+
reply.status(500).send({
270+
success: false,
271+
error: "Failed to delete file",
272+
});
273+
}
274+
}
275+
);
40276

41277
// Download an attachment
278+
fastify.get(
279+
"/api/v1/storage/download/:fileId",
280+
async (request: FastifyRequest, reply: FastifyReply) => {
281+
try {
282+
const file = await prisma.ticketFile.findUnique({
283+
where: {
284+
id: request.params.fileId,
285+
},
286+
});
287+
288+
if (!file) {
289+
return reply.status(404).send({
290+
success: false,
291+
error: "File not found",
292+
});
293+
}
294+
295+
const storageProvider = await StorageService.getProvider();
296+
const fileContent = await storageProvider.download(file.path);
297+
298+
reply.header("Content-Type", file.mime);
299+
reply.header(
300+
"Content-Disposition",
301+
`attachment; filename="${file.filename}"`
302+
);
303+
304+
if (fileContent instanceof Readable) {
305+
return reply.send(fileContent);
306+
} else if (Buffer.isBuffer(fileContent)) {
307+
return reply.send(fileContent);
308+
} else {
309+
throw new Error("Unsupported file content type");
310+
}
311+
} catch (error) {
312+
console.error("Error downloading file:", error);
313+
reply.status(500).send({
314+
success: false,
315+
error: "Failed to download file",
316+
});
317+
}
318+
}
319+
);
42320
}

0 commit comments

Comments
 (0)