diff --git a/server/routes.ts b/server/routes.ts index 8b4197b..7736984 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -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 { }); } + // 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 { 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 { 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 + }); } }); diff --git a/server/videoSync.ts b/server/videoSync.ts index 806e1c1..dfc9ec4 100644 --- a/server/videoSync.ts +++ b/server/videoSync.ts @@ -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');