const path = require('path'); const fs = require('fs'); const { v4: uuidv4 } = require('uuid'); class VideoController { constructor(baseUploadPath = 'uploads/videos') { this.basePath = path.resolve(baseUploadPath); this._ensureDirectory(this.basePath); } // ============ PRIVATE METHODS ============ _ensureDirectory(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } _isVideoFile(filename) { return /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(filename); } _getFileInfo(filePath, relativePath = '') { const stat = fs.statSync(filePath); const filename = path.basename(filePath); return { id: uuidv4(), filename, folder: relativePath, path: `/videos/${relativePath ? relativePath + '/' : ''}${filename}`, size: stat.size, createdAt: stat.birthtime.toISOString(), modifiedAt: stat.mtime.toISOString(), }; } _scanFolders(dir, relativePath = '') { const folders = []; if (!fs.existsSync(dir)) return folders; const items = fs.readdirSync(dir); items.forEach((item) => { const fullPath = path.join(dir, item); const relPath = relativePath ? `${relativePath}/${item}` : item; if (fs.statSync(fullPath).isDirectory()) { folders.push({ name: item, path: relPath, level: relPath.split('/').length, }); // Ricorsione per sottocartelle folders.push(...this._scanFolders(fullPath, relPath)); } }); return folders; } // ============ FOLDER METHODS ============ /** * Ottiene tutte le cartelle */ getFolders = async (req, res) => { try { const folders = this._scanFolders(this.basePath); res.json({ success: true, data: { folders }, message: 'Cartelle recuperate con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Crea una nuova cartella */ createFolder = async (req, res) => { try { const { folderName, parentPath = '' } = req.body; if (!folderName || !folderName.trim()) { return res.status(400).json({ success: false, error: 'Nome cartella richiesto', }); } // Sanitizza il nome cartella const sanitizedName = folderName.replace(/[<>:"/\\|?*]/g, '_').trim(); const basePath = parentPath ? path.join(this.basePath, parentPath) : this.basePath; const newFolderPath = path.join(basePath, sanitizedName); if (fs.existsSync(newFolderPath)) { return res.status(409).json({ success: false, error: 'La cartella esiste già', }); } fs.mkdirSync(newFolderPath, { recursive: true }); const folderData = { name: sanitizedName, path: parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName, createdAt: new Date().toISOString(), }; res.status(201).json({ success: true, data: { folder: folderData }, message: 'Cartella creata con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Rinomina una cartella */ renameFolder = async (req, res) => { try { const { folderPath } = req.params; const { newName } = req.body; if (!newName || !newName.trim()) { return res.status(400).json({ success: false, error: 'Nuovo nome richiesto', }); } const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, '_').trim(); const oldPath = path.join(this.basePath, folderPath); const parentDir = path.dirname(oldPath); const newPath = path.join(parentDir, sanitizedName); if (!fs.existsSync(oldPath)) { return res.status(404).json({ success: false, error: 'Cartella non trovata', }); } if (fs.existsSync(newPath)) { return res.status(409).json({ success: false, error: 'Una cartella con questo nome esiste già', }); } fs.renameSync(oldPath, newPath); res.json({ success: true, message: 'Cartella rinominata con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Elimina una cartella */ deleteFolder = async (req, res) => { try { const { folderPath } = req.params; const fullPath = path.join(this.basePath, folderPath); if (!fs.existsSync(fullPath)) { return res.status(404).json({ success: false, error: 'Cartella non trovata', }); } // Verifica che sia una directory if (!fs.statSync(fullPath).isDirectory()) { return res.status(400).json({ success: false, error: 'Il percorso non è una cartella', }); } fs.rmSync(fullPath, { recursive: true, force: true }); res.json({ success: true, message: 'Cartella eliminata con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; // ============ VIDEO METHODS ============ /** * Ottiene i video di una cartella */ getVideos = async (req, res) => { try { const folder = req.query.folder || ''; const targetPath = folder ? path.join(this.basePath, folder) : this.basePath; if (!fs.existsSync(targetPath)) { return res.json({ success: true, data: { videos: [], folders: [], currentPath: folder, }, }); } const items = fs.readdirSync(targetPath); const videos = []; const subfolders = []; items.forEach((item) => { const itemPath = path.join(targetPath, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { subfolders.push({ name: item, path: folder ? `${folder}/${item}` : item, }); } else if (stat.isFile() && this._isVideoFile(item)) { videos.push(this._getFileInfo(itemPath, folder)); } }); // Ordina per data di creazione (più recenti prima) videos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); res.json({ success: true, data: { videos, folders: subfolders, currentPath: folder, totalVideos: videos.length, }, }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Upload singolo video */ uploadVideo = async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: 'Nessun file caricato', }); } // ✅ Legge da query parameter const folder = req.query.folder || 'default'; const videoInfo = { id: uuidv4(), originalName: req.file.originalname, filename: req.file.filename, folder: folder, path: `/videos/${folder}/${req.file.filename}`, size: req.file.size, mimetype: req.file.mimetype, uploadedAt: new Date().toISOString(), }; res.status(201).json({ success: true, data: { video: videoInfo }, message: 'Video caricato con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Upload multiplo video */ uploadVideos = async (req, res) => { try { if (!req.files || req.files.length === 0) { return res.status(400).json({ success: false, error: 'Nessun file caricato', }); } // ✅ Legge da query parameter const folder = req.query.folder || 'default'; const videos = req.files.map((file) => ({ id: uuidv4(), originalName: file.originalname, filename: file.filename, folder: folder, path: `/videos/${folder}/${file.filename}`, size: file.size, mimetype: file.mimetype, uploadedAt: new Date().toISOString(), })); res.status(201).json({ success: true, data: { videos }, message: `${videos.length} video caricati con successo`, }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Ottiene info di un singolo video */ getVideo = async (req, res) => { try { const { folder, filename } = req.params; const videoPath = path.join(this.basePath, folder, filename); if (!fs.existsSync(videoPath)) { return res.status(404).json({ success: false, error: 'Video non trovato', }); } const videoInfo = this._getFileInfo(videoPath, folder); res.json({ success: true, data: { video: videoInfo }, }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Rinomina un video */ renameVideo = async (req, res) => { try { const { folder, filename } = req.params; const { newFilename } = req.body; if (!newFilename || !newFilename.trim()) { return res.status(400).json({ success: false, error: 'Nuovo nome file richiesto', }); } const oldPath = path.join(this.basePath, folder, filename); const newPath = path.join(this.basePath, folder, newFilename); if (!fs.existsSync(oldPath)) { return res.status(404).json({ success: false, error: 'Video non trovato', }); } if (fs.existsSync(newPath)) { return res.status(409).json({ success: false, error: 'Un file con questo nome esiste già', }); } fs.renameSync(oldPath, newPath); res.json({ success: true, message: 'Video rinominato con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Sposta un video in un'altra cartella */ moveVideo = async (req, res) => { try { const { folder, filename } = req.params; const { destinationFolder } = req.body; const sourcePath = path.join(this.basePath, folder, filename); const destDir = path.join(this.basePath, destinationFolder); const destPath = path.join(destDir, filename); if (!fs.existsSync(sourcePath)) { return res.status(404).json({ success: false, error: 'Video non trovato', }); } this._ensureDirectory(destDir); if (fs.existsSync(destPath)) { return res.status(409).json({ success: false, error: 'Un file con questo nome esiste già nella destinazione', }); } fs.renameSync(sourcePath, destPath); res.json({ success: true, message: 'Video spostato con successo', data: { newPath: `/videos/${destinationFolder}/${filename}`, }, }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Elimina un video */ deleteVideo = async (req, res) => { try { const { folder, filename } = req.params; const videoPath = path.join(this.basePath, folder, filename); if (!fs.existsSync(videoPath)) { return res.status(404).json({ success: false, error: 'Video non trovato', }); } fs.unlinkSync(videoPath); res.json({ success: true, message: 'Video eliminato con successo', }); } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; /** * Stream video (per player) */ streamVideo = async (req, res) => { try { const { folder, filename } = req.params; const videoPath = path.join(this.basePath, folder, filename); if (!fs.existsSync(videoPath)) { return res.status(404).json({ success: false, error: 'Video non trovato', }); } const stat = fs.statSync(videoPath); const fileSize = stat.size; const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, '').split('-'); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunkSize = end - start + 1; const file = fs.createReadStream(videoPath, { start, end }); const headers = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunkSize, 'Content-Type': 'video/mp4', }; res.writeHead(206, headers); file.pipe(res); } else { const headers = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, headers); fs.createReadStream(videoPath).pipe(res); } } catch (error) { res.status(500).json({ success: false, error: error.message, }); } }; // ============ ERROR HANDLER MIDDLEWARE ============ static errorHandler = (error, req, res, next) => { if (error.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ success: false, error: 'File troppo grande. Dimensione massima: 500MB', }); } if (error.code === 'LIMIT_FILE_COUNT') { return res.status(400).json({ success: false, error: 'Troppi file. Massimo 10 file per upload', }); } if (error.message.includes('Tipo file non supportato')) { return res.status(415).json({ success: false, error: error.message, }); } res.status(500).json({ success: false, error: error.message || 'Errore interno del server', }); }; } module.exports = VideoController;