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:
parent
b054a0c2d9
commit
92e74abb6d
167
server/routes.ts
167
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<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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user