feat(database): Implement DatabaseManager for managing database structure and initialization
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 1m51s
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 1m51s
feat(routes): Add camera, image, measurement, project, and video routes with Swagger documentation feat(services): Create storageService and videoService for file management and video processing fix(errorHandler): Enhance error handling with standardized responses and database operation wrappers
This commit is contained in:
86
README.md
Normal file
86
README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Timelapse Backend
|
||||||
|
|
||||||
|
Ce projet est une API de backend pour gérer une solution de timelapse avec capture, stockage et transformation d'images en vidéos.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
L'application a été refactorisée pour suivre un modèle MVC (Modèle-Vue-Contrôleur) et respecter le principe de séparation des préoccupations. Voici la structure du projet :
|
||||||
|
|
||||||
|
```
|
||||||
|
/src
|
||||||
|
/config - Configuration centralisée de l'application
|
||||||
|
/controllers - Logique de traitement des requêtes HTTP
|
||||||
|
/database - Connexion à la base de données
|
||||||
|
/data - Compatibilité avec l'ancienne version (à terme, à supprimer)
|
||||||
|
/middlewares - Middlewares Express
|
||||||
|
/models - Modèles de données et logique d'accès à la BDD
|
||||||
|
/routes - Définition des routes de l'API
|
||||||
|
/services - Services métier et logique complexe
|
||||||
|
/utils - Utilitaires partagés
|
||||||
|
/video - Compatibilité avec l'ancienne version (à terme, à supprimer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Points d'entrée
|
||||||
|
|
||||||
|
- `server.js` - Point d'entrée principal du serveur Express
|
||||||
|
- `api.js` - Routes principales de l'API
|
||||||
|
|
||||||
|
## Composants principaux
|
||||||
|
|
||||||
|
### Modèles
|
||||||
|
|
||||||
|
Les modèles encapsulent la logique d'accès aux données et les règles métier :
|
||||||
|
|
||||||
|
- `Project.js` - Gestion des projets de timelapse
|
||||||
|
- `Measurement.js` - Gestion des mesures et images
|
||||||
|
- `Video.js` - Gestion des vidéos générées
|
||||||
|
- `Camera.js` - Gestion de la caméra et des paramètres de capture
|
||||||
|
|
||||||
|
### Contrôleurs
|
||||||
|
|
||||||
|
Les contrôleurs gèrent le traitement des requêtes HTTP et interagissent avec les modèles et services :
|
||||||
|
|
||||||
|
- `projectController.js` - Gestion des projets
|
||||||
|
- `measurementController.js` - Gestion des mesures
|
||||||
|
- `videoController.js` - Gestion des vidéos
|
||||||
|
- `imageController.js` - Gestion des images et téléchargements
|
||||||
|
- `cameraController.js` - Gestion des paramètres de la caméra
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
Les services implémentent la logique métier complexe :
|
||||||
|
|
||||||
|
- `storageService.js` - Gestion du stockage des fichiers
|
||||||
|
- `videoService.js` - Service de création de vidéos à partir d'images
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
Les routes définissent les points d'accès HTTP de l'API :
|
||||||
|
|
||||||
|
- `projectRoutes.js` - Routes pour les projets
|
||||||
|
- `measurementRoutes.js` - Routes pour les mesures
|
||||||
|
- `videoRoutes.js` - Routes pour les vidéos
|
||||||
|
- `imageRoutes.js` - Routes pour les images
|
||||||
|
- `cameraRoutes.js` - Routes pour la caméra
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
Le déploiement est géré via Docker Compose, avec une configuration dans `docker-compose.yml`. Le script `deploy.sh` gère le déploiement automatisé.
|
||||||
|
|
||||||
|
## Phase de Transition
|
||||||
|
|
||||||
|
L'application est actuellement en phase de transition de l'ancienne architecture vers la nouvelle. Les fichiers suivants sont des ponts de compatibilité qui seront progressivement supprimés :
|
||||||
|
|
||||||
|
- `src/database/database_manager.js` - Redirige vers les nouveaux modèles
|
||||||
|
- `src/data/storage_manager.js` - Redirige vers le nouveau service de stockage
|
||||||
|
- `src/video/videoManager.js` - Redirige vers le nouveau service vidéo
|
||||||
|
|
||||||
|
Pour supprimer les fichiers obsolètes une fois toutes les références mises à jour, utilisez :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node cleanup.js delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation API
|
||||||
|
|
||||||
|
La documentation de l'API est disponible via Swagger à l'adresse `/api-docs`.
|
||||||
25
api.js
25
api.js
@@ -1,27 +1,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const cors = require('cors');
|
|
||||||
const projectRoutes = require('./routes/projectRoutes');
|
|
||||||
const measurementRoutes = require('./routes/measurementRoutes');
|
|
||||||
const videoRoutes = require('./routes/videoRoutes');
|
|
||||||
const imageRoutes = require('./routes/imageRoutes');
|
|
||||||
const uploadRoutes = require('./routes/uploadRoutes');
|
|
||||||
const FileWatcher = require('./src/data/filewatcher');
|
|
||||||
const database_manager = require('./src/database/database_manager');
|
|
||||||
const capture_system = require('./routes/capture_system');
|
|
||||||
|
|
||||||
router.use(cors({
|
// Importe toutes les routes depuis notre nouvelle structure
|
||||||
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
|
const apiRoutes = require('./src/routes');
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
||||||
allowedHeaders: ['Content-Type'],
|
|
||||||
credentials: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
router.use('/', projectRoutes);
|
// Utilise directement toutes les routes définies dans src/routes/index.js
|
||||||
router.use('/', measurementRoutes);
|
router.use('/', apiRoutes);
|
||||||
router.use('/', videoRoutes);
|
|
||||||
router.use('/', imageRoutes);
|
|
||||||
router.use('/', uploadRoutes);
|
|
||||||
router.use('/', capture_system);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
module.exports = {
|
/**
|
||||||
apps: [{
|
* Ce fichier de configuration est maintenu pour compatibilité
|
||||||
name: "backend",
|
* mais redirige vers notre nouvelle architecture centralisée.
|
||||||
script: "server.js",
|
* À terme, toutes les références à ce fichier devraient être remplacées
|
||||||
out_file: "/dev/stdout",
|
* par des importations directes de src/config/index.js
|
||||||
error_file: "/dev/stderr",
|
*/
|
||||||
log_date_format: "YYYY-MM-DD HH:mm:ss",
|
|
||||||
combine_logs: true, // Combine les logs stdout et stderr
|
const config = require('./src/config');
|
||||||
}]
|
module.exports = config;
|
||||||
};
|
|
||||||
|
|||||||
77
cleanup.js
Normal file
77
cleanup.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Script de nettoyage pour supprimer les fichiers obsolètes après refactoring
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Pour lister les fichiers obsolètes sans les supprimer : node cleanup.js list
|
||||||
|
* - Pour supprimer les fichiers obsolètes : node cleanup.js delete
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Liste des fichiers à considérer comme obsolètes
|
||||||
|
const deprecatedFiles = [
|
||||||
|
// Anciens fichiers de routes qui ont été remplacés par src/routes/*
|
||||||
|
'routes/uploadRoutes.js',
|
||||||
|
'routes/projectRoutes.js',
|
||||||
|
'routes/measurementRoutes.js',
|
||||||
|
'routes/videoRoutes.js',
|
||||||
|
'routes/capture_system.js',
|
||||||
|
'routes/imageRoutes.js',
|
||||||
|
|
||||||
|
// Utilitaires remplacés
|
||||||
|
'utils/serverError.js',
|
||||||
|
|
||||||
|
// Fichiers de backend qui ont été refactorisés
|
||||||
|
'ffmpeg.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fonction pour lister les fichiers obsolètes
|
||||||
|
function listDeprecatedFiles() {
|
||||||
|
console.log('====== Fichiers obsolètes ======');
|
||||||
|
deprecatedFiles.forEach(file => {
|
||||||
|
const filePath = path.join(__dirname, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.log(`✓ ${file} (existe)`);
|
||||||
|
} else {
|
||||||
|
console.log(`✗ ${file} (déjà supprimé)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('==============================');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour supprimer les fichiers obsolètes
|
||||||
|
function deleteDeprecatedFiles() {
|
||||||
|
console.log('====== Suppression des fichiers obsolètes ======');
|
||||||
|
deprecatedFiles.forEach(file => {
|
||||||
|
const filePath = path.join(__dirname, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
console.log(`✅ ${file} supprimé avec succès`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Erreur lors de la suppression de ${file}:`, error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ ${file} n'existe pas ou a déjà été supprimé`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('==============================================');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traitement des arguments
|
||||||
|
const action = process.argv[2];
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
listDeprecatedFiles();
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
listDeprecatedFiles();
|
||||||
|
console.log('\nConfirmation de suppression...');
|
||||||
|
deleteDeprecatedFiles();
|
||||||
|
} else {
|
||||||
|
console.log(`
|
||||||
|
Usage:
|
||||||
|
- Pour lister les fichiers obsolètes : node cleanup.js list
|
||||||
|
- Pour supprimer les fichiers obsolètes : node cleanup.js delete
|
||||||
|
`);
|
||||||
|
}
|
||||||
40
db.js
40
db.js
@@ -1,35 +1,9 @@
|
|||||||
const { Client } = require('pg');
|
/**
|
||||||
|
* Ce fichier est maintenu pour des raisons de compatibilité
|
||||||
|
* mais redirige vers la nouvelle structure de connexion à la base de données.
|
||||||
|
* À terme, toutes les références devraient utiliser src/database/connection.js
|
||||||
|
*/
|
||||||
|
|
||||||
const client = new Client({
|
const db = require('./src/database/connection');
|
||||||
host: 'timelapse-db',
|
|
||||||
port: 5432,
|
|
||||||
user: 'postgres',
|
|
||||||
password: 'postgres',
|
|
||||||
database: 'timelapse'
|
|
||||||
});
|
|
||||||
|
|
||||||
let isConnecting = false;
|
module.exports = db;
|
||||||
|
|
||||||
function init_database() {
|
|
||||||
if (isConnecting) {
|
|
||||||
console.log('[DB] Connection attempt already in progress, skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[DB] Initialisation de la base de données PostgreSQL...');
|
|
||||||
isConnecting = true;
|
|
||||||
|
|
||||||
client.connect(err => {
|
|
||||||
isConnecting = false;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
console.error('[DB] Erreur de connexion à la base de données:', err);
|
|
||||||
setTimeout(init_database, 3000);
|
|
||||||
} else {
|
|
||||||
console.log('[DB] Connecté à la base de données PostgreSQL.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
init_database();
|
|
||||||
module.exports = client;
|
|
||||||
80
server.js
80
server.js
@@ -1,47 +1,31 @@
|
|||||||
// server.js
|
// server.js
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
const swaggerUi = require('swagger-ui-express');
|
||||||
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
|
const config = require('./src/config');
|
||||||
|
const DatabaseManager = require('./src/models/database');
|
||||||
|
|
||||||
// Middleware pour gérer les requêtes JSON
|
// Middleware pour gérer les requêtes JSON
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Cors accès à tout
|
// Configuration CORS
|
||||||
app.use(cors({
|
app.use(cors(config.server.cors));
|
||||||
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
||||||
allowedHeaders: ['Content-Type'],
|
|
||||||
credentials: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Importer les routes
|
// Initialisation de la base de données
|
||||||
const apiRoutes = require('./api');
|
DatabaseManager.initialize()
|
||||||
|
.then(() => console.log('[SERVER] Base de données initialisée avec succès'))
|
||||||
|
.catch(err => console.error('[SERVER] Erreur d\'initialisation de la base de données:', err));
|
||||||
|
|
||||||
|
// Importer les routes API
|
||||||
|
const apiRoutes = require('./src/routes');
|
||||||
app.use('/api', apiRoutes);
|
app.use('/api', apiRoutes);
|
||||||
|
|
||||||
// Swagger dependencies
|
|
||||||
const swaggerUi = require('swagger-ui-express');
|
|
||||||
const swaggerJsdoc = require('swagger-jsdoc');
|
|
||||||
|
|
||||||
// Configuration de Swagger
|
// Configuration de Swagger
|
||||||
const swaggerOptions = {
|
const swaggerOptions = {
|
||||||
definition: {
|
definition: config.swagger.definition,
|
||||||
openapi: '3.0.0',
|
apis: config.swagger.apis
|
||||||
info: {
|
|
||||||
title: 'API Documentation',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'Documentation de l\'API avec Swagger',
|
|
||||||
},
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
url: 'https://timelapse.kerboul.me/api',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: 'http://localhost:3000/api',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
apis: ['./routes/*.js'], // Prend en compte tous les fichiers de routes pour générer la documentation
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialisation de swagger-jsdoc
|
// Initialisation de swagger-jsdoc
|
||||||
@@ -52,14 +36,36 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
|||||||
|
|
||||||
// Route de base pour tester le serveur
|
// Route de base pour tester le serveur
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
|
res.send('Bienvenue sur l\'API Timelapse!');
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
});
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
||||||
res.send('Bienvenue sur mon API Node.js!');
|
// Gestion des erreurs 404
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
error: {
|
||||||
|
message: 'Route non trouvée',
|
||||||
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
statusCode: 404
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion des erreurs globales
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error('[SERVER] Erreur non gérée:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: 'Erreur serveur interne',
|
||||||
|
statusCode: 500,
|
||||||
|
details: process.env.NODE_ENV === 'production' ? undefined : err.message
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Démarrer le serveur
|
// Démarrer le serveur
|
||||||
|
const port = config.server.port;
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
|
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
|
||||||
console.log(`[SERVER] Swagger documentation disponible sur http://localhost:${port}/api-docs`);
|
console.log(`[SERVER] Documentation Swagger disponible sur http://localhost:${port}/api-docs`);
|
||||||
});
|
});
|
||||||
|
|||||||
70
src/config/index.js
Normal file
70
src/config/index.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// src/config/index.js
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Configuration centralisée de l'application
|
||||||
|
module.exports = {
|
||||||
|
// Configuration du serveur
|
||||||
|
server: {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
cors: {
|
||||||
|
origins: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||||
|
allowedHeaders: ['Content-Type'],
|
||||||
|
credentials: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration de la base de données
|
||||||
|
database: {
|
||||||
|
host: process.env.DB_HOST || 'timelapse-db',
|
||||||
|
port: process.env.DB_PORT || 5432,
|
||||||
|
user: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
|
database: process.env.DB_NAME || 'timelapse',
|
||||||
|
reconnectInterval: 3000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration de Swagger
|
||||||
|
swagger: {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'API Documentation',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Documentation de l\'API avec Swagger'
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{ url: 'https://timelapse.kerboul.me/api' },
|
||||||
|
{ url: 'http://localhost:3000/api' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
apis: ['./src/routes/*.js', './src/controllers/*.js'] // Chemins pour la documentation
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chemins des répertoires
|
||||||
|
paths: {
|
||||||
|
storage: path.join(process.cwd(), 'storage'),
|
||||||
|
uploads: path.join(process.cwd(), 'uploads'),
|
||||||
|
samples: path.join(process.cwd(), 'sample')
|
||||||
|
},
|
||||||
|
|
||||||
|
// Statuts pour les projets et vidéos
|
||||||
|
status: {
|
||||||
|
waiting: 0,
|
||||||
|
completed: 1,
|
||||||
|
failed: 2,
|
||||||
|
inProgress: 3
|
||||||
|
},
|
||||||
|
|
||||||
|
// Paramètres par défaut pour la caméra
|
||||||
|
camera: {
|
||||||
|
defaultSettings: {
|
||||||
|
id: 1,
|
||||||
|
interval: null,
|
||||||
|
nbImages: null,
|
||||||
|
maintenance: false,
|
||||||
|
stopFlag: false,
|
||||||
|
idle: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
152
src/controllers/cameraController.js
Normal file
152
src/controllers/cameraController.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// src/controllers/cameraController.js
|
||||||
|
const Camera = require('../models/Camera');
|
||||||
|
const Project = require('../models/Project');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées à la caméra
|
||||||
|
*/
|
||||||
|
class CameraController {
|
||||||
|
/**
|
||||||
|
* Récupère le statut actuel de la caméra
|
||||||
|
*/
|
||||||
|
static getCameraStatus = asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const settings = await Camera.getCamera();
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
// Initialise la caméra si elle n'existe pas
|
||||||
|
const cameraSettings = await Camera.initializeCamera();
|
||||||
|
return res.json(cameraSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors de la récupération du statut de la caméra', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre une procédure de capture
|
||||||
|
*/
|
||||||
|
static startProcedure = asyncHandler(async (req, res) => {
|
||||||
|
const { project_id, interval, nb_images } = req.body;
|
||||||
|
|
||||||
|
if (!interval || !nb_images) {
|
||||||
|
return sendError('L\'intervalle et le nombre d\'images sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifie qu'aucune procédure n'est déjà en cours
|
||||||
|
const existingProject = await Project.findCurrentRenderingProject();
|
||||||
|
|
||||||
|
if (existingProject) {
|
||||||
|
return sendError('Un projet est déjà en cours de capture. Veuillez l\'arrêter avant d\'en démarrer un nouveau.', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Met à jour les paramètres de la caméra
|
||||||
|
const newSettings = {
|
||||||
|
interval: interval,
|
||||||
|
nb_images: nb_images,
|
||||||
|
stop_flag: false,
|
||||||
|
active: 1 // active = 1 (idle = 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
await Camera.updateCamera(1, newSettings);
|
||||||
|
|
||||||
|
// Met à jour le statut du projet
|
||||||
|
await Project.updateProject(project_id, { status: config.status.inProgress });
|
||||||
|
|
||||||
|
console.log(`[CAMERA] Procédure démarrée pour le projet : ${project_id}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Procédure de capture démarrée avec succès',
|
||||||
|
settings: { interval, nb_images }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors du démarrage de la procédure de capture', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initie l'arrêt de la procédure de capture
|
||||||
|
*/
|
||||||
|
static stopProcedure = asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Marque le drapeau d'arrêt
|
||||||
|
await Camera.updateCamera(1, { stop_flag: true });
|
||||||
|
|
||||||
|
console.log('[CAMERA] Arrêt de la caméra demandé, en attente de confirmation...');
|
||||||
|
|
||||||
|
res.json({ message: 'Procédure d\'arrêt de la caméra initiée avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors de l\'arrêt de la procédure de capture', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirme l'arrêt de la caméra (appelé par la caméra)
|
||||||
|
*/
|
||||||
|
static confirmStopProcedure = asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Réinitialise les paramètres de la caméra
|
||||||
|
const newSettings = {
|
||||||
|
interval: null,
|
||||||
|
nb_images: null,
|
||||||
|
stop_flag: false,
|
||||||
|
active: 0 // idle = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await Camera.updateCamera(1, newSettings);
|
||||||
|
|
||||||
|
// Réinitialise le statut du projet en cours
|
||||||
|
const currentProject = await Project.findCurrentRenderingProject();
|
||||||
|
|
||||||
|
if (currentProject) {
|
||||||
|
await Project.updateProject(currentProject.id, { status: config.status.waiting });
|
||||||
|
console.log(`[CAMERA] Projet : ${currentProject.id} arrêté.`);
|
||||||
|
} else {
|
||||||
|
console.log('[CAMERA] Aucun projet à arrêter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[CAMERA] Caméra arrêtée.');
|
||||||
|
|
||||||
|
res.json({ message: 'Caméra arrêtée avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors de la confirmation de l\'arrêt de la caméra', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active le mode maintenance
|
||||||
|
*/
|
||||||
|
static activateMaintenance = asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
await Camera.updateCamera(1, { maintenance: 1 });
|
||||||
|
|
||||||
|
console.log('[CAMERA] Mode maintenance activé.');
|
||||||
|
|
||||||
|
res.json({ message: 'Caméra en mode maintenance' });
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors de l\'activation du mode maintenance', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Désactive le mode maintenance
|
||||||
|
*/
|
||||||
|
static deactivateMaintenance = asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
await Camera.updateCamera(1, { maintenance: 0 });
|
||||||
|
|
||||||
|
console.log('[CAMERA] Mode maintenance désactivé.');
|
||||||
|
|
||||||
|
res.json({ message: 'Caméra sortie du mode maintenance' });
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors de la désactivation du mode maintenance', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CameraController;
|
||||||
175
src/controllers/imageController.js
Normal file
175
src/controllers/imageController.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// src/controllers/imageController.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const path = require('path');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux images
|
||||||
|
*/
|
||||||
|
class ImageController {
|
||||||
|
/**
|
||||||
|
* Récupère une image par projet et ordre ID
|
||||||
|
*/
|
||||||
|
static getImageByProjectAndOrderId = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Image non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = measurement.path;
|
||||||
|
|
||||||
|
// Vérifie si l'image existe
|
||||||
|
await fs.promises.access(imagePath, fs.constants.F_OK);
|
||||||
|
|
||||||
|
res.download(imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
sendError('Image non trouvée sur le disque', res, error, 404);
|
||||||
|
} else {
|
||||||
|
sendError('Erreur lors de la récupération de l\'image', res, error, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une image par son ID de mesure
|
||||||
|
*/
|
||||||
|
static getImageByMeasurementId = asyncHandler(async (req, res) => {
|
||||||
|
const measurementId = req.params.measurementId;
|
||||||
|
|
||||||
|
if (!measurementId || isNaN(measurementId)) {
|
||||||
|
return sendError('ID de mesure invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const measurement = await Measurement.getMeasurementById(measurementId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Image non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = measurement.path;
|
||||||
|
|
||||||
|
// Vérifie si l'image existe
|
||||||
|
await fs.promises.access(imagePath, fs.constants.F_OK);
|
||||||
|
|
||||||
|
res.download(imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
sendError('Image non trouvée sur le disque', res, error, 404);
|
||||||
|
} else {
|
||||||
|
sendError('Erreur lors de la récupération de l\'image', res, error, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un aperçu redimensionné d'une image
|
||||||
|
*/
|
||||||
|
static getImagePreview = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Image non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePath = measurement.path;
|
||||||
|
|
||||||
|
// Vérifie si l'image existe
|
||||||
|
await fs.promises.access(imagePath, fs.constants.F_OK);
|
||||||
|
|
||||||
|
// Redimensionne l'image
|
||||||
|
const metadata = await sharp(imagePath).metadata();
|
||||||
|
const width = Math.floor(metadata.width / 7);
|
||||||
|
const height = Math.floor(metadata.height / 7);
|
||||||
|
|
||||||
|
const resizedImage = await sharp(imagePath)
|
||||||
|
.resize(width, height)
|
||||||
|
.jpeg({ quality: 65 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
res.set('Content-Type', 'image/jpeg');
|
||||||
|
res.send(resizedImage);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
sendError('Image non trouvée sur le disque', res, error, 404);
|
||||||
|
} else {
|
||||||
|
sendError('Erreur lors de la récupération de l\'aperçu de l\'image', res, error, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge une nouvelle image avec des données de mesure
|
||||||
|
*/
|
||||||
|
static uploadImage = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, timestamp, temperature, humidity } = req.body;
|
||||||
|
const image = req.file;
|
||||||
|
|
||||||
|
if (!image || !projectId || !timestamp || !temperature || !humidity) {
|
||||||
|
return sendError('Tous les champs sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Obtention du prochain ordre ID
|
||||||
|
const nextOrderId = await Measurement.getNextOrderId(projectId);
|
||||||
|
|
||||||
|
if (nextOrderId === null) {
|
||||||
|
return sendError('Projet non trouvé', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrement de l'image
|
||||||
|
const imagePath = await StorageService.measurement.uploadMeasurementImage(
|
||||||
|
image, projectId, nextOrderId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Création de l'entrée dans la base de données
|
||||||
|
const measurement = await Measurement.createMeasurement(
|
||||||
|
projectId, timestamp, imagePath, temperature, humidity, nextOrderId
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Mesure téléchargée avec succès',
|
||||||
|
path: imagePath,
|
||||||
|
id: measurement.id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur lors du téléchargement de la mesure', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une image d'exemple (pour les tests)
|
||||||
|
*/
|
||||||
|
static getSmileImage = asyncHandler(async (req, res) => {
|
||||||
|
const imagePath = path.join(config.paths.samples, 'smile.png');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.access(imagePath, fs.constants.F_OK);
|
||||||
|
res.sendFile(imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Image d\'exemple non trouvée', res, error, 404);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ImageController;
|
||||||
135
src/controllers/measurementController.js
Normal file
135
src/controllers/measurementController.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// src/controllers/measurementController.js
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux mesures
|
||||||
|
*/
|
||||||
|
class MeasurementController {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures
|
||||||
|
*/
|
||||||
|
static getAllMeasurements = asyncHandler(async (req, res) => {
|
||||||
|
const measurements = await Measurement.getAllMeasurements();
|
||||||
|
|
||||||
|
if (!measurements || measurements.length === 0) {
|
||||||
|
return sendError('Aucune mesure trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurements);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son ID
|
||||||
|
*/
|
||||||
|
static getMeasurementById = asyncHandler(async (req, res) => {
|
||||||
|
const measurementId = req.params.id;
|
||||||
|
|
||||||
|
if (!measurementId || isNaN(measurementId)) {
|
||||||
|
return sendError('ID de mesure invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = await Measurement.getMeasurementById(measurementId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son projet ID et son ordre ID
|
||||||
|
*/
|
||||||
|
static getMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par son ID
|
||||||
|
*/
|
||||||
|
static deleteMeasurement = asyncHandler(async (req, res) => {
|
||||||
|
const measurementId = req.params.id;
|
||||||
|
|
||||||
|
if (!measurementId || isNaN(measurementId)) {
|
||||||
|
return sendError('ID de mesure invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les informations de la mesure avant suppression
|
||||||
|
const measurement = await Measurement.getMeasurementById(measurementId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime le fichier image associé si existant
|
||||||
|
if (measurement.path) {
|
||||||
|
try {
|
||||||
|
await fs.access(measurement.path);
|
||||||
|
await fs.unlink(measurement.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime l'entrée de la base de données
|
||||||
|
await Measurement.deleteMeasurement(measurementId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Mesure supprimée avec succès',
|
||||||
|
id: measurementId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par projet ID et ordre ID
|
||||||
|
*/
|
||||||
|
static deleteMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les informations de la mesure avant suppression
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime le fichier image associé si existant
|
||||||
|
if (measurement.path) {
|
||||||
|
try {
|
||||||
|
await fs.access(measurement.path);
|
||||||
|
await fs.unlink(measurement.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime l'entrée de la base de données
|
||||||
|
await Measurement.deleteMeasurement(measurement.id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Mesure supprimée avec succès',
|
||||||
|
id: measurement.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MeasurementController;
|
||||||
123
src/controllers/projectController.js
Normal file
123
src/controllers/projectController.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// src/controllers/projectController.js
|
||||||
|
const Project = require('../models/Project');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux projets
|
||||||
|
*/
|
||||||
|
class ProjectController {
|
||||||
|
/**
|
||||||
|
* Récupère tous les projets
|
||||||
|
*/
|
||||||
|
static getAllProjects = asyncHandler(async (req, res) => {
|
||||||
|
const projects = await Project.getAllProjects();
|
||||||
|
res.json(projects);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un projet par son ID
|
||||||
|
*/
|
||||||
|
static getProjectById = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await Project.getProjectById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return sendError('Projet non trouvé', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les vidéos d'un projet
|
||||||
|
*/
|
||||||
|
static getProjectVideos = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const videos = await Video.getVideosByProjectId(projectId);
|
||||||
|
|
||||||
|
if (videos.length === 0) {
|
||||||
|
return sendError('Aucune vidéo trouvée pour ce projet', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(videos);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les mesures d'un projet
|
||||||
|
*/
|
||||||
|
static getProjectMeasurements = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurements = await Measurement.getMeasurementsByProjectId(projectId);
|
||||||
|
|
||||||
|
if (measurements.length === 0) {
|
||||||
|
return sendError('Aucune mesure trouvée pour ce projet', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurements);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau projet
|
||||||
|
*/
|
||||||
|
static createProject = asyncHandler(async (req, res) => {
|
||||||
|
const { name, description } = req.body;
|
||||||
|
|
||||||
|
if (!name || !description) {
|
||||||
|
return sendError('Le nom et la description sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const defaultStatus = config.status.waiting;
|
||||||
|
|
||||||
|
const project = await Project.createProject(name, description, date, defaultStatus);
|
||||||
|
await StorageService.project.createProjectDirectory(project.id);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Projet ajouté avec succès',
|
||||||
|
id: project.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un projet existant
|
||||||
|
*/
|
||||||
|
static deleteProject = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime d'abord le répertoire du projet
|
||||||
|
await StorageService.project.deleteProjectDirectory(projectId);
|
||||||
|
|
||||||
|
// Puis supprime l'entrée dans la base de données
|
||||||
|
await Project.deleteProject(projectId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Projet supprimé avec succès',
|
||||||
|
id: projectId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProjectController;
|
||||||
256
src/controllers/videoController.js
Normal file
256
src/controllers/videoController.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
// src/controllers/videoController.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const rangeParser = require('range-parser');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const VideoService = require('../services/videoService');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux vidéos
|
||||||
|
*/
|
||||||
|
class VideoController {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les vidéos
|
||||||
|
*/
|
||||||
|
static getAllVideos = asyncHandler(async (req, res) => {
|
||||||
|
const videos = await Video.getAllVideos();
|
||||||
|
res.json(videos);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une vidéo par son ID
|
||||||
|
*/
|
||||||
|
static getVideoById = asyncHandler(async (req, res) => {
|
||||||
|
const videoId = req.params.id;
|
||||||
|
|
||||||
|
if (!videoId || isNaN(videoId)) {
|
||||||
|
return sendError('ID de vidéo invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = await Video.getVideoById(videoId);
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
return sendError('Vidéo non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(video);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle vidéo
|
||||||
|
*/
|
||||||
|
static createVideo = asyncHandler(async (req, res) => {
|
||||||
|
const { project_id, measurement_ids, name, resolution, duration } = req.body;
|
||||||
|
|
||||||
|
if (!project_id || !measurement_ids || !name || !resolution || !duration) {
|
||||||
|
return sendError('Tous les champs sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[VIDEO] Création de vidéo avec les mesures:', measurement_ids);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Crée l'entrée vidéo dans la base de données
|
||||||
|
const video = await Video.createVideo(
|
||||||
|
project_id,
|
||||||
|
measurement_ids,
|
||||||
|
name,
|
||||||
|
resolution,
|
||||||
|
duration,
|
||||||
|
config.status.waiting
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!video || !video.id) {
|
||||||
|
return sendError('Erreur lors de la création de la vidéo', res, null, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[VIDEO] Vidéo créée avec succès:', video.id);
|
||||||
|
|
||||||
|
// Parse et récupère les chemins des images
|
||||||
|
const pathList = await this.getMeasurementPathList(measurement_ids, project_id);
|
||||||
|
|
||||||
|
if (!pathList || pathList.length === 0) {
|
||||||
|
return sendError('Aucun chemin trouvé pour les mesures', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse la résolution (ex: 1920x1080)
|
||||||
|
const [resWidth, resHeight] = resolution.split('x').map(Number);
|
||||||
|
if (isNaN(resWidth) || isNaN(resHeight)) {
|
||||||
|
return sendError('Format de résolution invalide. Utiliser LARGEURxHAUTEUR (ex: 1920x1080)', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Démarre le processus de rendu en arrière-plan
|
||||||
|
VideoService.createVideoFromImages(
|
||||||
|
project_id,
|
||||||
|
pathList,
|
||||||
|
duration,
|
||||||
|
video.id,
|
||||||
|
resWidth,
|
||||||
|
resHeight
|
||||||
|
)
|
||||||
|
.then(videoFile => {
|
||||||
|
console.log('[VIDEO] Rendu terminé:', videoFile);
|
||||||
|
return Video.updateVideoFilePath(video.id, videoFile);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('[VIDEO] Échec du rendu:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Réponse immédiate
|
||||||
|
res.json({
|
||||||
|
message: 'Vidéo créée avec succès et le rendu a démarré',
|
||||||
|
id: video.id
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[VIDEO] Erreur lors de la création de la vidéo:', err);
|
||||||
|
sendError('Erreur lors de la création de la vidéo', res, err, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une vidéo existante
|
||||||
|
*/
|
||||||
|
static deleteVideo = asyncHandler(async (req, res) => {
|
||||||
|
const videoId = req.params.id;
|
||||||
|
|
||||||
|
if (!videoId || isNaN(videoId)) {
|
||||||
|
return sendError('ID de vidéo invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les informations de la vidéo
|
||||||
|
const video = await Video.getVideoById(videoId);
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
return sendError('Vidéo non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime le fichier vidéo s'il existe
|
||||||
|
if (video.video_file) {
|
||||||
|
try {
|
||||||
|
console.log('[VIDEO] Suppression du fichier vidéo:', video.video_file);
|
||||||
|
await StorageService.deleteFile(video.video_file);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[VIDEO] Erreur lors de la suppression du fichier vidéo:', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[VIDEO] Pas de fichier vidéo à supprimer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime l'entrée dans la base de données
|
||||||
|
await Video.deleteVideo(videoId);
|
||||||
|
|
||||||
|
res.json({ message: 'Vidéo supprimée avec succès' });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le fichier vidéo avec support du streaming
|
||||||
|
*/
|
||||||
|
static getVideoFile = asyncHandler(async (req, res) => {
|
||||||
|
const videoId = req.params.video_id;
|
||||||
|
const video = await Video.getVideoById(videoId);
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
return sendError('Vidéo non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.status === config.status.waiting || video.status === config.status.inProgress) {
|
||||||
|
return sendError('Vidéo pas encore produite', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoPath = video.video_file;
|
||||||
|
|
||||||
|
// Vérifie si le fichier existe
|
||||||
|
try {
|
||||||
|
await fs.promises.access(videoPath, fs.constants.F_OK);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[VIDEO] Fichier vidéo non trouvé:', err);
|
||||||
|
return sendError('Fichier vidéo non trouvé', res, err, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(videoPath);
|
||||||
|
const fileSize = stat.size;
|
||||||
|
const range = req.headers.range;
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
const parts = rangeParser(fileSize, range);
|
||||||
|
const start = parts[0].start;
|
||||||
|
const end = parts[0].end;
|
||||||
|
const chunksize = (end - start) + 1;
|
||||||
|
const file = fs.createReadStream(videoPath, { start, end });
|
||||||
|
|
||||||
|
const head = {
|
||||||
|
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Content-Length': chunksize,
|
||||||
|
'Content-Type': 'video/mp4',
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(206, head);
|
||||||
|
file.pipe(res);
|
||||||
|
} else {
|
||||||
|
const head = {
|
||||||
|
'Content-Length': fileSize,
|
||||||
|
'Content-Type': 'video/mp4',
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200, head);
|
||||||
|
fs.createReadStream(videoPath).pipe(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la progression d'une vidéo en cours de rendu
|
||||||
|
*/
|
||||||
|
static getVideoProgress = asyncHandler(async (req, res) => {
|
||||||
|
const videoId = req.params.video_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const progress = await VideoService.getVideoProgress(videoId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
progress: progress.progress,
|
||||||
|
elapsed: progress.elapsed,
|
||||||
|
eta: progress.eta,
|
||||||
|
status: VideoService.getStatusLabel(progress.status)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Erreur de récupération de la progression', res, error, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les chemins d'images à partir d'une liste d'IDs de mesures
|
||||||
|
* Méthode utilitaire privée
|
||||||
|
*/
|
||||||
|
static async getMeasurementPathList(idListJson, projectId) {
|
||||||
|
let idList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
idList = JSON.parse(idListJson);
|
||||||
|
|
||||||
|
if (!Array.isArray(idList)) {
|
||||||
|
throw new Error('Le format de la liste des IDs est invalide');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VIDEO] Erreur de parsing de la liste d\'IDs:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathList = [];
|
||||||
|
|
||||||
|
for (const orderId of idList) {
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (measurement && measurement.path) {
|
||||||
|
pathList.push(measurement.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VideoController;
|
||||||
@@ -1,200 +1,30 @@
|
|||||||
const fs = require('fs').promises;
|
/**
|
||||||
const path = require('path');
|
* Ce fichier est conservé pour la rétrocompatibilité mais redirige vers le nouveau service de stockage.
|
||||||
const { Buffer } = require('buffer');
|
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||||
const PROJECTS_DIR = path.join('.');
|
*/
|
||||||
const database_manager = require('../database/database_manager.js');
|
|
||||||
|
|
||||||
async function createFolder(name) {
|
const StorageService = require('../services/storageService');
|
||||||
const projectDir = path.join(PROJECTS_DIR, `${name}`);
|
|
||||||
try {
|
|
||||||
await fs.access(projectDir);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
await fs.mkdir(projectDir, { recursive: true });
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return projectDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFolder(name) {
|
// Structure de redirection pour la compatibilité
|
||||||
const projectDir = path.join(PROJECTS_DIR, `${name}`);
|
const storage_manager = {
|
||||||
try {
|
project: {
|
||||||
await fs.access(projectDir);
|
create_project_directory: StorageService.project.createProjectDirectory,
|
||||||
await fs.rm(projectDir, { recursive: true, force: true });
|
delete_project_directory: StorageService.project.deleteProjectDirectory
|
||||||
} catch (error) {
|
},
|
||||||
if (error.code !== 'ENOENT') {
|
measurement: {
|
||||||
throw error;
|
get_measurement_image: StorageService.measurement.getMeasurementImage,
|
||||||
}
|
upload_measurement_image: StorageService.measurement.uploadMeasurementImage
|
||||||
}
|
},
|
||||||
}
|
video: {
|
||||||
|
get_video: StorageService.video.getVideo,
|
||||||
async function scanAllImages(dir = 'storage') {
|
delete_video: StorageService.video.deleteVideo
|
||||||
const projectDir = path.join(PROJECTS_DIR, dir);
|
},
|
||||||
let results = [];
|
scan_images: StorageService.scanImages,
|
||||||
|
create_directory: StorageService.createDirectory,
|
||||||
// check if the directory exists and create it if not
|
delete_directory: StorageService.deleteDirectory,
|
||||||
try {
|
save_file: StorageService.saveFile,
|
||||||
await fs.access(projectDir);
|
get_file: StorageService.getFile,
|
||||||
} catch (error) {
|
delete_file: StorageService.deleteFile
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
await fs.mkdir(projectDir, { recursive: true });
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scanDirectory(directory) {
|
|
||||||
const files = await fs.readdir(directory);
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(directory, file);
|
|
||||||
const stat = await fs.stat(filePath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
await scanDirectory(filePath);
|
|
||||||
} else if (file.endsWith('.jpg')) {
|
|
||||||
results.push(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await scanDirectory(projectDir);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveFile(filePath, content) {
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
await fs.mkdir(dir, { recursive: true });
|
|
||||||
if (Buffer.isBuffer(content)) {
|
|
||||||
await fs.writeFile(filePath, content);
|
|
||||||
} else {
|
|
||||||
throw new Error('Content must be a buffer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getFile(name) {
|
|
||||||
const filePath = path.join(PROJECTS_DIR, `${name}`);
|
|
||||||
return await fs.readFile(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFile(name) {
|
|
||||||
const filePath = path.join(PROJECTS_DIR, `${name}`);
|
|
||||||
try {
|
|
||||||
await fs.access(filePath); // Vérifie si le fichier existe
|
|
||||||
await fs.rm(filePath); // Supprime le fichier
|
|
||||||
return `File ${filePath} deleted successfully.`;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return `File ${filePath} does not exist.`;
|
|
||||||
} else {
|
|
||||||
throw error; // Relance l'erreur si ce n'est pas une erreur de fichier introuvable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileOperation(operation, ...args) {
|
|
||||||
try {
|
|
||||||
return await operation(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[FILE OPERATION ERROR] ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = {
|
|
||||||
createProjectDirectory: async function (projectId) {
|
|
||||||
const projectPath = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
await handleFileOperation(createFolder, projectPath);
|
|
||||||
await handleFileOperation(createFolder, `${projectPath}/images`);
|
|
||||||
await handleFileOperation(createFolder, `${projectPath}/videos`);
|
|
||||||
console.log("[FILE] createProjectDirectory : " + projectPath);
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteProjectDirectory: async function (projectId) {
|
|
||||||
const projectPath = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
await handleFileOperation(deleteFolder, projectPath);
|
|
||||||
console.log("[FILE] deleteProjectDirectory : " + projectPath);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const measurement = {
|
module.exports = storage_manager;
|
||||||
get_measurement_image: async function (projectId, orderId) {
|
|
||||||
const projectPath = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
const imagePath = path.join(projectPath, 'images', `${orderId}.jpg`);
|
|
||||||
console.log("[FILE] get_measurement_image : " + imagePath);
|
|
||||||
return await handleFileOperation(getFile, imagePath);
|
|
||||||
},
|
|
||||||
|
|
||||||
upload_measurement_image: async function (image, projectId, orderId) {
|
|
||||||
const projectPath = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
const imagePath = path.join(projectPath, 'images', `${orderId}.jpg`);
|
|
||||||
console.log("[FILE] upload_measurement_image : " + imagePath);
|
|
||||||
await handleFileOperation(saveFile, imagePath, image.buffer);
|
|
||||||
return imagePath;
|
|
||||||
},
|
|
||||||
|
|
||||||
get_path_from_id: async function (projectId, orderId) {
|
|
||||||
const query = await database_manager.measurement.get_measurement_by_project_and_order_id(projectId, orderId);
|
|
||||||
console.log("[FILE] get_path_from_id : " + query.path);
|
|
||||||
console.log("[FILE] get_path_from_id : " + query);
|
|
||||||
return query.path;
|
|
||||||
},
|
|
||||||
|
|
||||||
get_path_list: async function (IdList, projectId) {
|
|
||||||
let parsedIdList;
|
|
||||||
try {
|
|
||||||
parsedIdList = JSON.parse(IdList);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error parsing IdList:", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathList = [];
|
|
||||||
for (const orderId of parsedIdList) {
|
|
||||||
const path = await this.get_path_from_id(projectId, orderId);
|
|
||||||
pathList.push(path);
|
|
||||||
}
|
|
||||||
return pathList;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const video = {
|
|
||||||
get_video: async function (projectId, orderId) {
|
|
||||||
const projectPath = path.join(PROJECTS_DIR, `${projectId}`);
|
|
||||||
const videoPath = `/storage/${projectPath}/videos/${orderId}.mp4`;
|
|
||||||
console.log("[FILE] get_video : " + videoPath);
|
|
||||||
return await handleFileOperation(getFile, videoPath);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_video: async function (videoId) {
|
|
||||||
const query = database_manager.video.get_video_by_id(videoId);
|
|
||||||
const videoPath = query.video_file;
|
|
||||||
console.log("[FILE] delete_video : " + videoPath);
|
|
||||||
return await handleFileOperation(deleteFile, videoPath);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete_unfinished_videos: async function () {
|
|
||||||
const unfinishedVideos = await database_manager.video.get_unfinished_videos();
|
|
||||||
|
|
||||||
for (const video of unfinishedVideos) {
|
|
||||||
try {
|
|
||||||
await this.delete_video(video.id);
|
|
||||||
console.log(`Deleted unfinished video with id: ${video.id}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error deleting unfinished video with id: ${video.id}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createFolder,
|
|
||||||
deleteFolder,
|
|
||||||
scanAllImages,
|
|
||||||
saveFile,
|
|
||||||
getFile,
|
|
||||||
deleteFile,
|
|
||||||
project,
|
|
||||||
measurement,
|
|
||||||
video
|
|
||||||
};
|
|
||||||
44
src/database/connection.js
Normal file
44
src/database/connection.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// src/database/connection.js
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Création du client PostgreSQL avec la configuration centralisée
|
||||||
|
const client = new Client({
|
||||||
|
host: config.database.host,
|
||||||
|
port: config.database.port,
|
||||||
|
user: config.database.user,
|
||||||
|
password: config.database.password,
|
||||||
|
database: config.database.database
|
||||||
|
});
|
||||||
|
|
||||||
|
let isConnecting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la connexion à la base de données
|
||||||
|
* Réessaie automatiquement si la connexion échoue
|
||||||
|
*/
|
||||||
|
function initDatabase() {
|
||||||
|
if (isConnecting) {
|
||||||
|
console.log('[DB] Tentative de connexion déjà en cours, ignorer...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DB] Initialisation de la connexion à PostgreSQL...');
|
||||||
|
isConnecting = true;
|
||||||
|
|
||||||
|
client.connect(err => {
|
||||||
|
isConnecting = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error('[DB] Erreur de connexion à la base de données:', err);
|
||||||
|
setTimeout(initDatabase, config.database.reconnectInterval);
|
||||||
|
} else {
|
||||||
|
console.log('[DB] Connecté à la base de données PostgreSQL.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise la connexion lors de l'importation de ce module
|
||||||
|
initDatabase();
|
||||||
|
|
||||||
|
module.exports = client;
|
||||||
@@ -1,302 +1,47 @@
|
|||||||
const db = require('../../db.js');
|
// src/database/database_manager.js
|
||||||
|
/**
|
||||||
|
* Ce fichier est conservé pour la rétrocompatibilité mais redirige vers les nouveaux modèles.
|
||||||
|
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||||
|
*/
|
||||||
|
|
||||||
// Fonctions de gestion de la base de données interne
|
const Project = require('../models/Project');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Camera = require('../models/Camera');
|
||||||
|
|
||||||
async function create_database() {
|
// Structure de redirection pour la compatibilité
|
||||||
const queries = [
|
const database_manager = {
|
||||||
`CREATE TABLE IF NOT EXISTS projects (
|
project: {
|
||||||
id SERIAL PRIMARY KEY,
|
get_all_projects: Project.getAllProjects,
|
||||||
name VARCHAR(255) NOT NULL,
|
get_project_by_id: Project.getProjectById,
|
||||||
description TEXT,
|
create_project: Project.createProject,
|
||||||
start_date DATE,
|
edit_project_by_id: Project.updateProject,
|
||||||
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3]))
|
delete_project: Project.deleteProject,
|
||||||
);`,
|
find_current_rendering_project: Project.findCurrentRenderingProject
|
||||||
`ALTER TABLE projects OWNER TO timelapse;`,
|
},
|
||||||
`CREATE TABLE IF NOT EXISTS measurements (
|
measurement: {
|
||||||
id SERIAL PRIMARY KEY,
|
get_all_measurements: Measurement.getAllMeasurements,
|
||||||
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
get_measurement_by_id: Measurement.getMeasurementById,
|
||||||
timestamp TIMESTAMP NOT NULL,
|
get_measurement_by_project_id_and_order_id: Measurement.getMeasurementByProjectAndOrderId,
|
||||||
path VARCHAR(255),
|
get_measurements_by_project_id: Measurement.getMeasurementsByProjectId,
|
||||||
temperature DOUBLE PRECISION,
|
create_measurement: Measurement.createMeasurement,
|
||||||
humidity DOUBLE PRECISION,
|
edit_measurement_by_id: Measurement.updateMeasurement,
|
||||||
order_id INTEGER NOT NULL,
|
delete_measurement: Measurement.deleteMeasurement,
|
||||||
CONSTRAINT unique_project_photo_order UNIQUE (project_id, order_id)
|
get_next_order_id: Measurement.getNextOrderId
|
||||||
);`,
|
},
|
||||||
`ALTER TABLE measurements OWNER TO timelapse;`,
|
video: {
|
||||||
`CREATE TABLE IF NOT EXISTS videos (
|
get_all_videos: Video.getAllVideos,
|
||||||
id SERIAL PRIMARY KEY,
|
get_video_by_id: Video.getVideoById,
|
||||||
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
get_videos_by_project_id: Video.getVideosByProjectId,
|
||||||
measurement_ids TEXT NOT NULL,
|
create_video: Video.createVideo,
|
||||||
video_file VARCHAR(255),
|
edit_video_by_id: Video.updateVideo,
|
||||||
resolution VARCHAR(255),
|
delete_video: Video.deleteVideo
|
||||||
duration INTEGER,
|
},
|
||||||
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3])),
|
capture: {
|
||||||
name VARCHAR(255),
|
get_camera: Camera.getCamera,
|
||||||
progress DOUBLE PRECISION,
|
edit_camera: Camera.updateCamera,
|
||||||
started_at TIMESTAMP,
|
init_camera: Camera.initializeCamera
|
||||||
updated_at TIMESTAMP,
|
}
|
||||||
eta DOUBLE PRECISION
|
|
||||||
);`,
|
|
||||||
`ALTER TABLE videos OWNER TO timelapse;`,
|
|
||||||
`CREATE TABLE IF NOT EXISTS camera (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
interval INTEGER NOT NULL,
|
|
||||||
maintenance INTEGER NOT NULL,
|
|
||||||
active INTEGER DEFAULT 0 NOT NULL
|
|
||||||
);`,
|
|
||||||
`ALTER TABLE camera OWNER TO timelapse;`
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const query of queries) {
|
|
||||||
await db.query(query);
|
|
||||||
}
|
|
||||||
console.log('Database tables created or verified successfully.');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error creating database tables:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function check_database_existence() {
|
|
||||||
const query = `
|
|
||||||
SELECT table_name
|
|
||||||
FROM information_schema.tables
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name IN ('projects', 'measurements', 'videos', 'camera');
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await db.query(query);
|
|
||||||
const existingTables = result.rows.map(row => row.table_name);
|
|
||||||
|
|
||||||
const requiredTables = ['projects', 'measurements', 'videos', 'camera'];
|
|
||||||
const missingTables = requiredTables.filter(table => !existingTables.includes(table));
|
|
||||||
|
|
||||||
if (missingTables.length > 0) {
|
|
||||||
console.error('Missing or improperly constructed tables:', missingTables);
|
|
||||||
throw new Error(`The following tables are missing or not properly constructed: ${missingTables.join(', ')}`);
|
|
||||||
} else {
|
|
||||||
console.log('All required tables exist and are properly constructed.');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error checking database tables:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function delete_database() {
|
|
||||||
const queries = [
|
|
||||||
`DROP TABLE IF EXISTS videos;`,
|
|
||||||
`DROP TABLE IF EXISTS measurements;`,
|
|
||||||
`DROP TABLE IF EXISTS projects;`,
|
|
||||||
`DROP TABLE IF EXISTS camera;`
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const query of queries) {
|
|
||||||
await db.query(query);
|
|
||||||
}
|
|
||||||
console.log('Database tables deleted successfully.');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error deleting database tables:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init_function() {
|
|
||||||
try {
|
|
||||||
await check_database_existence();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Database check failed:', err);
|
|
||||||
try {
|
|
||||||
await delete_database();
|
|
||||||
await create_database();
|
|
||||||
console.log('Database initialized successfully.');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error initializing database:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
console.log('Database initialization process completed.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init_function()
|
|
||||||
.then(() => console.log('Database initialization completed.'))
|
|
||||||
.catch(err => console.error('Error during database initialization:', err));
|
|
||||||
|
|
||||||
// Fonctions pour les projets
|
|
||||||
function handleDatabaseOperation(operation) {
|
|
||||||
return async (...args) => {
|
|
||||||
try {
|
|
||||||
return await operation(...args);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error during database operation: ${operation.name}`, err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = {
|
|
||||||
get_all_projects: handleDatabaseOperation(async () => {
|
|
||||||
const query = `SELECT * FROM projects;`;
|
|
||||||
return (await db.query(query)).rows;
|
|
||||||
}),
|
|
||||||
get_project_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `SELECT * FROM projects WHERE id = $1;`;
|
|
||||||
return (await db.query(query, [id])).rows[0];
|
|
||||||
}),
|
|
||||||
create_project: handleDatabaseOperation(async (name, description, start_date, status) => {
|
|
||||||
const query = `INSERT INTO projects (name, description, start_date, status) VALUES ($1, $2, $3, $4) RETURNING *;`;
|
|
||||||
return (await db.query(query, [name, description, start_date, status])).rows[0];
|
|
||||||
}),
|
|
||||||
edit_project_by_id: handleDatabaseOperation(async (id, updates) => {
|
|
||||||
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
|
||||||
const values = [id, ...Object.values(updates)];
|
|
||||||
const query = `UPDATE projects SET ${fields} WHERE id = $1 RETURNING *;`;
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
delete_project_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `DELETE FROM projects WHERE id = $1;`;
|
|
||||||
await db.query(query, [id]);
|
|
||||||
}),
|
|
||||||
find_current_rendering_project: handleDatabaseOperation(async () => {
|
|
||||||
const query = `SELECT * FROM projects WHERE status = 1;`;
|
|
||||||
return (await db.query(query)).rows[0];
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const measurement = {
|
module.exports = database_manager;
|
||||||
get_all_measurements: handleDatabaseOperation(async () => {
|
|
||||||
const query = `SELECT * FROM measurements;`;
|
|
||||||
return (await db.query(query)).rows;
|
|
||||||
}),
|
|
||||||
get_measurement_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `SELECT * FROM measurements WHERE id = $1;`;
|
|
||||||
return (await db.query(query, [id])).rows[0];
|
|
||||||
}),
|
|
||||||
get_measurement_by_project_and_order_id: handleDatabaseOperation(async (project_id, order_id) => {
|
|
||||||
const query = `SELECT * FROM measurements WHERE project_id = $1 AND order_id = $2;`;
|
|
||||||
return (await db.query(query, [project_id, order_id])).rows[0];
|
|
||||||
}),
|
|
||||||
get_measurements_by_project_id: handleDatabaseOperation(async (project_id) => {
|
|
||||||
const query = `SELECT * FROM measurements WHERE project_id = $1;`;
|
|
||||||
return (await db.query(query, [project_id])).rows;
|
|
||||||
}),
|
|
||||||
create_measurement: handleDatabaseOperation(async (project_id, timestamp, path, temperature, humidity, order_id) => {
|
|
||||||
const query = `INSERT INTO measurements (project_id, timestamp, path, temperature, humidity, order_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;`;
|
|
||||||
return (await db.query(query, [project_id, timestamp, path, temperature, humidity, order_id])).rows[0];
|
|
||||||
}),
|
|
||||||
edit_measurement_by_id: handleDatabaseOperation(async (id, updates) => {
|
|
||||||
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
|
||||||
const values = [id, ...Object.values(updates)];
|
|
||||||
const query = `UPDATE measurements SET ${fields} WHERE id = $1 RETURNING *;`;
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
edit_measurement_by_project_and_order_id: handleDatabaseOperation(async (project_id, order_id, updates) => {
|
|
||||||
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 3}`).join(', ');
|
|
||||||
const values = [project_id, order_id, ...Object.values(updates)];
|
|
||||||
const query = `UPDATE measurements SET ${fields} WHERE project_id = $1 AND order_id = $2 RETURNING *;`;
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete_measurement_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `DELETE FROM measurements WHERE id = $1;`;
|
|
||||||
await db.query(query, [id]);
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_next_order_id: handleDatabaseOperation(async (project_id) => {
|
|
||||||
const query = `SELECT COALESCE(MAX(order_id), 0) + 1 AS next_order_id FROM measurements WHERE project_id = $1;`;
|
|
||||||
const result = await db.query(query, [project_id]);
|
|
||||||
return result.rows[0].next_order_id;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const video = {
|
|
||||||
get_all_videos: handleDatabaseOperation(async () => {
|
|
||||||
const query = `SELECT * FROM videos;`;
|
|
||||||
return (await db.query(query)).rows;
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_video_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `SELECT * FROM videos WHERE id = $1;`;
|
|
||||||
return (await db.query(query, [id])).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_videos_by_project_id: handleDatabaseOperation(async (project_id) => {
|
|
||||||
const query = `SELECT * FROM videos WHERE project_id = $1;`;
|
|
||||||
return (await db.query(query, [project_id])).rows;
|
|
||||||
}),
|
|
||||||
|
|
||||||
create_video: handleDatabaseOperation(async (projectId, measurementIds, name, resolution, duration, status = 0) => {
|
|
||||||
const query = `INSERT INTO public.videos (project_id, measurement_ids, name, resolution, duration, status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id;`;
|
|
||||||
const values = [projectId, measurementIds, name, resolution, duration, status];
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
edit_video_by_id: handleDatabaseOperation(async (id, updates) => {
|
|
||||||
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
|
||||||
const values = [id, ...Object.values(updates)];
|
|
||||||
const query = `UPDATE videos SET ${fields} WHERE id = $1 RETURNING *;`;
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
update_video_file_path_by_id: handleDatabaseOperation(async (id, video_file) => {
|
|
||||||
const query = `UPDATE videos SET video_file = $1 WHERE id = $2 RETURNING *;`;
|
|
||||||
return (await db.query(query, [video_file, id])).rows[0];
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
delete_video_by_id: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `DELETE FROM videos WHERE id = $1;`;
|
|
||||||
await db.query(query, [id]);
|
|
||||||
}),
|
|
||||||
|
|
||||||
get_unfinished_videos: handleDatabaseOperation(async () => {
|
|
||||||
// récupérer liste des vidéos dont le status est = 0, 2 ou 3
|
|
||||||
const query = `SELECT * FROM videos WHERE status IN (0, 2, 3);`;
|
|
||||||
return (await db.query(query)).rows;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const capture = {
|
|
||||||
get_camera: handleDatabaseOperation(async () => {
|
|
||||||
const query = `SELECT * FROM camera WHERE id = 1;`;
|
|
||||||
return (await db.query(query)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
edit_camera: handleDatabaseOperation(async (id, updates) => {
|
|
||||||
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
|
||||||
const values = [id, ...Object.values(updates)];
|
|
||||||
const query = `UPDATE camera SET ${fields} WHERE id = $1 RETURNING *;`;
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete_camera: handleDatabaseOperation(async (id) => {
|
|
||||||
const query = `DELETE FROM camera WHERE id = $1;`;
|
|
||||||
await db.query(query, [id]);
|
|
||||||
}),
|
|
||||||
|
|
||||||
init_camera: handleDatabaseOperation(async (id, interval, nb_images, maintenance, stop_flag, idle) => {
|
|
||||||
const query = `INSERT INTO camera (id, interval, nb_images, maintenance, stop_flag, idle) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;`;
|
|
||||||
const values = [id, interval, nb_images, maintenance, stop_flag, idle];
|
|
||||||
return (await db.query(query, values)).rows[0];
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// zone de test
|
|
||||||
async function test_zone(){
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
test_zone();
|
|
||||||
|
|
||||||
// Export des modules
|
|
||||||
module.exports = {
|
|
||||||
project,
|
|
||||||
measurement,
|
|
||||||
video,
|
|
||||||
capture,
|
|
||||||
};
|
|
||||||
|
|||||||
72
src/models/Camera.js
Normal file
72
src/models/Camera.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// src/models/Camera.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion de la caméra
|
||||||
|
*/
|
||||||
|
class Camera {
|
||||||
|
/**
|
||||||
|
* Récupère les paramètres de la caméra
|
||||||
|
* @returns {Promise<Object|null>} Paramètres de la caméra ou null si non trouvés
|
||||||
|
*/
|
||||||
|
static getCamera = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM camera WHERE id = 1;`;
|
||||||
|
const result = await db.query(query);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les paramètres de la caméra
|
||||||
|
* @param {number} id - ID de l'entrée caméra (normalement 1)
|
||||||
|
* @param {Object} updates - Paramètres à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Paramètres mis à jour ou null si non trouvés
|
||||||
|
*/
|
||||||
|
static updateCamera = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE camera SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les paramètres de la caméra
|
||||||
|
* @param {number} id - ID de l'entrée caméra à supprimer
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteCamera = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM camera WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les paramètres de la caméra par défaut
|
||||||
|
* @returns {Promise<Object>} Paramètres de caméra créés
|
||||||
|
*/
|
||||||
|
static initializeCamera = wrapDatabaseOperation(async () => {
|
||||||
|
const { defaultSettings } = config.camera;
|
||||||
|
const query = `
|
||||||
|
INSERT INTO camera (id, interval, maintenance, active)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT (id) DO NOTHING
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(query, [
|
||||||
|
defaultSettings.id,
|
||||||
|
defaultSettings.interval,
|
||||||
|
defaultSettings.maintenance ? 1 : 0,
|
||||||
|
defaultSettings.idle ? 0 : 1
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Si l'insertion a échoué à cause d'un conflit, récupérer l'enregistrement existant
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return await this.getCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Camera;
|
||||||
126
src/models/Measurement.js
Normal file
126
src/models/Measurement.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// src/models/Measurement.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion des mesures (photos avec données)
|
||||||
|
*/
|
||||||
|
class Measurement {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures
|
||||||
|
* @returns {Promise<Array>} Liste de toutes les mesures
|
||||||
|
*/
|
||||||
|
static getAllMeasurements = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM measurements;`;
|
||||||
|
return (await db.query(query)).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son ID
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static getMeasurementById = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE id = $1;`;
|
||||||
|
const result = await db.query(query, [id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son projet ID et son ordre ID
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static getMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE project_id = $1 AND order_id = $2;`;
|
||||||
|
const result = await db.query(query, [projectId, orderId]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures d'un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @returns {Promise<Array>} Liste des mesures du projet
|
||||||
|
*/
|
||||||
|
static getMeasurementsByProjectId = wrapDatabaseOperation(async (projectId) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE project_id = $1 ORDER BY order_id;`;
|
||||||
|
return (await db.query(query, [projectId])).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle mesure
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {string} timestamp - Horodatage de la mesure
|
||||||
|
* @param {string} path - Chemin vers l'image
|
||||||
|
* @param {number} temperature - Température mesurée
|
||||||
|
* @param {number} humidity - Humidité mesurée
|
||||||
|
* @param {number} orderId - Ordre séquentiel de la mesure
|
||||||
|
* @returns {Promise<Object>} Mesure créée
|
||||||
|
*/
|
||||||
|
static createMeasurement = wrapDatabaseOperation(async (
|
||||||
|
projectId, timestamp, path, temperature, humidity, orderId
|
||||||
|
) => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO measurements (project_id, timestamp, path, temperature, humidity, order_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(
|
||||||
|
query, [projectId, timestamp, path, temperature, humidity, orderId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une mesure existante
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateMeasurement = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE measurements SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une mesure par projet ID et ordre ID
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 3}`).join(', ');
|
||||||
|
const values = [projectId, orderId, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE measurements SET ${fields} WHERE project_id = $1 AND order_id = $2 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par son ID
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteMeasurement = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM measurements WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient le prochain ordre ID disponible pour un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @returns {Promise<number>} Prochain ordre ID
|
||||||
|
*/
|
||||||
|
static getNextOrderId = wrapDatabaseOperation(async (projectId) => {
|
||||||
|
const query = `SELECT COALESCE(MAX(order_id), 0) + 1 AS next_order_id FROM measurements WHERE project_id = $1;`;
|
||||||
|
const result = await db.query(query, [projectId]);
|
||||||
|
return result.rows[0].next_order_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Measurement;
|
||||||
83
src/models/Project.js
Normal file
83
src/models/Project.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// src/models/Project.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion des projets
|
||||||
|
*/
|
||||||
|
class Project {
|
||||||
|
/**
|
||||||
|
* Récupère tous les projets
|
||||||
|
* @returns {Promise<Array>} Liste de tous les projets
|
||||||
|
*/
|
||||||
|
static getAllProjects = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM projects;`;
|
||||||
|
return (await db.query(query)).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un projet par son ID
|
||||||
|
* @param {number} id - ID du projet
|
||||||
|
* @returns {Promise<Object|null>} Détails du projet ou null si non trouvé
|
||||||
|
*/
|
||||||
|
static getProjectById = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `SELECT * FROM projects WHERE id = $1;`;
|
||||||
|
const result = await db.query(query, [id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau projet
|
||||||
|
* @param {string} name - Nom du projet
|
||||||
|
* @param {string} description - Description du projet
|
||||||
|
* @param {Date} startDate - Date de début du projet
|
||||||
|
* @param {number} status - Statut du projet
|
||||||
|
* @returns {Promise<Object>} Projet créé
|
||||||
|
*/
|
||||||
|
static createProject = wrapDatabaseOperation(async (name, description, startDate, status) => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO projects (name, description, start_date, status)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(query, [name, description, startDate, status]);
|
||||||
|
return result.rows[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour un projet existant
|
||||||
|
* @param {number} id - ID du projet
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Projet mis à jour ou null si non trouvé
|
||||||
|
*/
|
||||||
|
static updateProject = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE projects SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un projet par son ID
|
||||||
|
* @param {number} id - ID du projet
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteProject = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM projects WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve le projet en cours de rendu (status = 1)
|
||||||
|
* @returns {Promise<Object|null>} Projet en cours de rendu ou null
|
||||||
|
*/
|
||||||
|
static findCurrentRenderingProject = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM projects WHERE status = ${config.status.inProgress};`;
|
||||||
|
const result = await db.query(query);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Project;
|
||||||
135
src/models/Video.js
Normal file
135
src/models/Video.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// src/models/Video.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion des vidéos
|
||||||
|
*/
|
||||||
|
class Video {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les vidéos
|
||||||
|
* @returns {Promise<Array>} Liste de toutes les vidéos
|
||||||
|
*/
|
||||||
|
static getAllVideos = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM videos;`;
|
||||||
|
return (await db.query(query)).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une vidéo par son ID
|
||||||
|
* @param {number} id - ID de la vidéo
|
||||||
|
* @returns {Promise<Object|null>} Détails de la vidéo ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static getVideoById = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `SELECT * FROM videos WHERE id = $1;`;
|
||||||
|
const result = await db.query(query, [id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les vidéos d'un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @returns {Promise<Array>} Liste des vidéos du projet
|
||||||
|
*/
|
||||||
|
static getVideosByProjectId = wrapDatabaseOperation(async (projectId) => {
|
||||||
|
const query = `SELECT * FROM videos WHERE project_id = $1;`;
|
||||||
|
return (await db.query(query, [projectId])).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle vidéo
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {string} measurementIds - IDs des mesures (format JSON)
|
||||||
|
* @param {string} name - Nom de la vidéo
|
||||||
|
* @param {string} resolution - Résolution de la vidéo
|
||||||
|
* @param {number} duration - Durée de la vidéo en secondes
|
||||||
|
* @param {number} status - Statut de la vidéo (défaut: 0 = en attente)
|
||||||
|
* @returns {Promise<Object>} Vidéo créée
|
||||||
|
*/
|
||||||
|
static createVideo = wrapDatabaseOperation(async (
|
||||||
|
projectId, measurementIds, name, resolution, duration, status = config.status.waiting
|
||||||
|
) => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO videos (project_id, measurement_ids, name, resolution, duration, status)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(
|
||||||
|
query, [projectId, measurementIds, name, resolution, duration, status]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une vidéo existante
|
||||||
|
* @param {number} id - ID de la vidéo
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateVideo = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE videos SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le chemin du fichier vidéo
|
||||||
|
* @param {number} id - ID de la vidéo
|
||||||
|
* @param {string} videoFile - Chemin du fichier vidéo
|
||||||
|
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateVideoFilePath = wrapDatabaseOperation(async (id, videoFile) => {
|
||||||
|
const query = `UPDATE videos SET video_file = $1 WHERE id = $2 RETURNING *;`;
|
||||||
|
const result = await db.query(query, [videoFile, id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une vidéo par son ID
|
||||||
|
* @param {number} id - ID de la vidéo
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteVideo = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM videos WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les vidéos non terminées (statut 0, 2 ou 3)
|
||||||
|
* @returns {Promise<Array>} Liste des vidéos non terminées
|
||||||
|
*/
|
||||||
|
static getUnfinishedVideos = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `
|
||||||
|
SELECT * FROM videos
|
||||||
|
WHERE status IN (
|
||||||
|
${config.status.waiting},
|
||||||
|
${config.status.failed},
|
||||||
|
${config.status.inProgress}
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
return (await db.query(query)).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la progression d'une vidéo en cours de rendu
|
||||||
|
* @param {number} id - ID de la vidéo
|
||||||
|
* @param {number} progress - Pourcentage de progression
|
||||||
|
* @param {number} eta - Temps estimé restant en secondes
|
||||||
|
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateVideoProgress = wrapDatabaseOperation(async (id, progress, eta) => {
|
||||||
|
const query = `
|
||||||
|
UPDATE videos
|
||||||
|
SET progress = $1, eta = $2, updated_at = NOW()
|
||||||
|
WHERE id = $3
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(query, [progress, eta, id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Video;
|
||||||
144
src/models/database.js
Normal file
144
src/models/database.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// src/models/database.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module gérant l'initialisation et la maintenance de la structure de la base de données
|
||||||
|
*/
|
||||||
|
class DatabaseManager {
|
||||||
|
/**
|
||||||
|
* Crée les tables de la base de données si elles n'existent pas déjà
|
||||||
|
*/
|
||||||
|
static async createDatabase() {
|
||||||
|
const queries = [
|
||||||
|
`CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
start_date DATE,
|
||||||
|
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3]))
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE projects OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS measurements (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
path VARCHAR(255),
|
||||||
|
temperature DOUBLE PRECISION,
|
||||||
|
humidity DOUBLE PRECISION,
|
||||||
|
order_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT unique_project_photo_order UNIQUE (project_id, order_id)
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE measurements OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS videos (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
||||||
|
measurement_ids TEXT NOT NULL,
|
||||||
|
video_file VARCHAR(255),
|
||||||
|
resolution VARCHAR(255),
|
||||||
|
duration INTEGER,
|
||||||
|
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3])),
|
||||||
|
name VARCHAR(255),
|
||||||
|
progress DOUBLE PRECISION,
|
||||||
|
started_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
eta DOUBLE PRECISION
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE videos OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS camera (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
interval INTEGER NOT NULL,
|
||||||
|
maintenance INTEGER NOT NULL,
|
||||||
|
active INTEGER DEFAULT 0 NOT NULL
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE camera OWNER TO timelapse;`
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const query of queries) {
|
||||||
|
await db.query(query);
|
||||||
|
}
|
||||||
|
console.log('[DB] Tables créées ou vérifiées avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la création des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence des tables requises dans la base de données
|
||||||
|
*/
|
||||||
|
static async checkDatabaseExistence() {
|
||||||
|
const query = `
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('projects', 'measurements', 'videos', 'camera');
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(query);
|
||||||
|
const existingTables = result.rows.map(row => row.table_name);
|
||||||
|
|
||||||
|
const requiredTables = ['projects', 'measurements', 'videos', 'camera'];
|
||||||
|
const missingTables = requiredTables.filter(table => !existingTables.includes(table));
|
||||||
|
|
||||||
|
if (missingTables.length > 0) {
|
||||||
|
console.error('[DB] Tables manquantes ou incorrectement construites:', missingTables);
|
||||||
|
throw new Error(`Les tables suivantes sont manquantes ou incorrectement construites: ${missingTables.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log('[DB] Toutes les tables requises existent et sont correctement construites.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la vérification des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime toutes les tables de la base de données
|
||||||
|
*/
|
||||||
|
static async deleteDatabase() {
|
||||||
|
const queries = [
|
||||||
|
`DROP TABLE IF EXISTS videos;`,
|
||||||
|
`DROP TABLE IF EXISTS measurements;`,
|
||||||
|
`DROP TABLE IF EXISTS projects;`,
|
||||||
|
`DROP TABLE IF EXISTS camera;`
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const query of queries) {
|
||||||
|
await db.query(query);
|
||||||
|
}
|
||||||
|
console.log('[DB] Tables supprimées avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la suppression des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la base de données en vérifiant et en créant les tables si nécessaire
|
||||||
|
*/
|
||||||
|
static async initialize() {
|
||||||
|
try {
|
||||||
|
await this.checkDatabaseExistence();
|
||||||
|
console.log('[DB] Structure de base de données validée');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Vérification de la base de données échouée:', err);
|
||||||
|
try {
|
||||||
|
await this.deleteDatabase();
|
||||||
|
await this.createDatabase();
|
||||||
|
console.log('[DB] Base de données initialisée avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de l\'initialisation de la base de données:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
console.log('[DB] Processus d\'initialisation de la base de données terminé.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DatabaseManager;
|
||||||
117
src/routes/cameraRoutes.js
Normal file
117
src/routes/cameraRoutes.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// src/routes/cameraRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const CameraController = require('../controllers/cameraController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /camera/status:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère le statut actuel de la caméra
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Statut de la caméra
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* interval:
|
||||||
|
* type: integer
|
||||||
|
* nullable: true
|
||||||
|
* maintenance:
|
||||||
|
* type: integer
|
||||||
|
* active:
|
||||||
|
* type: integer
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/camera/status', CameraController.getCameraStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /procedure/start:
|
||||||
|
* post:
|
||||||
|
* summary: Démarre une procédure de capture
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* project_id:
|
||||||
|
* type: integer
|
||||||
|
* interval:
|
||||||
|
* type: integer
|
||||||
|
* nb_images:
|
||||||
|
* type: integer
|
||||||
|
* required:
|
||||||
|
* - project_id
|
||||||
|
* - interval
|
||||||
|
* - nb_images
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Procédure démarrée avec succès
|
||||||
|
* 400:
|
||||||
|
* description: Paramètres invalides ou procédure déjà en cours
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/procedure/start', CameraController.startProcedure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /procedure/stop:
|
||||||
|
* post:
|
||||||
|
* summary: Initie l'arrêt d'une procédure de capture
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Procédure d'arrêt initiée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/procedure/stop', CameraController.stopProcedure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /camera/stop:
|
||||||
|
* post:
|
||||||
|
* summary: Confirme l'arrêt de la caméra
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Caméra arrêtée avec succès
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/camera/stop', CameraController.confirmStopProcedure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /camera/maintenance:
|
||||||
|
* post:
|
||||||
|
* summary: Active le mode maintenance
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Mode maintenance activé
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/camera/maintenance', CameraController.activateMaintenance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /camera/maintenance/deactivate:
|
||||||
|
* post:
|
||||||
|
* summary: Désactive le mode maintenance
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Mode maintenance désactivé
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/camera/maintenance/deactivate', CameraController.deactivateMaintenance);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
143
src/routes/imageRoutes.js
Normal file
143
src/routes/imageRoutes.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// src/routes/imageRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const multer = require('multer');
|
||||||
|
const ImageController = require('../controllers/imageController');
|
||||||
|
|
||||||
|
// Configuration de Multer pour le téléchargement des images
|
||||||
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /smile:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une image de test (smile)
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Image de test
|
||||||
|
* 404:
|
||||||
|
* description: Image non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/smile', ImageController.getSmileImage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /images/{projectId}/{orderId}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une image par projet ID et ordre ID
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: projectId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* - in: path
|
||||||
|
* name: orderId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID d'ordre
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Image
|
||||||
|
* 400:
|
||||||
|
* description: IDs invalides
|
||||||
|
* 404:
|
||||||
|
* description: Image non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/images/:projectId/:orderId', ImageController.getImageByProjectAndOrderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /images/{measurementId}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une image par ID de mesure
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: measurementId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la mesure
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Image
|
||||||
|
* 400:
|
||||||
|
* description: ID invalide
|
||||||
|
* 404:
|
||||||
|
* description: Image non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/images/:measurementId', ImageController.getImageByMeasurementId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /preview/{projectId}/{orderId}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère un aperçu d'une image
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: projectId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* - in: path
|
||||||
|
* name: orderId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID d'ordre
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Aperçu d'image redimensionné
|
||||||
|
* 400:
|
||||||
|
* description: IDs invalides
|
||||||
|
* 404:
|
||||||
|
* description: Image non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/preview/:projectId/:orderId', ImageController.getImagePreview);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /camera/upload:
|
||||||
|
* post:
|
||||||
|
* summary: Télécharge une image avec données de mesure
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* multipart/form-data:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* image:
|
||||||
|
* type: string
|
||||||
|
* format: binary
|
||||||
|
* projectId:
|
||||||
|
* type: integer
|
||||||
|
* timestamp:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* temperature:
|
||||||
|
* type: number
|
||||||
|
* humidity:
|
||||||
|
* type: number
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Image téléchargée avec succès
|
||||||
|
* 400:
|
||||||
|
* description: Paramètres invalides
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/camera/upload', upload.single('image'), ImageController.uploadImage);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
34
src/routes/index.js
Normal file
34
src/routes/index.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/routes/index.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const cors = require('cors');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Importe toutes les routes modulaires
|
||||||
|
const projectRoutes = require('./projectRoutes');
|
||||||
|
const measurementRoutes = require('./measurementRoutes');
|
||||||
|
const videoRoutes = require('./videoRoutes');
|
||||||
|
const imageRoutes = require('./imageRoutes');
|
||||||
|
const cameraRoutes = require('./cameraRoutes');
|
||||||
|
|
||||||
|
// Configuration CORS
|
||||||
|
router.use(cors({
|
||||||
|
origin: config.server.cors.origins,
|
||||||
|
methods: config.server.cors.methods,
|
||||||
|
allowedHeaders: config.server.cors.allowedHeaders,
|
||||||
|
credentials: config.server.cors.credentials
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Enregistre toutes les routes
|
||||||
|
router.use(projectRoutes);
|
||||||
|
router.use(measurementRoutes);
|
||||||
|
router.use(videoRoutes);
|
||||||
|
router.use(imageRoutes);
|
||||||
|
router.use(cameraRoutes);
|
||||||
|
|
||||||
|
// Route de test/santé de l'API
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', uptime: process.uptime() });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
99
src/routes/measurementRoutes.js
Normal file
99
src/routes/measurementRoutes.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// src/routes/measurementRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const MeasurementController = require('../controllers/measurementController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /measurements:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère toutes les mesures
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste de toutes les mesures
|
||||||
|
* 404:
|
||||||
|
* description: Aucune mesure trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/measurements', MeasurementController.getAllMeasurements);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /measurements/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une mesure par ID
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la mesure
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Détails de la mesure
|
||||||
|
* 400:
|
||||||
|
* description: ID de mesure invalide
|
||||||
|
* 404:
|
||||||
|
* description: Mesure non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/measurements/:id', MeasurementController.getMeasurementById);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /measurements/{projectId}/{orderId}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une mesure par projet ID et ordre ID
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: projectId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* - in: path
|
||||||
|
* name: orderId
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID d'ordre
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Détails de la mesure
|
||||||
|
* 400:
|
||||||
|
* description: IDs invalides
|
||||||
|
* 404:
|
||||||
|
* description: Mesure non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/measurements/:projectId/:orderId', MeasurementController.getMeasurementByProjectAndOrderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /measurements/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Supprime une mesure
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la mesure
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Mesure supprimée avec succès
|
||||||
|
* 400:
|
||||||
|
* description: ID de mesure invalide
|
||||||
|
* 404:
|
||||||
|
* description: Mesure non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.delete('/measurements/:id', MeasurementController.deleteMeasurement);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
142
src/routes/projectRoutes.js
Normal file
142
src/routes/projectRoutes.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// src/routes/projectRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const ProjectController = require('../controllers/projectController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère tous les projets
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste de tous les projets
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/projects', ProjectController.getAllProjects);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère un projet par ID
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Détails du projet
|
||||||
|
* 400:
|
||||||
|
* description: ID de projet invalide
|
||||||
|
* 404:
|
||||||
|
* description: Projet non trouvé
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/projects/:id', ProjectController.getProjectById);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects/{id}/videos:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère les vidéos d'un projet
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste des vidéos du projet
|
||||||
|
* 400:
|
||||||
|
* description: ID de projet invalide
|
||||||
|
* 404:
|
||||||
|
* description: Aucune vidéo trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/projects/:id/videos', ProjectController.getProjectVideos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects/{id}/measurements:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère les mesures d'un projet
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste des mesures du projet
|
||||||
|
* 400:
|
||||||
|
* description: ID de projet invalide
|
||||||
|
* 404:
|
||||||
|
* description: Aucune mesure trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/projects/:id/measurements', ProjectController.getProjectMeasurements);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects:
|
||||||
|
* post:
|
||||||
|
* summary: Crée un nouveau projet
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - description
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Projet créé avec succès
|
||||||
|
* 400:
|
||||||
|
* description: Paramètres invalides
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/projects', ProjectController.createProject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /projects/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Supprime un projet
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID du projet
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Projet supprimé avec succès
|
||||||
|
* 400:
|
||||||
|
* description: ID de projet invalide
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.delete('/projects/:id', ProjectController.deleteProject);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
169
src/routes/videoRoutes.js
Normal file
169
src/routes/videoRoutes.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// src/routes/videoRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const VideoController = require('../controllers/videoController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère toutes les vidéos
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Liste de toutes les vidéos
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/videos', VideoController.getAllVideos);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère une vidéo par ID
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la vidéo
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Détails de la vidéo
|
||||||
|
* 400:
|
||||||
|
* description: ID de vidéo invalide
|
||||||
|
* 404:
|
||||||
|
* description: Vidéo non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/videos/:id', VideoController.getVideoById);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos:
|
||||||
|
* post:
|
||||||
|
* summary: Crée une nouvelle vidéo
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* project_id:
|
||||||
|
* type: integer
|
||||||
|
* measurement_ids:
|
||||||
|
* type: string
|
||||||
|
* description: JSON array of measurement IDs
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* resolution:
|
||||||
|
* type: string
|
||||||
|
* example: "1920x1080"
|
||||||
|
* duration:
|
||||||
|
* type: integer
|
||||||
|
* description: Duration in seconds
|
||||||
|
* required:
|
||||||
|
* - project_id
|
||||||
|
* - measurement_ids
|
||||||
|
* - name
|
||||||
|
* - resolution
|
||||||
|
* - duration
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Vidéo créée avec succès
|
||||||
|
* 400:
|
||||||
|
* description: Paramètres invalides
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.post('/videos', VideoController.createVideo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: Supprime une vidéo
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la vidéo
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Vidéo supprimée avec succès
|
||||||
|
* 400:
|
||||||
|
* description: ID de vidéo invalide
|
||||||
|
* 404:
|
||||||
|
* description: Vidéo non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.delete('/videos/:id', VideoController.deleteVideo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos/file/{video_id}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère le fichier vidéo
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: video_id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la vidéo
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Fichier vidéo (stream)
|
||||||
|
* 206:
|
||||||
|
* description: Fichier vidéo partiel (range request)
|
||||||
|
* 400:
|
||||||
|
* description: Vidéo pas encore produite
|
||||||
|
* 404:
|
||||||
|
* description: Vidéo non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/videos/file/:video_id', VideoController.getVideoFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /videos/progress/{video_id}:
|
||||||
|
* get:
|
||||||
|
* summary: Récupère la progression du rendu d'une vidéo
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: video_id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: ID de la vidéo
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Informations de progression
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* progress:
|
||||||
|
* type: number
|
||||||
|
* elapsed:
|
||||||
|
* type: number
|
||||||
|
* eta:
|
||||||
|
* type: number
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* 404:
|
||||||
|
* description: Vidéo non trouvée
|
||||||
|
* 500:
|
||||||
|
* description: Erreur serveur
|
||||||
|
*/
|
||||||
|
router.get('/videos/progress/:video_id', VideoController.getVideoProgress);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
209
src/services/storageService.js
Normal file
209
src/services/storageService.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// src/services/storageService.js
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion du stockage des fichiers
|
||||||
|
*/
|
||||||
|
class StorageService {
|
||||||
|
/**
|
||||||
|
* Crée un dossier s'il n'existe pas déjà
|
||||||
|
* @param {string} dirPath - Chemin du dossier à créer
|
||||||
|
* @returns {Promise<string>} Chemin du dossier créé
|
||||||
|
*/
|
||||||
|
static async createDirectory(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un dossier et son contenu
|
||||||
|
* @param {string} dirPath - Chemin du dossier à supprimer
|
||||||
|
*/
|
||||||
|
static async deleteDirectory(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
await fs.rm(dirPath, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cherche toutes les images dans un dossier
|
||||||
|
* @param {string} dirPath - Dossier à scanner
|
||||||
|
* @returns {Promise<Array<string>>} Liste des chemins d'images trouvées
|
||||||
|
*/
|
||||||
|
static async scanImages(dirPath = 'storage') {
|
||||||
|
const basePath = path.join(config.paths.storage, dirPath);
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(basePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
await fs.mkdir(basePath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanDirectory(directory) {
|
||||||
|
const files = await fs.readdir(directory);
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(directory, file);
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await scanDirectory(filePath);
|
||||||
|
} else if (file.endsWith('.jpg')) {
|
||||||
|
results.push(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await scanDirectory(basePath);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un contenu dans un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier
|
||||||
|
* @param {Buffer} content - Contenu à enregistrer
|
||||||
|
*/
|
||||||
|
static async saveFile(filePath, content) {
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
await this.createDirectory(dirPath);
|
||||||
|
if (Buffer.isBuffer(content)) {
|
||||||
|
await fs.writeFile(filePath, content);
|
||||||
|
} else {
|
||||||
|
throw new Error('Le contenu doit être un buffer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le contenu d'un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier
|
||||||
|
* @returns {Promise<Buffer>} Contenu du fichier
|
||||||
|
*/
|
||||||
|
static async getFile(filePath) {
|
||||||
|
return await fs.readFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier à supprimer
|
||||||
|
* @returns {Promise<string>} Message de confirmation
|
||||||
|
*/
|
||||||
|
static async deleteFile(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
await fs.rm(filePath);
|
||||||
|
return `Fichier ${filePath} supprimé avec succès.`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return `Fichier ${filePath} inexistant.`;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de projet
|
||||||
|
*/
|
||||||
|
static project = {
|
||||||
|
/**
|
||||||
|
* Crée le répertoire d'un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
*/
|
||||||
|
createProjectDirectory: async function(projectId) {
|
||||||
|
const projectPath = path.join(config.paths.storage, `${projectId}`);
|
||||||
|
await StorageService.createDirectory(projectPath);
|
||||||
|
await StorageService.createDirectory(path.join(projectPath, 'images'));
|
||||||
|
await StorageService.createDirectory(path.join(projectPath, 'videos'));
|
||||||
|
console.log(`[STORAGE] Répertoire créé : ${projectPath}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime le répertoire d'un projet et son contenu
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
*/
|
||||||
|
deleteProjectDirectory: async function(projectId) {
|
||||||
|
const projectPath = path.join(config.paths.storage, `${projectId}`);
|
||||||
|
await StorageService.deleteDirectory(projectPath);
|
||||||
|
console.log(`[STORAGE] Répertoire supprimé : ${projectPath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de mesures (images)
|
||||||
|
*/
|
||||||
|
static measurement = {
|
||||||
|
/**
|
||||||
|
* Récupère l'image d'une mesure
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<Buffer>} Contenu de l'image
|
||||||
|
*/
|
||||||
|
getMeasurementImage: async function(projectId, orderId) {
|
||||||
|
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
|
||||||
|
console.log(`[STORAGE] Récupération de l'image : ${imagePath}`);
|
||||||
|
return await StorageService.getFile(imagePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre l'image d'une mesure
|
||||||
|
* @param {Object} image - Objet image avec buffer
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<string>} Chemin de l'image enregistrée
|
||||||
|
*/
|
||||||
|
uploadMeasurementImage: async function(image, projectId, orderId) {
|
||||||
|
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
|
||||||
|
console.log(`[STORAGE] Enregistrement de l'image : ${imagePath}`);
|
||||||
|
await StorageService.saveFile(imagePath, image.buffer);
|
||||||
|
return imagePath;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de vidéos
|
||||||
|
*/
|
||||||
|
static video = {
|
||||||
|
/**
|
||||||
|
* Récupère une vidéo
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} videoId - ID de la vidéo
|
||||||
|
* @returns {Promise<Buffer>} Contenu de la vidéo
|
||||||
|
*/
|
||||||
|
getVideo: async function(projectId, videoId) {
|
||||||
|
const videoPath = path.join(config.paths.storage, `${projectId}`, 'videos', `${videoId}.mp4`);
|
||||||
|
console.log(`[STORAGE] Récupération de la vidéo : ${videoPath}`);
|
||||||
|
return await StorageService.getFile(videoPath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une vidéo
|
||||||
|
* @param {string} videoPath - Chemin de la vidéo à supprimer
|
||||||
|
* @returns {Promise<string>} Message de confirmation
|
||||||
|
*/
|
||||||
|
deleteVideo: async function(videoPath) {
|
||||||
|
console.log(`[STORAGE] Suppression de la vidéo : ${videoPath}`);
|
||||||
|
return await StorageService.deleteFile(videoPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StorageService;
|
||||||
218
src/services/videoService.js
Normal file
218
src/services/videoService.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// src/services/videoService.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const config = require('../config');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des opérations vidéo
|
||||||
|
*/
|
||||||
|
class VideoService {
|
||||||
|
/**
|
||||||
|
* Crée une vidéo à partir d'une liste d'images
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {Array<string>} pathList - Liste des chemins d'images
|
||||||
|
* @param {number} duration - Durée souhaitée en secondes
|
||||||
|
* @param {number} videoId - ID de la vidéo
|
||||||
|
* @param {number} resWidth - Largeur de la résolution
|
||||||
|
* @param {number} resHeight - Hauteur de la résolution
|
||||||
|
* @returns {Promise<string>} Chemin du fichier vidéo créé
|
||||||
|
*/
|
||||||
|
static async createVideoFromImages(projectId, pathList, duration, videoId, resWidth, resHeight) {
|
||||||
|
const tempFile = path.join('temp.txt');
|
||||||
|
let ffmpegProcess;
|
||||||
|
let cleanupDone = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Configuration des chemins
|
||||||
|
const workdir = path.join(config.paths.storage, projectId.toString());
|
||||||
|
if (!fs.existsSync(workdir)) {
|
||||||
|
fs.mkdirSync(workdir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie que des images ont été fournies
|
||||||
|
if (!Array.isArray(pathList) || pathList.length === 0) {
|
||||||
|
throw new Error('Liste d\'images vide ou invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tri des images par ordre numérique
|
||||||
|
const sortedImages = pathList.sort((a, b) => {
|
||||||
|
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
|
||||||
|
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
|
||||||
|
return numA - numB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Création du fichier temporaire pour FFmpeg
|
||||||
|
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
|
||||||
|
|
||||||
|
// Calcul des paramètres vidéo
|
||||||
|
const totalFrames = sortedImages.length;
|
||||||
|
const frameRate = Math.ceil(totalFrames / parseInt(duration));
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0];
|
||||||
|
const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0];
|
||||||
|
const outputVideo = path.join(
|
||||||
|
workdir,
|
||||||
|
`${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mise à jour initiale du statut vidéo
|
||||||
|
await Video.updateVideo(videoId, {
|
||||||
|
status: config.status.inProgress,
|
||||||
|
progress: 0,
|
||||||
|
started_at: new Date(),
|
||||||
|
updated_at: new Date(),
|
||||||
|
eta: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration du scaling vidéo
|
||||||
|
const scale = resWidth && resHeight ? `scale=${resWidth}:${resHeight}` : 'scale=854:480';
|
||||||
|
|
||||||
|
// Configuration des arguments FFmpeg
|
||||||
|
const ffmpegArgs = [
|
||||||
|
'-y', // Écrase les fichiers existants sans demander
|
||||||
|
'-r', frameRate.toString(), // Framerate
|
||||||
|
'-f', 'concat', // Format de concaténation
|
||||||
|
'-safe', '0', // Autorise les chemins non sécurisés
|
||||||
|
'-i', tempFile, // Fichier d'entrée
|
||||||
|
'-vsync', 'vfr', // Synchronisation vidéo
|
||||||
|
'-pix_fmt', 'yuv420p', // Format de pixels
|
||||||
|
'-vf', scale, // Filter vidéo pour le scaling
|
||||||
|
'-b:v', '1500k', // Bitrate vidéo
|
||||||
|
outputVideo // Fichier de sortie
|
||||||
|
];
|
||||||
|
|
||||||
|
// Lancement du processus FFmpeg
|
||||||
|
ffmpegProcess = spawn('ffmpeg', ffmpegArgs, {
|
||||||
|
stdio: ['ignore', 'ignore', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastUpdate = 0;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Capture de la progression du rendu
|
||||||
|
ffmpegProcess.stderr.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
const frameMatch = output.match(/frame=\s*(\d+)/);
|
||||||
|
|
||||||
|
if (frameMatch) {
|
||||||
|
const currentFrame = parseInt(frameMatch[1], 10);
|
||||||
|
const progress = Math.min((currentFrame / totalFrames) * 100, 99.99);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Calcul du temps restant estimé
|
||||||
|
const elapsedSeconds = (now - startTime) / 1000;
|
||||||
|
const eta = elapsedSeconds / (currentFrame / totalFrames) - elapsedSeconds;
|
||||||
|
|
||||||
|
// Mise à jour périodique (max toutes les 500ms)
|
||||||
|
if (now - lastUpdate > 500) {
|
||||||
|
Video.updateVideo(videoId, {
|
||||||
|
progress: progress,
|
||||||
|
eta: Math.round(eta),
|
||||||
|
updated_at: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[VIDEO] Progression: ${progress.toFixed(2)}%, ETA: ${eta.toFixed(0)}s`);
|
||||||
|
lastUpdate = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attente de la fin du rendu vidéo
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ffmpegProcess.on('close', async (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
try {
|
||||||
|
// Mise à jour finale de la vidéo
|
||||||
|
await Video.updateVideo(videoId, {
|
||||||
|
status: config.status.completed,
|
||||||
|
progress: 100,
|
||||||
|
eta: 0,
|
||||||
|
video_file: outputVideo,
|
||||||
|
updated_at: new Date()
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`FFmpeg s'est terminé avec le code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpegProcess.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputVideo;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Gestion des erreurs
|
||||||
|
console.error('[VIDEO] Erreur lors de la création de la vidéo:', error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mise à jour du statut vidéo en cas d'erreur
|
||||||
|
await Video.updateVideo(videoId, {
|
||||||
|
status: config.status.failed,
|
||||||
|
progress: 0,
|
||||||
|
eta: null,
|
||||||
|
updated_at: new Date()
|
||||||
|
});
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('[VIDEO] Erreur lors de la mise à jour de la base de données:', dbError);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Nettoyage des ressources
|
||||||
|
if (!cleanupDone) {
|
||||||
|
if (tempFile && fs.existsSync(tempFile)) {
|
||||||
|
fs.unlinkSync(tempFile);
|
||||||
|
}
|
||||||
|
if (ffmpegProcess) {
|
||||||
|
ffmpegProcess.kill();
|
||||||
|
}
|
||||||
|
cleanupDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les informations de progression d'une vidéo
|
||||||
|
* @param {number} videoId - ID de la vidéo
|
||||||
|
* @returns {Promise<Object>} Informations de progression
|
||||||
|
*/
|
||||||
|
static async getVideoProgress(videoId) {
|
||||||
|
const video = await Video.getVideoById(videoId);
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
throw new Error('Vidéo non trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
progress: video.progress,
|
||||||
|
elapsed: video.started_at ? Math.floor((new Date() - new Date(video.started_at)) / 1000) : 0,
|
||||||
|
eta: video.eta,
|
||||||
|
status: video.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit un code de statut en libellé
|
||||||
|
* @param {number} status - Code de statut
|
||||||
|
* @returns {string} Libellé du statut
|
||||||
|
*/
|
||||||
|
static getStatusLabel(status) {
|
||||||
|
const statusMap = {
|
||||||
|
[config.status.waiting]: 'En attente',
|
||||||
|
[config.status.completed]: 'Terminé',
|
||||||
|
[config.status.failed]: 'Échec',
|
||||||
|
[config.status.inProgress]: 'En cours'
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusMap[status] || 'Inconnu';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VideoService;
|
||||||
58
src/utils/errorHandler.js
Normal file
58
src/utils/errorHandler.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// src/utils/errorHandler.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une réponse d'erreur standardisée
|
||||||
|
* @param {string} message - Message d'erreur à afficher
|
||||||
|
* @param {Object} res - Objet Response d'Express
|
||||||
|
* @param {Error|null} error - Objet d'erreur original (facultatif)
|
||||||
|
* @param {number} statusCode - Code HTTP d'erreur (défaut: 500)
|
||||||
|
*/
|
||||||
|
function sendError(message, res, error = null, statusCode = 500) {
|
||||||
|
console.error(`[ERROR] ${message}`, error);
|
||||||
|
|
||||||
|
res.status(statusCode).json({
|
||||||
|
error: {
|
||||||
|
message,
|
||||||
|
statusCode,
|
||||||
|
details: error ? (error.message || String(error)) : null,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper pour les contrôleurs qui gère automatiquement les erreurs
|
||||||
|
* @param {Function} controller - Fonction de contrôleur à exécuter
|
||||||
|
* @returns {Function} - Middleware Express qui gère les erreurs
|
||||||
|
*/
|
||||||
|
function asyncHandler(controller) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await controller(req, res, next);
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Une erreur est survenue lors du traitement de la requête', res, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un wrapper pour les opérations de base de données qui gère les erreurs
|
||||||
|
* @param {Function} operation - Fonction à exécuter
|
||||||
|
* @returns {Function} - Fonction qui execute l'opération et gère les erreurs
|
||||||
|
*/
|
||||||
|
function wrapDatabaseOperation(operation) {
|
||||||
|
return async (...args) => {
|
||||||
|
try {
|
||||||
|
return await operation(...args);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[DB ERROR] Erreur lors de l'opération ${operation.name || 'database'}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendError,
|
||||||
|
asyncHandler,
|
||||||
|
wrapDatabaseOperation
|
||||||
|
};
|
||||||
@@ -1,174 +1,13 @@
|
|||||||
const fs = require('fs');
|
/**
|
||||||
const path = require('path');
|
* Ce fichier est conservé pour la rétrocompatibilité mais redirige vers le nouveau service vidéo.
|
||||||
const { spawn } = require('child_process');
|
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||||
|
*/
|
||||||
|
|
||||||
const database_manager = require('../database/database_manager');
|
const VideoService = require('../services/videoService');
|
||||||
|
|
||||||
const PROJECTS_DIR = path.join('.');
|
|
||||||
|
|
||||||
|
// Fonction de pont pour maintenir la compatibilité avec l'ancien code
|
||||||
async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) {
|
async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) {
|
||||||
const tempFile = path.join('temp.txt');
|
return await VideoService.createVideoFromImages(projectId, pathList, duration, videoId, res_width, res_height);
|
||||||
let ffmpegProcess;
|
|
||||||
let cleanupDone = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Configuration des chemins
|
|
||||||
const workdir = path.join(PROJECTS_DIR, 'storage', projectId.toString());
|
|
||||||
if (!fs.existsSync(workdir)) {
|
|
||||||
fs.mkdirSync(workdir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tri des images
|
|
||||||
const sortedImages = pathList.sort((a, b) => {
|
|
||||||
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
|
|
||||||
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
|
|
||||||
return numA - numB;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Création du fichier temporaire
|
|
||||||
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
|
|
||||||
|
|
||||||
// Calcul des paramètres vidéo
|
|
||||||
const totalFrames = sortedImages.length;
|
|
||||||
const frameRate = Math.ceil(totalFrames / parseInt(duration));
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0];
|
|
||||||
const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0];
|
|
||||||
const outputVideo = path.join(
|
|
||||||
workdir,
|
|
||||||
`${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mise à jour initiale de la base de données
|
|
||||||
let edit_video = {
|
|
||||||
status: 3,
|
|
||||||
progress: 0,
|
|
||||||
started_at: new Date(),
|
|
||||||
updated_at: new Date(),
|
|
||||||
eta: null
|
|
||||||
}
|
|
||||||
await database_manager.video.edit_video_by_id(videoId, edit_video)
|
|
||||||
const scale = res_width && res_height ? `scale=${res_width}:${res_height}` : 'scale=854:480'; // Redimensionne la vidéo en 480p par défaut
|
|
||||||
|
|
||||||
// Configuration de FFmpeg
|
|
||||||
const ffmpegArgs = [
|
|
||||||
'-y',
|
|
||||||
'-r', frameRate.toString(),
|
|
||||||
'-f', 'concat',
|
|
||||||
'-safe', '0',
|
|
||||||
'-i', tempFile,
|
|
||||||
'-vsync', 'vfr',
|
|
||||||
'-pix_fmt', 'yuv420p',
|
|
||||||
'-vf', scale,
|
|
||||||
'-b:v', '1500k', // Force un bitrate vidéo de 1500 kbps (ajuste si nécessaire)
|
|
||||||
outputVideo
|
|
||||||
];
|
|
||||||
|
|
||||||
ffmpegProcess = spawn('ffmpeg', ffmpegArgs, {
|
|
||||||
stdio: ['ignore', 'ignore', 'pipe']
|
|
||||||
});
|
|
||||||
|
|
||||||
let lastUpdate = 0;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Écoute de la sortie d'erreur pour la progression
|
|
||||||
ffmpegProcess.stderr.on('data', (data) => {
|
|
||||||
const output = data.toString();
|
|
||||||
const frameMatch = output.match(/frame=\s*(\d+)/);
|
|
||||||
|
|
||||||
if (frameMatch) {
|
|
||||||
const currentFrame = parseInt(frameMatch[1], 10);
|
|
||||||
const progress = Math.min((currentFrame / totalFrames) * 100, 99.99);
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// Calcul de l'ETA
|
|
||||||
const elapsedSeconds = (now - startTime) / 1000;
|
|
||||||
const eta = elapsedSeconds / (currentFrame / totalFrames) - elapsedSeconds;
|
|
||||||
|
|
||||||
// Mise à jour max toutes les 500ms
|
|
||||||
if (now - lastUpdate > 500) {
|
|
||||||
let update_video = {
|
|
||||||
progress: progress,
|
|
||||||
eta: Math.round(eta),
|
|
||||||
updated_at: new Date()
|
|
||||||
}
|
|
||||||
database_manager.video.edit_video_by_id(videoId, update_video)
|
|
||||||
|
|
||||||
console.log('Progress:', progress.toFixed(2), '%, ETA:', eta.toFixed(0), 's');
|
|
||||||
lastUpdate = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attente de la fin du processus
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
ffmpegProcess.on('close', async (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
try {
|
|
||||||
// Mise à jour finale
|
|
||||||
// await db.query(`
|
|
||||||
// UPDATE public.videos
|
|
||||||
// SET
|
|
||||||
// status = 1,
|
|
||||||
// progress = 100,
|
|
||||||
// eta = 0,
|
|
||||||
// video_file = $1,
|
|
||||||
// updated_at = NOW()
|
|
||||||
// WHERE id = $2
|
|
||||||
// `, [outputVideo, videoId]);
|
|
||||||
let latest_update = {
|
|
||||||
status: 1,
|
|
||||||
progress: 100,
|
|
||||||
eta: 0,
|
|
||||||
video_file: outputVideo,
|
|
||||||
updated_at: new Date()
|
|
||||||
}
|
|
||||||
await database_manager.video.edit_video_by_id(videoId, latest_update)
|
|
||||||
resolve();
|
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error(`FFmpeg process exited with code ${code}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ffmpegProcess.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
return outputVideo;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Gestion des erreurs
|
|
||||||
console.error('Error in video creation:', error);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Mise à jour de la base de données en cas d'erreur
|
|
||||||
let error_video = {
|
|
||||||
status: 0,
|
|
||||||
progress: 0,
|
|
||||||
eta: null,
|
|
||||||
updated_at: new Date()
|
|
||||||
}
|
|
||||||
await database_manager.video.edit_video_by_id(videoId, error_video)
|
|
||||||
} catch (dbError) {
|
|
||||||
console.error('Database update error:', dbError);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Nettoyage
|
|
||||||
if (!cleanupDone) {
|
|
||||||
if (tempFile && fs.existsSync(tempFile)) {
|
|
||||||
fs.unlinkSync(tempFile);
|
|
||||||
}
|
|
||||||
if (ffmpegProcess) {
|
|
||||||
ffmpegProcess.kill();
|
|
||||||
}
|
|
||||||
cleanupDone = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createVideoWithList };
|
module.exports = { createVideoWithList };
|
||||||
|
|||||||
Reference in New Issue
Block a user