Files
Final-Year-Project/Backend/routes/videos.ts

185 lines
4.7 KiB
TypeScript

import { Router } from 'express';
import { z } from 'zod';
import { db } from '../db/client';
import { videos } from '../db/schema';
import { requireAuth } from '../middleware/auth';
import {
ensureMinioBucket,
minioBucket,
minioClient,
minioPresignedExpirySeconds,
} from '../utils/minio';
const router = Router();
const uploadUrlSchema = z.object({
fileName: z.string().trim().min(1).max(255),
prefix: z.string().trim().optional(),
});
const downloadUrlSchema = z.object({
objectKey: z.string().trim().min(1),
});
const listSchema = z.object({
prefix: z.string().trim().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
const sanitizeSegment = (value: string): string => value.replace(/[^a-zA-Z0-9._/-]/g, '_');
const buildObjectKey = (userId: string, fileName: string, prefix?: string): string => {
const safePrefix = prefix ? sanitizeSegment(prefix).replace(/^\/+|\/+$/g, '') : 'uploads';
const safeFileName = sanitizeSegment(fileName);
return `${safePrefix}/${userId}/${Date.now()}-${safeFileName}`;
};
router.use(requireAuth);
router.post('/upload-url', async (req, res) => {
const parsed = uploadUrlSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
return;
}
const user = req.user;
if (!user) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
await ensureMinioBucket();
const objectKey = buildObjectKey(user.userId, parsed.data.fileName, parsed.data.prefix);
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
const now = new Date();
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
const [videoRecord] = await db
.insert(videos)
.values({
userId: user.userId,
objectKey,
bucket: minioBucket,
uploadUrl,
status: 'upload_link_sent',
expiresAt,
updatedAt: now,
})
.returning({
id: videos.id,
objectKey: videos.objectKey,
bucket: videos.bucket,
status: videos.status,
createdAt: videos.createdAt,
expiresAt: videos.expiresAt,
});
if (!videoRecord) {
res.status(500).json({ message: 'Unable to persist video metadata' });
return;
}
res.status(201).json({
message: 'Dummy upload URL generated',
bucket: minioBucket,
objectKey,
uploadUrl,
expiresInSeconds: minioPresignedExpirySeconds,
video: videoRecord,
});
});
router.get('/download-url', async (req, res) => {
const parsed = downloadUrlSchema.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() });
return;
}
await ensureMinioBucket();
const downloadUrl = await minioClient.presignedGetObject(
minioBucket,
parsed.data.objectKey,
minioPresignedExpirySeconds,
);
res.json({
message: 'Dummy download URL generated',
bucket: minioBucket,
objectKey: parsed.data.objectKey,
downloadUrl,
expiresInSeconds: minioPresignedExpirySeconds,
});
});
router.get('/', async (req, res) => {
const parsed = listSchema.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() });
return;
}
await ensureMinioBucket();
const objects = await new Promise<
{ objectKey: string | undefined; size: number; etag: string | undefined; lastModified: Date | undefined }[]
>((resolve, reject) => {
const results: {
objectKey: string | undefined;
size: number;
etag: string | undefined;
lastModified: Date | undefined;
}[] = [];
const stream = minioClient.listObjectsV2(minioBucket, parsed.data.prefix, true);
stream.on('data', (item) => {
if (results.length >= parsed.data.limit) {
stream.destroy();
return;
}
results.push({
objectKey: item.name,
size: item.size,
etag: item.etag,
lastModified: item.lastModified,
});
});
stream.on('error', (error) => reject(error));
stream.on('end', () => resolve(results));
stream.on('close', () => resolve(results));
});
res.json({
bucket: minioBucket,
count: objects.length,
objects,
});
});
router.delete('/', async (req, res) => {
const parsed = downloadUrlSchema.safeParse(req.query);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid query params', errors: parsed.error.flatten() });
return;
}
await ensureMinioBucket();
await minioClient.removeObject(minioBucket, parsed.data.objectKey);
res.json({ message: 'Object deleted', bucket: minioBucket, objectKey: parsed.data.objectKey });
});
export default router;