feat: integrate MinIO for video storage, add video routes, and update README with API documentation
This commit is contained in:
154
Backend/routes/videos.ts
Normal file
154
Backend/routes/videos.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
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);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Dummy upload URL generated',
|
||||
bucket: minioBucket,
|
||||
objectKey,
|
||||
uploadUrl,
|
||||
expiresInSeconds: minioPresignedExpirySeconds,
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user