Improve video upload stability and error handling

Implement robust error handling and directory creation for video uploads, enhance file validation with MIME type checking, and adjust database sync timeout for improved reliability.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 70557b10-8a46-4f62-9cec-2397b6c48e73
Replit-Commit-Checkpoint-Type: full_checkpoint
This commit is contained in:
sebastjanartic 2025-09-06 19:33:55 +00:00
parent b054a0c2d9
commit 92e74abb6d
2 changed files with 151 additions and 34 deletions

View File

@ -48,24 +48,63 @@ declare module "express-session" {
}
}
// Configure multer for video uploads
// Ensure upload directory exists
const uploadDir = './uploads/videos';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
console.log('📁 Created upload directory:', uploadDir);
}
// Configure multer for video uploads with better error handling
const upload = multer({
storage: multer.diskStorage({
destination: './uploads/videos',
destination: (req, file, cb) => {
try {
// Ensure directory exists
if (!fs.existsSync('./uploads/videos')) {
fs.mkdirSync('./uploads/videos', { recursive: true });
}
cb(null, './uploads/videos');
} catch (error) {
console.error('❌ Error creating upload directory:', error);
cb(error as Error, './uploads/videos');
}
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
try {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const filename = file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname);
cb(null, filename);
} catch (error) {
console.error('❌ Error generating filename:', error);
cb(error as Error, file.originalname);
}
}
}),
limits: {
fileSize: 500 * 1024 * 1024, // 500MB max file size
fieldSize: 1024 * 1024, // 1MB field size
fields: 10,
files: 1
},
fileFilter: (req, file, cb) => {
// Allow video files only
if (file.mimetype.startsWith('video/')) {
cb(null, true);
} else {
cb(new Error('Only video files are allowed'));
try {
// Allow video files only with strict validation
const allowedMimes = [
'video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo',
'video/webm', 'video/ogg', 'video/3gpp', 'video/x-flv'
];
if (file.mimetype.startsWith('video/') && allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
const error = new Error(`Unsupported video format: ${file.mimetype}. Only video files are allowed.`);
console.error('❌ File filter error:', error.message);
cb(error);
}
} catch (error) {
console.error('❌ Error in file filter:', error);
cb(error as Error);
}
}
});
@ -501,47 +540,104 @@ export async function registerRoutes(app: Express): Promise<Server> {
});
}
// Validate file size (max 500MB)
const parsedFileSize = parseInt(fileSize);
if (isNaN(parsedFileSize) || parsedFileSize <= 0 || parsedFileSize > 500 * 1024 * 1024) {
return res.status(400).json({
message: "Invalid file size. Maximum file size is 500MB"
});
}
// Validate MIME type
const allowedMimes = [
'video/mp4', 'video/mpeg', 'video/quicktime', 'video/x-msvideo',
'video/webm', 'video/ogg', 'video/3gpp', 'video/x-flv'
];
if (!mimeType.startsWith('video/') || !allowedMimes.includes(mimeType)) {
return res.status(400).json({
message: `Unsupported video format: ${mimeType}. Only video files are allowed.`
});
}
const uploadData = {
userId: req.session.userId!,
originalFileName,
fileSize: parseInt(fileSize),
fileSize: parsedFileSize,
mimeType,
uploadStatus: "uploading" as const,
uploadProgress: 0
};
const upload = await storage.createVideoUpload(uploadData);
console.log(`📤 Upload initialized: ${upload.id} for file: ${originalFileName}`);
res.status(201).json(upload);
} catch (error) {
res.status(500).json({ message: "Failed to initialize upload" });
console.error('❌ Failed to initialize upload:', error);
res.status(500).json({
message: error instanceof Error ? error.message : "Failed to initialize upload"
});
}
});
app.post("/api/uploads/:id/video", authenticate, upload.single('video'), async (req, res) => {
let uploadId: string | null = null;
try {
const uploadId = req.params.id;
uploadId = req.params.id;
const file = req.file;
console.log(`📤 Processing video upload for ID: ${uploadId}`);
if (!file) {
console.error('❌ No video file provided in request');
return res.status(400).json({ message: "No video file provided" });
}
// Validate upload ID
if (!uploadId || uploadId.trim() === '') {
console.error('❌ Invalid upload ID provided');
return res.status(400).json({ message: "Invalid upload ID" });
}
// Check if upload record exists
const existingUpload = await storage.getVideoUpload(uploadId);
if (!existingUpload) {
console.error(`❌ Upload record not found for ID: ${uploadId}`);
return res.status(404).json({ message: "Upload record not found" });
}
console.log(`📁 File uploaded: ${file.originalname}, size: ${file.size} bytes`);
// Update upload with file information
await storage.updateVideoUpload(uploadId, {
uploadStatus: "processing",
uploadProgress: 1.0
});
// Create video record
// Parse tags safely
let tags: string[] = [];
if (req.body.tags) {
try {
tags = JSON.parse(req.body.tags);
if (!Array.isArray(tags)) {
tags = [];
}
} catch (parseError) {
console.warn('⚠️ Failed to parse tags, using empty array:', parseError);
tags = [];
}
}
// Create video record with safe data validation
const videoData = {
title: req.body.title || path.parse(file.originalname).name,
description: req.body.description || "",
thumbnailUrl: req.body.thumbnailUrl || "https://via.placeholder.com/800x450",
title: req.body.title?.trim() || path.parse(file.originalname).name,
description: req.body.description?.trim() || "",
thumbnailUrl: req.body.thumbnailUrl?.trim() || "https://via.placeholder.com/800x450",
videoUrl: `/uploads/videos/${file.filename}`,
duration: parseInt(req.body.duration) || 0,
duration: Math.max(0, parseInt(req.body.duration) || 0),
views: 0,
category: req.body.category || "",
tags: req.body.tags ? JSON.parse(req.body.tags) : [],
category: req.body.category?.trim() || "",
tags: tags,
isPublic: req.body.isPublic !== "false",
uploadStatus: "completed",
originalFileName: file.originalname,
@ -549,6 +645,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
format: path.extname(file.originalname).slice(1)
};
console.log(`🎬 Creating video record: ${videoData.title}`);
const video = await storage.createVideo(videoData);
// Link video to upload
@ -557,19 +654,33 @@ export async function registerRoutes(app: Express): Promise<Server> {
uploadStatus: "completed"
});
res.json({ video, upload: { id: uploadId, status: "completed" } });
console.log(`✅ Video upload completed successfully: ${video.id}`);
res.json({
video,
upload: { id: uploadId, status: "completed" },
message: "Video uploaded successfully"
});
} catch (error) {
console.error("Upload error:", error);
console.error("Upload error:", error);
// Update upload status to failed
if (req.params.id) {
await storage.updateVideoUpload(req.params.id, {
uploadStatus: "failed",
errorMessage: error instanceof Error ? error.message : "Upload failed"
});
// Update upload status to failed if we have an uploadId
if (uploadId) {
try {
await storage.updateVideoUpload(uploadId, {
uploadStatus: "failed",
errorMessage: error instanceof Error ? error.message : "Upload failed"
});
} catch (updateError) {
console.error("❌ Failed to update upload status:", updateError);
}
}
res.status(500).json({ message: "Failed to upload video" });
// Return appropriate error response
const errorMessage = error instanceof Error ? error.message : "Failed to upload video";
res.status(500).json({
message: errorMessage,
uploadId: uploadId
});
}
});

View File

@ -166,15 +166,21 @@ class VideoSyncService {
try {
await this.syncVideos();
// Add timeout protection for database sync
// Add timeout protection for database sync with longer timeout
const syncTimeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Database sync timeout after 30 seconds')), 30000)
setTimeout(() => reject(new Error('Database sync timeout after 60 seconds')), 60000)
);
await Promise.race([
this.syncVideosToDatabase(),
syncTimeout
]);
try {
await Promise.race([
this.syncVideosToDatabase(),
syncTimeout
]);
} catch (error) {
console.error('❌ Database sync failed with timeout:', error);
console.log('⚠️ Continuing with cached data only - videos will still be available');
// Don't throw the error, just log it and continue
}
this.startPeriodicSync();
console.log('✅ Video sync service initialized successfully');