diff --git a/.DS_Store b/.DS_Store index 205ff75..8deddae 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/controllers/VideoController.js b/src/controllers/VideoController.js new file mode 100644 index 0000000..87bb895 --- /dev/null +++ b/src/controllers/VideoController.js @@ -0,0 +1,588 @@ +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; diff --git a/src/middleware/uploadMiddleware.js b/src/middleware/uploadMiddleware.js new file mode 100644 index 0000000..7dd7ac7 --- /dev/null +++ b/src/middleware/uploadMiddleware.js @@ -0,0 +1,81 @@ +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { v4: uuidv4 } = require('uuid'); + +class UploadMiddleware { + constructor(baseUploadPath = 'uploads/videos') { + this.baseUploadPath = path.resolve(baseUploadPath); + this._ensureDirectory(this.baseUploadPath); + } + + _ensureDirectory(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + + _createStorage() { + return multer.diskStorage({ + destination: (req, file, cb) => { + // ✅ Legge SOLO da req.query (affidabile con multer) + const folder = req.query.folder || 'default'; + + console.log('📁 Upload folder:', folder); // Debug + + const uploadPath = path.join(this.baseUploadPath, folder); + this._ensureDirectory(uploadPath); + cb(null, uploadPath); + }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname); + const uniqueName = `${uuidv4()}-${Date.now()}${ext}`; + cb(null, uniqueName); + } + }); + } + + _fileFilter(req, file, cb) { + const allowedMimes = [ + 'video/mp4', + 'video/webm', + 'video/ogg', + 'video/quicktime', + 'video/x-msvideo', + 'video/x-matroska' + ]; + + if (allowedMimes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`Tipo file non supportato: ${file.mimetype}`), false); + } + } + + getUploader(options = {}) { + const config = { + storage: this._createStorage(), + fileFilter: this._fileFilter.bind(this), + limits: { + fileSize: options.maxSize || 500 * 1024 * 1024, + files: options.maxFiles || 10 + } + }; + + return multer(config); + } + + single(fieldName = 'video') { + return this.getUploader().single(fieldName); + } + + multiple(fieldName = 'videos', maxCount = 10) { + return this.getUploader({ maxFiles: maxCount }).array(fieldName, maxCount); + } + + getBasePath() { + return this.baseUploadPath; + } +} + +module.exports = UploadMiddleware; \ No newline at end of file diff --git a/src/router/api_router.js b/src/router/api_router.js index 2995897..fb46050 100644 --- a/src/router/api_router.js +++ b/src/router/api_router.js @@ -33,6 +33,13 @@ const { MyElem } = require('../models/myelem'); const axios = require('axios'); +// Importa le routes video +const videoRoutes = require('../routes/videoRoutes'); + +// Monta le routes video +router.use('/video', videoRoutes); + + router.use('/templates', authenticate, templatesRouter); router.use('/posters', authenticate, postersRouter); router.use('/assets', authenticate, assetsRouter); diff --git a/src/router/users_router.js b/src/router/users_router.js index a86d8ca..4917dd8 100755 --- a/src/router/users_router.js +++ b/src/router/users_router.js @@ -660,6 +660,7 @@ router.post('/notifs', authenticate, async (req, res) => { router.post('/newtok', async (req, res) => { try { const refreshToken = req.body.refreshToken; + const browser_random = req.body.br; // return res.status(403).send({ error: 'Refresh token non valido' }); diff --git a/src/routes/videoRoutes.js b/src/routes/videoRoutes.js new file mode 100644 index 0000000..a361246 --- /dev/null +++ b/src/routes/videoRoutes.js @@ -0,0 +1,58 @@ +const express = require('express'); +const VideoController = require('../controllers/VideoController'); +const UploadMiddleware = require('../middleware/uploadMiddleware'); + +const { + authenticate, + authenticate_noerror, + authenticate_noerror_WithUser, + authenticate_noerror_WithUserLean, +} = require('../middleware/authenticate'); + + +const router = express.Router(); + +// Configurazione +const UPLOAD_PATH = process.env.VIDEO_UPLOAD_PATH || 'uploads/videos'; + +// Istanze +const videoController = new VideoController(UPLOAD_PATH); +const uploadMiddleware = new UploadMiddleware(UPLOAD_PATH); + +// ============ FOLDER ROUTES ============ +router.get('/folders', authenticate, videoController.getFolders); +router.post('/folders', authenticate, videoController.createFolder); +router.put('/folders/:folderPath(*)', authenticate, videoController.renameFolder); +router.delete('/folders/:folderPath(*)', authenticate, videoController.deleteFolder); + +// ============ VIDEO ROUTES ============ +router.get('/videos', authenticate, videoController.getVideos); +router.get('/videos/:folder/:filename', authenticate, videoController.getVideo); + +// Upload +router.post( + '/videos/upload', + uploadMiddleware.single('video'), + videoController.uploadVideo +); + +router.post( + '/videos/upload-multiple', + uploadMiddleware.multiple('videos', 10), + videoController.uploadVideos +); + +// Modifica +router.put('/videos/:folder/:filename/rename', authenticate, videoController.renameVideo); +router.put('/videos/:folder/:filename/move', authenticate, videoController.moveVideo); + +// Elimina +router.delete('/videos/:folder/:filename', authenticate, videoController.deleteVideo); + +// Stream +router.get('/stream/:folder/:filename', authenticate, videoController.streamVideo); + +// Error Handler +router.use(VideoController.errorHandler); + +module.exports = router; \ No newline at end of file diff --git a/src/server_old.js b/src/server_old.js index bac849b..5810c79 100755 --- a/src/server_old.js +++ b/src/server_old.js @@ -150,6 +150,8 @@ connectToDatabase(connectionUrl, options) const { MyEvent } = require('./models/myevent'); + app.use('/videos', express.static(path.join(__dirname, 'uploads/videos'))); + app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); @@ -1062,6 +1064,7 @@ connectToDatabase(connectionUrl, options) const NOCORS = false; const { domains, domainsAllowed } = parseDomains(); + console.log('domains:', domains); diff --git a/uploads/videos/Prova1/026df7dc-43d9-4dd3-b7a4-b0342cc74340-1766181500049.mp4 b/uploads/videos/Prova1/026df7dc-43d9-4dd3-b7a4-b0342cc74340-1766181500049.mp4 new file mode 100644 index 0000000..0f6c733 Binary files /dev/null and b/uploads/videos/Prova1/026df7dc-43d9-4dd3-b7a4-b0342cc74340-1766181500049.mp4 differ diff --git a/uploads/videos/default/7ac8ea8f-55c0-4262-96a8-c37cc307f41f-1766180753884.mp4 b/uploads/videos/default/7ac8ea8f-55c0-4262-96a8-c37cc307f41f-1766180753884.mp4 new file mode 100644 index 0000000..0f6c733 Binary files /dev/null and b/uploads/videos/default/7ac8ea8f-55c0-4262-96a8-c37cc307f41f-1766180753884.mp4 differ diff --git a/uploads/videos/default/7ca8586a-3525-414d-9d77-1c66d5939bed-1766180471598.mp4 b/uploads/videos/default/7ca8586a-3525-414d-9d77-1c66d5939bed-1766180471598.mp4 new file mode 100644 index 0000000..0f6c733 Binary files /dev/null and b/uploads/videos/default/7ca8586a-3525-414d-9d77-1c66d5939bed-1766180471598.mp4 differ