265 lines
11 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.uploadRoutes = uploadRoutes;
const promises_1 = require("stream/promises");
const fs_1 = require("fs");
const promises_2 = require("fs/promises");
const path_1 = require("path");
const crypto_1 = require("crypto");
const database_1 = require("../../../config/database");
const ffprobe_service_1 = require("../services/ffprobe.service");
const thumbnail_service_1 = require("../services/thumbnail.service");
const logger_1 = require("../../../utils/logger");
const zod_1 = require("zod");
const auth_1 = require("../middleware/auth");
// Allowed video extensions
const ALLOWED_EXTENSIONS = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v', '.flv'];
// Zod schema for upload metadata
const UploadMetadataSchema = zod_1.z.object({
title: zod_1.z.string().optional(),
producer: zod_1.z.string().optional(),
creator: zod_1.z.string().optional(),
});
/**
* Upload a single video file
*/
async function uploadVideo(request, reply) {
let tempFilePath = null;
try {
// Get the uploaded file
const data = await request.file();
if (!data) {
return reply.code(400).send({ message: 'No file uploaded' });
}
// Validate file extension
const ext = (0, path_1.extname)(data.filename).toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return reply.code(400).send({
message: `Invalid file type. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`,
});
}
// Extract metadata fields from form data
const metadataFields = data.fields;
const metadata = {
title: metadataFields.title?.value,
producer: metadataFields.producer?.value,
creator: metadataFields.creator?.value,
};
// Validate metadata
const validatedMetadata = UploadMetadataSchema.parse(metadata);
// Generate unique filename
const filename = `${(0, crypto_1.randomUUID)()}${ext}`;
const inboxDir = '/media/local/inbox';
// Ensure inbox directory exists
await (0, promises_2.mkdir)(inboxDir, { recursive: true });
const filePath = (0, path_1.join)(inboxDir, filename);
tempFilePath = filePath;
// Stream file to disk
logger_1.logger.info(`Uploading video to ${filePath}`);
await (0, promises_1.pipeline)(data.file, (0, fs_1.createWriteStream)(filePath));
// Validate video file
logger_1.logger.info(`Validating video file: ${filePath}`);
const isValid = await (0, ffprobe_service_1.validateVideoFile)(filePath);
if (!isValid) {
await (0, promises_2.unlink)(filePath);
return reply.code(400).send({ message: 'Invalid or corrupted video file' });
}
// Extract metadata
logger_1.logger.info(`Extracting metadata from: ${filePath}`);
const videoMetadata = await (0, ffprobe_service_1.extractVideoMetadata)(filePath);
// Insert into database
const video = await database_1.prisma.video.create({
data: {
path: filePath, // Store full path: /media/local/inbox/uuid.mp4
filename,
originalFilename: data.filename,
title: validatedMetadata.title || data.filename,
durationSeconds: videoMetadata.durationSeconds,
width: videoMetadata.width,
height: videoMetadata.height,
orientation: videoMetadata.orientation,
quality: videoMetadata.quality,
hasAudio: videoMetadata.hasAudio,
fileSize: videoMetadata.fileSize,
directoryType: 'inbox',
isValid: true,
producer: validatedMetadata.producer || null,
creator: validatedMetadata.creator || null,
},
});
logger_1.logger.info(`Video uploaded successfully: ${video.id}`);
// Generate thumbnail
try {
const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({
videoPath: filePath,
videoId: video.id,
duration: videoMetadata.durationSeconds,
orientation: videoMetadata.orientation,
});
// Update video with thumbnail path
await database_1.prisma.video.update({
where: { id: video.id },
data: { thumbnailPath },
});
logger_1.logger.info(`Thumbnail generated for video ${video.id}`);
}
catch (thumbnailError) {
// Log error but don't fail the upload
logger_1.logger.error(`Failed to generate thumbnail for video ${video.id}:`, thumbnailError);
}
return reply.code(201).send({
message: 'Video uploaded successfully',
video,
});
}
catch (error) {
// Clean up file on error
if (tempFilePath) {
try {
await (0, promises_2.unlink)(tempFilePath);
}
catch (cleanupError) {
logger_1.logger.error('Failed to clean up file after error:', cleanupError);
}
}
logger_1.logger.error('Video upload failed:', error);
if (error instanceof zod_1.z.ZodError) {
return reply.code(400).send({
message: 'Invalid metadata',
errors: error.errors,
});
}
return reply.code(500).send({
message: error instanceof Error ? error.message : 'Upload failed',
});
}
}
/**
* Upload multiple video files in batch
*/
async function uploadBatch(request, reply) {
try {
const files = request.files();
const results = [];
for await (const file of files) {
let tempFilePath = null;
try {
// Validate file extension
const ext = (0, path_1.extname)(file.filename).toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
results.push({
filename: file.filename,
success: false,
error: `Invalid file type. Allowed: ${ALLOWED_EXTENSIONS.join(', ')}`,
});
continue;
}
// Generate unique filename
const filename = `${(0, crypto_1.randomUUID)()}${ext}`;
const inboxDir = '/media/local/inbox';
// Ensure inbox directory exists
await (0, promises_2.mkdir)(inboxDir, { recursive: true });
const filePath = (0, path_1.join)(inboxDir, filename);
tempFilePath = filePath;
// Stream file to disk
await (0, promises_1.pipeline)(file.file, (0, fs_1.createWriteStream)(filePath));
// Validate video file
const isValid = await (0, ffprobe_service_1.validateVideoFile)(filePath);
if (!isValid) {
await (0, promises_2.unlink)(filePath);
results.push({
filename: file.filename,
success: false,
error: 'Invalid or corrupted video file',
});
continue;
}
// Extract metadata
const videoMetadata = await (0, ffprobe_service_1.extractVideoMetadata)(filePath);
// Insert into database
const video = await database_1.prisma.video.create({
data: {
path: filePath, // Store full path: /media/local/inbox/uuid.mp4
filename,
originalFilename: file.filename,
title: file.filename,
durationSeconds: videoMetadata.durationSeconds,
width: videoMetadata.width,
height: videoMetadata.height,
orientation: videoMetadata.orientation,
quality: videoMetadata.quality,
hasAudio: videoMetadata.hasAudio,
fileSize: videoMetadata.fileSize,
directoryType: 'inbox',
isValid: true,
},
});
// Generate thumbnail
try {
const thumbnailPath = await thumbnail_service_1.ThumbnailService.generateThumbnail({
videoPath: filePath,
videoId: video.id,
duration: videoMetadata.durationSeconds,
orientation: videoMetadata.orientation,
});
// Update video with thumbnail path
await database_1.prisma.video.update({
where: { id: video.id },
data: { thumbnailPath },
});
logger_1.logger.info(`Thumbnail generated for video ${video.id}`);
}
catch (thumbnailError) {
// Log error but don't fail the upload
logger_1.logger.error(`Failed to generate thumbnail for video ${video.id}:`, thumbnailError);
}
results.push({
filename: file.filename,
success: true,
video,
});
logger_1.logger.info(`Batch upload successful: ${file.filename} -> ${video.id}`);
}
catch (error) {
// Clean up file on error
if (tempFilePath) {
try {
await (0, promises_2.unlink)(tempFilePath);
}
catch (cleanupError) {
logger_1.logger.error('Failed to clean up file after error:', cleanupError);
}
}
logger_1.logger.error(`Batch upload failed for ${file.filename}:`, error);
results.push({
filename: file.filename,
success: false,
error: error instanceof Error ? error.message : 'Upload failed',
});
}
}
const successCount = results.filter((r) => r.success).length;
const failCount = results.length - successCount;
return reply.code(207).send({
message: `Batch upload complete: ${successCount} succeeded, ${failCount} failed`,
results,
});
}
catch (error) {
logger_1.logger.error('Batch upload failed:', error);
return reply.code(500).send({
message: error instanceof Error ? error.message : 'Batch upload failed',
});
}
}
async function uploadRoutes(fastify) {
// Single file upload
fastify.post('/upload', {
preHandler: auth_1.requireAdminRole,
}, uploadVideo);
// Batch upload
fastify.post('/upload/batch', {
preHandler: auth_1.requireAdminRole,
}, uploadBatch);
}
//# sourceMappingURL=upload.routes.js.map