- Caricamento Video

This commit is contained in:
Surya Paolo
2025-12-19 22:59:20 +01:00
parent 80c929436c
commit afeedf27a5
10 changed files with 738 additions and 0 deletions

View File

@@ -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;