50 Commits

Author SHA1 Message Date
3df02703e8 feat(image): Mettre à jour la description de l'API pour détecter automatiquement le projet actif lors de l'enregistrement d'une image
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 16:15:01 +02:00
e6fd5b3a87 feat(image): Supprimer la nécessité de l'ID du projet lors du téléchargement d'une image, en utilisant le projet actif
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 15:06:54 +02:00
3d65ccb7fc fix(routes): Mettre à jour les descriptions des statuts des projets et des vidéos pour plus de clarté
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 14:51:40 +02:00
aa7f901442 feat(camera): Ajouter un log pour afficher l'identifiant du projet lors du démarrage de la procédure
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 12:41:34 +02:00
8480686fd4 refactor(video): Mettre à jour les statuts vidéo pour utiliser la nouvelle configuration des statuts
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 12:26:34 +02:00
98128253d9 feat(camera): Améliorer la gestion de l'arrêt de la caméra en ajoutant la recherche de projets en cours d'arrêt et en mettant à jour les statuts appropriés
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:54:29 +02:00
11c8951b6f fix(camera): Renommer la propriété 'active' en 'idle' dans les paramètres de la caméra et mettre à jour la documentation des routes de projet
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:47:58 +02:00
4427e6dde0 feat(camera): Ajouter le statut 'stopping' pour gérer l'arrêt des projets et mettre à jour la documentation des routes
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:41:58 +02:00
2533eacf5e fix(camera): Mettre à jour le statut du projet en 'idle' lors de l'arrêt de la caméra
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:36:15 +02:00
98bb822673 fix(camera): Renommer la propriété 'active' en 'idle' pour clarifier l'état de la caméra
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:34:39 +02:00
fde6a0454c feat(status): Refactor project and video status management with centralized configuration and update related controllers and routes
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:21:36 +02:00
65fa693986 feat(api): Améliorer la documentation Swagger pour les routes de l'API, ajout de descriptions détaillées et de schémas pour les entités Project, Measurement, Video, Camera, et Error.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 01:20:27 +02:00
1890051a0f Remove deprecated routes and functionalities related to camera, image, measurement, project, upload, and video management. This includes the deletion of route handlers and associated logic to streamline the codebase and improve maintainability.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 01:11:47 +02:00
d8b2cf63a3 fix(Dockerfile): Corriger la commande de démarrage pour utiliser server.js au lieu de backend.config.js
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 01:09:34 +02:00
4513af3aa0 feat(database): Implement DatabaseManager for managing database structure and initialization
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
2025-04-27 01:02:33 +02:00
792bdca965 Remplacer 'git checkout' par 'git reset --hard' pour une mise à jour plus précise du dépôt.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 00:11:07 +02:00
d55180e048 Améliorer la mise à jour du dépôt en remplaçant 'git pull' par 'git fetch' et 'git checkout' pour une gestion plus précise des branches.
Some checks failed
SSH Backend Deploy / ssh-deploy (push) Failing after 2s
2025-04-27 00:09:27 +02:00
32094d702b Mettre à jour la documentation Swagger pour l'upload de mesures avec image, en précisant les paramètres et les réponses.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-26 19:23:54 +02:00
83ac64262a Ajouter la documentation Swagger pour l'upload de mesures avec image
Some checks failed
SSH Backend Deploy / ssh-deploy (push) Failing after 0s
2025-04-26 18:59:24 +02:00
fd92aa067e Renommer le workflow en 'SSH Backend Deploy' et mettre à jour le script de déploiement pour exécuter './deploy.sh' au lieu d'un message d'accueil.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-24 00:27:36 +02:00
19bfde36a7 Ajouter des espaces pour améliorer la lisibilité dans la fonction de nettoyage du script de déploiement
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:23:34 +02:00
0277975cee Corriger la commande de démarrage des conteneurs Docker dans le script de déploiement pour utiliser 'docker compose' au lieu de 'docker-compose'.
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:22:33 +02:00
e7cb4582b0 Améliorer le script de déploiement en ajoutant des commandes pour construire et démarrer les conteneurs Docker avec gestion des erreurs.
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:21:12 +02:00
f5d73c5c3f Ajouter un script de déploiement avec gestion des erreurs et journalisation
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:20:11 +02:00
66dd0e0835 Mettre à jour les ports et les volumes dans docker-compose.yml pour une configuration locale
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:01:36 +02:00
9aedbdd127 Ajouter les informations d'identification du système Portainer dans le fichier stuff.md
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:59:13 +02:00
cb97bfb718 Supprimer le fichier server_local.js et ses dépendances
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:48:33 +02:00
2ce3eafb79 Refactor le workflow de déploiement et simplifie la connexion à la base de données PostgreSQL
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:47:36 +02:00
166bd53beb Actualiser db.js 2025-04-08 11:57:03 +00:00
1deb11d6aa revert b7715df51c
revert Actualiser db.js
2025-04-08 11:56:06 +00:00
b7715df51c Actualiser db.js 2025-04-08 11:44:50 +00:00
f3c8176733 Renommage du job de déploiement dans le workflow pour une meilleure clarté 2025-04-03 16:13:00 +02:00
4b6382cc98 Suppression des dépendances inutilisées dans le système de capture 2025-04-03 16:10:15 +02:00
45223bc670 Ajout de la gestion des projets dans les procédures de démarrage et d'arrêt de la caméra, incluant la vérification de l'état du projet en cours et la mise à jour du statut du projet. 2025-04-03 16:00:02 +02:00
70ec69ba84 Mise à jour des appels à la fonction edit_camera pour inclure l'identifiant de la caméra dans le système de capture 2025-04-03 15:41:04 +02:00
8961c366d3 Ajout de l'exportation du routeur dans le système de capture 2025-04-03 15:28:12 +02:00
09d756bf93 Ajout de la gestion du système de capture dans les routes de l'API 2025-04-03 15:26:50 +02:00
55cba1f3ea Ajout de la documentation Swagger pour les routes de gestion de la caméra, y compris l'état, le démarrage, l'arrêt et le mode de maintenance. 2025-04-03 15:25:49 +02:00
eea117bc70 Ajout de la gestion du système de capture, y compris l'initialisation de la caméra, les procédures de démarrage et d'arrêt, ainsi que la gestion des modes de maintenance. 2025-04-03 15:25:09 +02:00
7f1269bd2f Ajout de la gestion des paramètres de capture dans le système de caméra et mise à jour de la documentation associée. 2025-04-03 15:02:29 +02:00
e0fa309b21 Renommage de la fonction de suppression de projet pour une meilleure clarté 2025-04-03 14:50:39 +02:00
7d01ea28ce Suppression de la documentation Swagger pour l'ajout de mesures et conversion des routes de mesures en appels asynchrones. 2025-04-03 14:44:54 +02:00
265d1c5f18 Ajout de la gestion des routes pour le système de capture, mise à jour des chemins de fichiers et amélioration de la logique de gestion des mesures et vidéos. 2025-04-03 14:42:51 +02:00
cedd9949bd Correction de l'appel asynchrone dans la fonction get_path_from_id pour récupérer correctement le chemin à partir de l'ID du projet et de l'ordre. 2025-04-03 13:47:24 +02:00
f958e9d491 Ajout de logs pour le débogage dans la fonction get_path_from_id et suppression de logs redondants dans get_path_list 2025-04-03 13:45:08 +02:00
44d846b01c Ajout d'un log pour afficher la liste des chemins récupérés dans la fonction get_path_list 2025-04-03 13:41:25 +02:00
99fb5331ed Correction de la création de vidéos pour récupérer l'ID de la vidéo à partir de la réponse de la base de données. 2025-04-03 11:54:48 +02:00
4d1bfac99b Mise à jour de la route de création de vidéos pour utiliser le gestionnaire de base de données, ajout de la récupération des informations de vidéo et amélioration de la gestion des erreurs. 2025-04-03 11:52:19 +02:00
48b105be13 Refactor la gestion des vidéos en remplaçant le gestionnaire de vidéos par le gestionnaire de base de données. Ajout de la gestion des erreurs lors de la création et de la suppression des vidéos. 2025-04-03 11:45:36 +02:00
7b4a032249 Merge pull request 'Refactor des fonctions primaires' (#5) from refactor into main
Reviewed-on: https://gitea.kerboul.me/timelapse/timelapse-backend/pulls/5
2025-04-03 09:41:15 +00:00
46 changed files with 3633 additions and 2514 deletions

View File

@@ -1,45 +1,22 @@
name: SSH Backend Deploy
on: on:
push: push:
branches: branches:
- main # Déclenche l'action pour la branche principale - main
jobs: jobs:
ssh-connect: ssh-deploy:
runs-on: ubuntu-latest # Utilisation de l'image Ubuntu pour l'environnement de job runs-on: ubuntu-latest
steps: steps:
# Étape 1: Setup SSH - name: Write SSH Key
- name: Setup SSH and Add Private Key
run: | run: |
# Créez un dossier pour stocker les clés SSH echo "$SSH_PRIVATE_KEY" > id_rsa
mkdir -p ~/.ssh chmod 600 id_rsa
env:
SSH_PRIVATE_KEY: ${{ vars.SSH_PRIVATE_KEY }}
# Ajoutez la clé privée stockée dans le secret à un fichier id_rsa - name: Run SSH Deploy Script
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
# Protéger les permissions du fichier de la clé privée
chmod 600 ~/.ssh/id_rsa
# Ajoutez l'hôte distant à known_hosts pour éviter les erreurs de vérification de l'host
ssh-keyscan -H 192.168.1.87 >> ~/.ssh/known_hosts
# Vérifiez les permissions du fichier id_rsa (optionnel, juste pour être sûr)
ls -l ~/.ssh/id_rsa
# Étape 2: Test SSH Connection
- name: Test SSH connection
run: | run: |
# Testez la connexion SSH avec l'hôte distant ssh -i id_rsa -o StrictHostKeyChecking=no ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} "cd /root/timelapse-backend && ./deploy.sh"
ssh -v kerboul@192.168.1.87 "echo 'Connection successful!'"
# Étape 3: Ajouter une action qui utilise la connexion SSH
- name: Run remote command
run: |
# Exemple de commande distante exécutée sur le serveur distant via SSH
ssh kerboul@192.168.1.87 "cd /home/kerboul/scripts/timelapse && ./update_timelapse.sh"
# Étape 4: Nettoyage (optionnel)
- name: Clean up SSH keys
run: |
# Supprimer la clé privée pour des raisons de sécurité (optionnel)
rm -f ~/.ssh/id_rsa
rm -f ~/.ssh/known_hosts

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@ node_modules/
info.log info.log
storage/ storage/
uploads/ uploads/
package-lock.json package-lock.json
deploy.log

View File

@@ -21,4 +21,4 @@ COPY . .
EXPOSE 3000 EXPOSE 3000
# Commande pour démarrer l'application avec PM2 # Commande pour démarrer l'application avec PM2
CMD ["pm2-runtime", "start", "backend.config.js"] CMD ["pm2-runtime", "start", "server.js"]

86
README.md Normal file
View 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
View File

@@ -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 cameraRoutes = require('./routes/cameraRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const FileWatcher = require('./src/data/filewatcher');
const database_manager = require('./src/database/database_manager');
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('/', cameraRoutes);
module.exports = router; module.exports = router;

View File

@@ -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
View 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
`);
}

44
db.js
View File

@@ -1,39 +1,9 @@
const { Client } = require('pg'); /**
const devlock = require('./devlock.js'); * 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
*/
let dev = devlock.is_dev; const db = require('./src/database/connection');
console.log('[INFO] Environment:', dev ? 'Development Local' : 'Development Remote');
let client = new Client({ module.exports = db;
host: '192.168.192.3',
port: 5432,
user: 'timelapse',
password: 'timelapse',
database: 'timelapse'
});
if (dev) {
client = new Client({
host: 'mikoshi',
port: 54322,
user: 'timelapse',
password: 'timelapse',
database: 'timelapse_dev'
});
}
function init_database() {
console.log('[DB] Initialisation de la base de données PostgreSQL...');
client.connect(err => {
if (err) {
console.error('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;

53
deploy.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Set strict error handling
set -e # Exit immediately if a command exits with a non-zero status
set -u # Treat unset variables as an error
# Script variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Function for logging
log() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $1"
}
# Function for cleanup on exit
cleanup() {
log "Cleaning up..."
# Add cleanup tasks here
log "Cleanup completed - Deployment Completed."
}
# Register the cleanup function to be called on exit
trap cleanup EXIT
# Main function
main() {
log "Starting deployment..."
log "Updating repository to match remote main branch..."
git fetch origin
git reset --hard origin/main
if [ $? -ne 0 ]; then
log "Failed to update to the latest main branch."
exit 1
fi
log "Repository successfully updated to latest main branch."
# Lancer le docker
log "Building and starting Docker containers..."
docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d --build
if [ $? -ne 0 ]; then
log "Failed to start Docker containers."
exit 1
fi
log "Docker containers started successfully."
log "Deployment completed successfully."
}
# Execute main function
main "$@"

View File

@@ -1,5 +0,0 @@
let is_dev = false; // Set to true for development mode
module.exports = {
is_dev
}

View File

@@ -1,14 +1,14 @@
services: services:
timelapse-api: timelapse-api:
build: build:
context: . # Chemin vers le répertoire contenant le Dockerfile context: .
dockerfile: Dockerfile # Nom du Dockerfile, par défaut c'est "Dockerfile" dockerfile: Dockerfile
container_name: timelapse-api container_name: timelapse-api
ports: ports:
- "8053:3000" - "3000:3000"
volumes: volumes:
- /home/timelapse/backend:/backend - ./:/backend
- /home/timelapse/storage:/storage - ./storage:/storage
- node_modules:/backend/node_modules - node_modules:/backend/node_modules
environment: environment:
- NODE_VERSION=22.9.0 - NODE_VERSION=22.9.0
@@ -17,13 +17,24 @@ services:
restart: always restart: always
networks: networks:
- bridge - bridge
- timelapse_network timelapse-db:
image: postgres:14
container_name: timelapse-db
ports:
- "5432:5432"
volumes:
- /home/timelapse/db:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=timelapse
restart: always
networks:
- bridge
networks: networks:
bridge: bridge:
driver: bridge driver: bridge
timelapse_network:
driver: bridge
volumes: volumes:
node_modules: node_modules:

View File

@@ -1,700 +0,0 @@
// Routes SWAGGER - Documentation API
// Documentation: https://swagger.io/docs/specification/2-0/basic-structure/
// Les documentations sont volontairement séparées du code source pour des raisons de lisibilité et de maintenabilité.
/**
* @swagger
* /projects/{id}:
* delete:
* summary: Supprimer un projet par son ID
* description: Supprime un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Projet supprimé avec succès.
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects:
* get:
* summary: Récupérer tous les projets
* description: Récupère tous les projets disponibles.
* responses:
* 200:
* description: Une liste de projets.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}:
* get:
* summary: Récupérer un projet par ID
* description: Récupère un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Un projet.
* content:
* application/json:
* schema:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}/videos:
* get:
* summary: Récupérer les vidéos d'un projet par ID
* description: Récupère les vidéos associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}/measurements:
* get:
* summary: Récupérer les mesures d'un projet par ID
* description: Récupère les mesures associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects:
* post:
* summary: Ajouter un nouveau projet
* description: Ajoute un nouveau projet à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Projet ajouté avec succès.
* 400:
* description: Le nom et la description sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /smile:
* get:
* summary: Retrieve a smile image
* responses:
* 200:
* description: A smile image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /images/{projectId}/{orderId}:
* get:
* summary: Retrieve an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /images/{measurementId}:
* get:
* summary: Retrieve an image by measurement ID
* parameters:
* - in: path
* name: measurementId
* required: true
* schema:
* type: string
* description: The measurement ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /preview/{projectId}/{orderId}:
* get:
* summary: Retrieve a preview of an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: A resized preview of the image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
* 500:
* description: Internal Server Error
*/
/**
* @swagger
* /measurements:
* get:
* summary: Récupérer toutes les mesures
* description: Récupère toutes les mesures de la base de données.
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{id}:
* get:
* summary: Récupérer une mesure par ID
* description: Récupère une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* get:
* summary: Récupérer une mesure par project ID et order ID
* description: Récupère une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements:
* post:
* summary: Ajouter une nouvelle mesure
* description: Ajoute une nouvelle mesure à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* responses:
* 201:
* description: Mesure ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{id}:
* delete:
* summary: Supprimer une mesure par ID
* description: Supprime une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* delete:
* summary: Supprimer une mesure par project ID et order ID
* description: Supprime une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /uploadmeasurement:
* post:
* summary: Télécharger une mesure avec une image
* description: Télécharge une mesure avec une image pour un projet spécifique.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* timestamp:
* type: string
* format: date-time
* description: Horodatage de la mesure
* temperature:
* type: number
* description: Température mesurée
* humidity:
* type: number
* description: Humidité mesurée
* responses:
* 200:
* description: Mesure téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* id:
* type: integer
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos:
* get:
* summary: Récupérer toutes les vidéos
* description: Récupère toutes les vidéos de la base de données.
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/{id}:
* get:
* summary: Récupérer une vidéo par ID
* description: Récupère une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Une vidéo.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 400:
* description: ID de vidéo invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos:
* post:
* summary: Créer une nouvelle vidéo
* description: Crée une nouvelle vidéo avec les informations fournies.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* name:
* type: string
* resolution:
* type: string
* duration:
* type: number
* responses:
* 200:
* description: Vidéo créée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* video:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* name:
* type: string
* resolution:
* type: string
* duration:
* type: number
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/{id}:
* delete:
* summary: Supprimer une vidéo par ID
* description: Supprime une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* 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: Aucune vidéo trouvée avec cet ID.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/file/{video_id}:
* get:
* summary: Retrieve a video by video ID
* parameters:
* - in: path
* name: video_id
* required: true
* schema:
* type: string
* description: The video ID
* responses:
* 200:
* description: A video file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Video not found
* 400:
* description: Video not yet produced
*/
/**
* @swagger
* /cat:
* get:
* summary: Retrieve a cat video
* responses:
* 200:
* description: A cat video
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Video not found
*/

View File

@@ -1,359 +0,0 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const serverError = require('../utils/serverError');
const { start } = require('repl');
//const minInterval = 3; // Minutes
//const maxInterval = 60; // Minutes
var defaultCaptureInterval = 5; // minutes
var defaultMaintenance = 0;
var defaultActive = 0; // 0 = pas de capture, 1 = capture en cours
async function initCamera() {
const query = 'SELECT * FROM public.camera WHERE id = $1';
const values = [1];
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la vérification de l\'entrée caméra:', err);
return;
}
if (result.rows.length === 0) {
const insertQuery = `
INSERT INTO public.camera (id, interval, maintenance, active)
VALUES ($1, $2, $3, $4)
`;
const insertValues = [1, defaultCaptureInterval, defaultMaintenance, defaultActive];
db.query(insertQuery, insertValues, (err) => {
if (err) {
console.error('Erreur lors de l\'initialisation de la caméra:', err);
} else {
console.log('Caméra initialisée avec les valeurs par défaut.');
}
});
} else {
console.log('L\'entrée caméra avec l\'ID 1 existe déjà. Aucune initialisation nécessaire.');
}
});
}
async function getCamera() {
// retourner l'état de la caméra
const query = 'SELECT * FROM public.camera WHERE id = $1';
const values = [1];
try {
const result = await db.query(query, values);
if (result.rows.length === 0) {
console.log('Aucune entrée caméra trouvée.');
return null;
} else {
const camera = result.rows[0];
console.log('État de la caméra récupéré avec succès:', camera);
return {
captureInterval: camera.interval,
captureProjectID: camera.active,
captureStatus: camera.active,
maintenance: camera.maintenance
};
}
} catch (err) {
console.error('Erreur lors de la récupération de l\'état de la caméra:', err);
throw err;
}
}
async function printCameraStatus() {
let camera = await getCamera();
console.log('Statut de la caméra:');
console.log('Intervalle de capture:', camera.captureInterval, 'minutes');
console.log('Maintenance:', camera.maintenance === 1 ? 'En cours' : 'Aucune');
console.log('Statut de la capture:', camera.active === 1 ? 'En cours' : 'Arrêté');
console.log('-----------------------------------');
}
async function isCameraOccupied() {
try {
const query = 'SELECT id FROM public.projects WHERE status = $1 LIMIT 1';
const values = [1];
return new Promise((resolve, reject) => {
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la vérification de l\'occupation de la caméra:', err);
reject(err);
} else {
const isOccupied = result.rows.length > 0 ? result.rows[0].id : null;
resolve(isOccupied);
}
});
});
} catch (err) {
console.error('Erreur inattendue lors de la vérification de l\'occupation de la caméra:', err);
}
}
async function getCurrentProject() {
try {
const query = 'SELECT * FROM public.projects WHERE status = $1 LIMIT 1';
const values = [1];
return new Promise((resolve, reject) => {
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la récupération du projet en cours:', err);
reject(err);
} else if (result.rows.length === 0) {
console.log('Aucun projet en cours trouvé.');
resolve(null);
} else {
const currentProject = result.rows[0];
console.log('Projet en cours récupéré avec succès:', currentProject);
resolve(currentProject);
}
});
});
} catch (err) {
console.error('Erreur inattendue lors de la récupération du projet en cours:', err);
}
}
async function resetProjectStatus() {
const query = 'UPDATE public.projects SET status = $1 WHERE status = $2';
const values = [2, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la réinitialisation du statut des projets:', err);
} else {
console.log('Statut des projets réinitialisé avec succès.');
}
});
}
async function activateCamera() {
const query = 'UPDATE public.camera SET active = $1 WHERE id = $2';
const values = [1, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de l\'activation de la caméra:', err);
} else {
console.log('Caméra activée avec succès.');
}
});
}
async function deactivateCamera() {
const query = 'UPDATE public.camera SET active = $1 WHERE id = $2';
const values = [0, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la désactivation de la caméra:', err);
} else {
console.log('Caméra désactivée avec succès.');
}
});
}
async function changeProjectStatus(projectId, status) {
try {
const query = 'UPDATE public.projects SET status = $1 WHERE id = $2';
const values = [status, projectId];
await db.query(query, values);
console.log(`Statut du projet ID ${projectId} modifié avec succès à ${status}.`);
} catch (err) {
console.error('Une erreur inattendue s\'est produite lors de la modification du statut du projet:', err);
}
}
async function startup() {
await initCamera();
await printCameraStatus();
}
startup()
.catch(err => {
console.error('Erreur lors de l\'initialisation de la caméra:', err);
});
/**
* @swagger
* /camera/status:
* get:
* summary: Get the current status of the camera
* tags:
* - Camera
* responses:
* 200:
* description: Successfully retrieved the camera status
* content:
* application/json:
* schema:
* type: object
* properties:
* captureInterval:
* type: integer
* description: Capture interval in minutes
* captureProjectID:
* type: integer
* description: ID of the project currently being captured
* captureStatus:
* type: integer
* description: Capture status (0 = stopped, 1 = ongoing)
* maintenance:
* type: integer
* description: Maintenance status (0 = none, 1 = ongoing)
* 500:
* description: Internal server error
*/
router.get('/camera/status', async (req, res) => {
try {
const cameraStatus = await getCamera();
res.status(200).json(cameraStatus);
} catch (err) {
serverError.sendError('Erreur lors de la récupération de l\'état de la caméra:', res, err, 500);
}
});
async function setCameraSettings(interval, maintenance) {
try {
const query = `
UPDATE public.camera
SET interval = $1, maintenance = $2
WHERE id = $3
`;
const values = [interval, maintenance, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la mise à jour des paramètres de la caméra:', err);
} else {
console.log('Paramètres de la caméra mis à jour avec succès.');
//captureInterval = interval;
//maintenance = maintenance;
}
});
} catch (err) {
console.error('Une erreur inattendue s\'est produite lors de la mise à jour des paramètres de la caméra:', err);
}
}
async function startProcedure(projectId, interval, maintenance) {
if (isNaN(projectId) || isNaN(interval) || isNaN(maintenance)) {
return { error: 'Invalid parameters' };
}
const cameraOccupied = await isCameraOccupied();
if (cameraOccupied) {
return { error: 'Camera is occupied by another project' };
} else {
await activateCamera();
await setCameraSettings(interval, maintenance);
await changeProjectStatus(projectId, 1); // changer le statut du projet en cours à 1 (en cours)
console.log('Procédure de capture démarrée avec succès.');
return { message: 'Capture procedure started successfully' };
}
}
async function stopProcedure() {
var project = await getCurrentProject();
console.log(project);
if (project) {
await resetProjectStatus(); // réinitialiser le statut du projet en cours
await deactivateCamera(); // désactiver la caméra
await changeProjectStatus(project.id, 2); // changer le statut du projet en cours à 2 (terminé)
console.log('Procédure de capture arrêtée avec succès.');
return { message: 'Capture procedure stopped successfully' };
} else {
return { error: 'No project is currently being captured' };
}
}
/**
* @swagger
* /procedure/start/:
* post:
* summary: Start the capture procedure
* tags:
* - Procedure
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* projectId:
* type: integer
* description: ID of the project to start capturing
* interval:
* type: integer
* description: Capture interval in minutes
* maintenance:
* type: integer
* description: Maintenance status (0 = none, 1 = ongoing)
* responses:
* 200:
* description: Successfully started the capture procedure
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* description: Success message
* error:
* type: string
* description: Error message, if any
* 500:
* description: Internal server error
* /procedure/stop/:
* post:
* summary: Stop the capture procedure
* tags:
* - Procedure
* responses:
* 200:
* description: Successfully stopped the capture procedure
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* description: Success message
* error:
* type: string
* description: Error message, if any
* 500:
* description: Internal server error
*/
router.post('/procedure/start/', async (req, res) => {
const { projectId, interval, maintenance } = req.body;
try {
const result = await startProcedure(projectId, interval, maintenance);
res.status(200).json(result);
} catch (err) {
serverError.sendError('Erreur lors du démarrage de la procédure de capture:', res, err, 500);
}
});
router.post('/procedure/stop/', async (req, res) => {
try {
const result = await stopProcedure();
res.status(200).json(result);
} catch (err) {
serverError.sendError('Erreur lors de l\'arrêt de la procédure de capture:', res, err, 500);
}
});
module.exports = router;

View File

@@ -1,115 +0,0 @@
const express = require('express');
const router = express.Router();
const sharp = require('sharp');
const fs = require('fs');
const dbTester = require('../test/tester');
const db = require('../db');
const serverError = require('../utils/serverError');
router.get('/smile', (req, res) => {
const imagePath = dbTester.getSmileImage();
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.sendFile(imagePath);
});
});
router.get('/images/:projectId/:orderId', (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
db.query(query, [projectId, orderId], (err, results) => {
if (err) {
return serverError.sendError('Error getting image:', res, err, 500);
}
if (results.rows.length === 0) {
return res.status(404).json({ error: 'Image not found' });
}
const imagePath = results.rows[0].path;
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.download(imagePath);
});
});
});
router.get('/images/:measurementId', (req, res) => {
const measurementId = req.params.measurementId;
const query = 'SELECT path FROM public.measurements WHERE id = $1';
db.query(query, [measurementId], (err, results) => {
if (err) {
return serverError.sendError('Error getting image:', res, err, 500);
}
if (results.rows.length === 0) {
return res.status(404).json({ error: 'Image not found' });
}
const imagePath = results.rows[0].path;
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.download(imagePath);
});
});
});
const getImagePath = async (projectId, orderId) => {
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const result = await db.query(query, [projectId, orderId]);
if (result.rows.length === 0) {
throw new Error('Image not found');
}
return result.rows[0].path;
};
const checkImageExists = (imagePath) => {
return new Promise((resolve, reject) => {
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
reject(new Error('Image not found'));
} else {
resolve();
}
});
});
};
const resizeImage = async (imagePath) => {
const metadata = await sharp(imagePath).metadata();
const width = Math.floor(metadata.width / 7);
const height = Math.floor(metadata.height / 7);
return sharp(imagePath)
.resize(width, height)
.jpeg({ quality: 65 })
.toBuffer();
};
router.get('/preview/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
try {
const imagePath = await getImagePath(projectId, orderId);
await checkImageExists(imagePath);
const resizedImage = await resizeImage(imagePath);
res.set('Content-Type', 'image/jpeg');
res.send(resizedImage);
} catch (err) {
console.error('Error getting image preview:', err);
if (err.message === 'Image not found') {
return res.status(404).json({ error: 'Image not found' });
}
return res.status(500).json({ error: 'Internal Server Error' });
}
});
module.exports = router;

View File

@@ -1,54 +0,0 @@
const express = require('express');
const router = express.Router();
const database_manager = require('../src/database/database_manager');
router.get('/measurements', (req, res) => {
const measurements = database_manager.measurement.get_all_measurements();
if (!measurements) {
return res.status(404).json({ error: 'No measurements found' });
}
res.json(measurements);
});
router.get('/measurements/:id', (req, res) => {
const measurement = database_manager.measurement.get_measurement_by_id(req.params.id);
if (!measurement) {
return res.status(404).json({ error: 'Measurement not found' });
}
res.json(measurement);
});
router.get('/measurements/:projectId/:orderId', async (req, res) => {
const measurement = await database_manager.measurement.get_measurement_by_project_and_order_id(req.params.projectId, req.params.orderId);
if (!measurement) {
return res.status(404).json({ error: 'Measurement not found' });
}
res.json(measurement);
});
router.post('/measurements', (req, res) => {
const { projectId, timestamp, imagePath, temperature, humidity, orderId } = req.body;
if (!projectId || !timestamp || !imagePath || !temperature || !humidity || !orderId) {
return res.status(400).json({ error: 'All fields are required' });
}
const measurement = database_manager.measurement.add_measurement(projectId, timestamp, imagePath, temperature, humidity, orderId);
res.status(201).json(measurement);
});
router.delete('/measurements/:id', async (req, res) => {
const measurement = await database_manager.measurement.delete_measurement_by_id(req.params.id);
if (!measurement) {
return res.status(404).json({ error: 'Measurement not found' });
}
res.json({ message: 'Measurement deleted successfully', id: measurement.id });
});
router.delete('/measurements/:projectId/:orderId', async (req, res) => {
const measurement = await database_manager.measurement.delete_measurement_by_project_and_order_id(req.params.projectId, req.params.orderId);
if (!measurement) {
return res.status(404).json({ error: 'Measurement not found' });
}
res.json({ message: 'Measurement deleted successfully', id: measurement.id });
});
module.exports = router;

View File

@@ -1,91 +0,0 @@
const express = require('express');
const router = express.Router();
const serverError = require('../utils/serverError');
const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
router.get('/projects', async (req, res) => {
try {
const projects = await database_manager.project.get_all_projects();
res.json(projects);
} catch (error) {
serverError.sendError('Error getting all projects:', res, error, 500);
}
});
router.get('/projects/:id', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const project = await database_manager.project.get_project_by_id(projectId);
res.json(project);
} catch (error) {
serverError.sendError('Error getting project by ID:', res, error, 500);
}
});
router.get('/projects/:id/videos', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const videos = await database_manager.video.get_videos_by_project_id(projectId);
if (videos.length === 0) {
return res.status(404).json({ error: 'No videos found for this project' });
}
res.json(videos);
} catch (error) {
serverError.sendError('Error getting videos by project ID:', res, error, 500);
}
});
router.get('/projects/:id/measurements', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const measurements = await database_manager.measurement.get_measurements_by_project_id(projectId);
if (measurements.length === 0) {
return res.status(404).json({ error: 'No measurements found for this project' });
}
res.json(measurements);
} catch (error) {
serverError.sendError('Error getting measurements by project ID:', res, error, 500);
}
});
router.post('/projects', async (req, res) => {
const { name, description } = req.body;
if (!name || !description) {
return res.status(400).json({ error: 'Name and description are required' });
}
try {
const date = new Date();
const default_status = 0;
const project = await database_manager.project.create_project(name, description, date, default_status);
storage_manager.project.createProjectDirectory(project.id);
res.status(201).json({ message: 'Project added successfully', id: project.id });
} catch (error) {
serverError.sendError('Error creating project:', res, error, 500);
}
});
router.delete('/projects/:id', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
storage_manager.project.deleteProjectDirectory(projectId);
await database_manager.project.delete_project(projectId);
res.status(200).json({ message: 'Project deleted successfully', id: projectId });
} catch (error) {
serverError.sendError('Error deleting project:', res, error, 500);
}
});
module.exports = router;

View File

@@ -1,48 +0,0 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
const serverError = require('../utils/serverError');
const upload = multer({ storage: multer.memoryStorage() });
router.post('/uploadmeasurement', upload.single('image'), async (req, res) => {
//afficher le body de la requête
console.log(req.body);
const { projectId, timestamp, temperature, humidity } = req.body;
const image = req.file;
if (!image || !projectId || !timestamp || !temperature || !humidity) {
return res.status(400).json({ error: 'All fields are required' });
}
try {
const nextOrderId = await database_manager.measurement.get_next_order_id(projectId);
if (nextOrderId === null) {
return res.status(404).json({ error: 'Project not found' });
}
// Log types for debugging
console.log('Types:', {
image: typeof image,
projectId: typeof projectId,
nextOrderId: typeof nextOrderId
});
const imagePath = await storage_manager.measurement.upload_measurement_image(image, projectId, nextOrderId);
if (!imagePath) {
return res.status(500).json({ error: 'Failed to upload image' });
}
const measurement = await database_manager.measurement.add_measurement(projectId, timestamp, imagePath, temperature, humidity, nextOrderId);
if (!measurement) {
return res.status(500).json({ error: 'Failed to add measurement' });
}
res.json({ message: 'Measurement uploaded successfully', path: imagePath, id: measurement.id });
} catch (error) {
serverError.sendError('Error uploading measurement:', res, error, 500);
}
});
module.exports = router;

View File

@@ -1,256 +0,0 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const fs = require('fs');
const rangeParser = require('range-parser');
const serverError = require('../utils/serverError');
const videoManager = require('../src/video/videoManager');
const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
const dbTester = require('../test/tester');
router.get('/videos', (req, res) => {
const query = 'SELECT * FROM public.videos';
db.query(query, (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération des vidéos:', res, err, 500);
}
res.json(results.rows);
});
});
router.get('/videos/:id', (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return res.status(400).json({ error: 'Invalid video ID' });
}
const query = 'SELECT * FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération de la vidéo:', res, err, 500);
}
res.json(results.rows);
});
});
router.post('/videos', async (req, res) => {
const { project_id, measurement_ids, name, resolution, duration } = req.body;
console.log('Creating video:', req.body);
if (!project_id || !measurement_ids || !name || !resolution || !duration) {
return res.status(400).json({ error: 'Tous les champs sont requis.' });
}
console.log('Creating video with measurements:', measurement_ids);
try {
const videoId = await videoManager.createVideoProject(project_id, measurement_ids, name, resolution, duration);
// Start rendering the video immediately after creation
const result = await db.query(
'SELECT measurement_ids, project_id, duration FROM public.videos WHERE id = $1',
[videoId]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Vidéo non trouvée' });
}
const { duration: videoDuration, measurement_ids: videoMeasurementIds, project_id: videoProjectId } = result.rows[0];
const pathList = await storage_manager.measurement.get_path_list(videoMeasurementIds, project_id);
if (!pathList || pathList.length === 0) {
return res.status(404).json({ error: 'Aucun chemin trouvé pour les mesures' });
}
// parser la résolution (ex: 1920x1080)
const [res_width, res_height] = resolution.split('x').map(Number);
if (isNaN(res_width) || isNaN(res_height)) {
return res.status(400).json({ error: 'Invalid resolution format. Use WIDTHxHEIGHT (e.g., 1920x1080)' });
}
// Start background processing
videoManager.createVideoWithList(videoProjectId, pathList, videoDuration, videoId, res_width, res_height)
.then(videoFile => {
console.log('Rendu vidéo terminé:', videoFile);
return database_manager.video.update_video_file_path_by_id(videoId, videoFile);
})
.catch(error => {
console.error('Échec du rendu vidéo:', error);
});
// Immediate response
res.json({
message: 'Vidéo créée avec succès et le rendu a démarré',
id: videoId
});
} catch (err) {
console.error('Erreur lors de la création de la vidéo:', err);
res.status(500).json({ error: 'Erreur lors de la création de la vidéo' });
}
});
router.delete('/videos/:id', (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return res.status(400).json({ error: 'Invalid video ID' });
}
const query = 'SELECT video_file FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => {
if (err) {
return serverError.sendError('Error getting video:', res, err, 500);
}
if (results.rows.length === 0) {
return res.status(404).json({ error: 'Video not found' });
}
const videoFile = results.rows[0].video_file;
console.log('Deleting video file:', videoFile);
if(videoFile==null){
console.log('No video file to delete');
videoManager.deleteVideoProject(videoId).then(() => {
res.json({ message: 'Vidéo supprimée avec succès' });
}).catch(err => {
console.error('Erreur lors de la suppression de la vidéo:', err);
res.status(500).json({ error: 'Erreur lors de la suppression de la vidéo' });
});
} else {
fs.unlink(videoFile, (err) => {
if (err) {
console.error('Error deleting video file:', err);
return res.status(500).json({ error: 'Error deleting video file' });
}
videoManager.deleteVideoProject(videoId).then(() => {
res.json({ message: 'Vidéo supprimée avec succès' });
}).catch(err => {
console.error('Erreur lors de la suppression de la vidéo:', err);
res.status(500).json({ error: 'Erreur lors de la suppression de la vidéo' });
});
});
}
});
});
router.get('/videos/file/:video_id', (req, res) => {
const videoId = req.params.video_id;
const query = 'SELECT video_file, status FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => {
if (err) {
console.error('Error getting video:', err);
return serveFallbackVideo(res);
}
if (results.rows.length === 0) {
console.error('Video not found');
return serveFallbackVideo(res);
}
const video = results.rows[0];
if (video.status === 0) {
return res.status(400).json({ error: 'Video not yet produced' });
}
let videoPath = video.video_file;
if (!videoPath) {
console.error('Video file path is null or undefined');
videoPath = dbTester.getCatVideo();
}
// Check if the video file exists
fs.access(videoPath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Video file not found:', err);
videoPath = dbTester.getCatVideo();
}
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);
}
});
});
});
function serveFallbackVideo() {
return dbTester.getCatVideo();
}
router.get('/videos/progress/:video_id', async (req, res) => {
try {
const result = await db.query(`
SELECT
progress,
EXTRACT(EPOCH FROM (NOW() - started_at)) as elapsed,
eta,
status
FROM public.videos
WHERE id = $1
`, [req.params.video_id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Vidéo non trouvée' });
}
const video = result.rows[0];
res.json({
progress: video.progress,
elapsed: video.elapsed,
eta: video.eta,
status: this.getStatusLabel(video.status)
});
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Erreur de récupération' });
}
});
// function getStatusLabel(status) {
// const statusMap = {
// 0: 'En attente',
// 1: 'Terminé',
// 2: 'Échec',
// 3: 'En cours'
// };
// return statusMap[status] || 'Inconnu';
// }
router.get('/cat', (_, res) => {
const videoPath = dbTester.getCatVideo();
fs.access(videoPath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Video not found:', err);
return res.status(404).json({ error: 'Video not found' });
}
res.download(videoPath);
});
});
module.exports = router;

View File

@@ -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`);
}); });

View File

@@ -1,68 +0,0 @@
const devlock = require('./devlock.js');
devlock.is_dev = true; // Set to true for development mode
// server.js version locale
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;
// Middleware pour gérer les requêtes JSON
app.use(express.json());
// Cors accès à tout
app.use(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
const apiRoutes = require('./api');
app.use('/api', apiRoutes);
// Swagger dependencies
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
// Configuration de Swagger
const swaggerOptions = {
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: ['./routes/*.js'], // Prend en compte tous les fichiers de routes pour générer la documentation
};
// Initialisation de swagger-jsdoc
const swaggerDocs = swaggerJsdoc(swaggerOptions);
// Route Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
// Route de base pour tester le serveur
app.get('/', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
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!');
});
// Démarrer le serveur
app.listen(port, () => {
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
console.log(`[SERVER] Swagger documentation disponible sur http://localhost:${port}/api-docs`);
});

182
src/config/index.js Normal file
View File

@@ -0,0 +1,182 @@
// 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 Timelapse',
version: '1.0.0',
description: 'Documentation de l\'API Timelapse pour la gestion des projets, mesures et vidéos',
contact: {
name: 'Support Timelapse',
email: 'support@timelapse.kerboul.me'
}
},
servers: [
{ url: 'https://timelapse.kerboul.me/api', description: 'Serveur de production' },
{ url: 'http://localhost:3000/api', description: 'Serveur de développement local' }
],
components: {
schemas: {
Project: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
description: { type: 'string' },
start_date: { type: 'string', format: 'date' },
status: { type: 'integer' }
}
},
Measurement: {
type: 'object',
properties: {
id: { type: 'integer' },
project_id: { type: 'integer' },
timestamp: { type: 'string', format: 'date-time' },
path: { type: 'string' },
temperature: { type: 'number' },
humidity: { type: 'number' },
order_id: { type: 'integer' }
}
},
Video: {
type: 'object',
properties: {
id: { type: 'integer' },
project_id: { type: 'integer' },
measurement_ids: { type: 'string' },
name: { type: 'string' },
resolution: { type: 'string' },
duration: { type: 'integer' },
status: { type: 'integer' },
progress: { type: 'number' },
video_file: { type: 'string', nullable: true },
started_at: { type: 'string', format: 'date-time', nullable: true },
updated_at: { type: 'string', format: 'date-time', nullable: true },
eta: { type: 'number', nullable: true }
}
},
Camera: {
type: 'object',
properties: {
id: { type: 'integer' },
interval: { type: 'integer', nullable: true },
maintenance: { type: 'integer' },
active: { type: 'integer' }
}
},
Error: {
type: 'object',
properties: {
message: { type: 'string' },
statusCode: { type: 'integer' },
details: { type: 'string', nullable: true },
timestamp: { type: 'string', format: 'date-time' }
}
}
},
responses: {
NotFound: {
description: 'La ressource demandée n\'a pas été trouvée',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
BadRequest: {
description: 'Requête invalide',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
ServerError: {
description: 'Erreur serveur',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
}
}
},
tags: [
{ name: 'Projets', description: 'Opérations relatives aux projets' },
{ name: 'Mesures', description: 'Opérations relatives aux mesures et images' },
{ name: 'Vidéos', description: 'Opérations relatives aux vidéos' },
{ name: 'Caméra', description: 'Opérations relatives au contrôle de la caméra' },
{ name: 'Images', description: 'Opérations relatives aux images' },
{ name: 'Système', description: 'Opérations relatives au système' }
],
},
apis: ['./src/routes/*.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
projectStatus: {
brouillon: 0,
capturing: 1,
idle: 2,
stopping: 3 // Projet en cours d'arrêt
},
// Statuts pour les vidéos
videoStatus: {
rendering: 0,
completed: 1,
error: 2
},
// Paramètres par défaut pour la caméra
camera: {
defaultSettings: {
id: 1,
interval: null,
nbImages: null,
maintenance: false,
stopFlag: false,
idle: true
}
}
};

View File

@@ -0,0 +1,187 @@
// 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;
console.log('project_id:', project_id);
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,
idle: false // idle = 1 (idle = 0)
};
await Camera.updateCamera(1, newSettings);
// Met à jour le statut du projet
await Project.updateProject(project_id, { status: config.projectStatus.capturing });
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 {
// Trouve le projet actuellement en cours de capture
const currentProject = await Project.findCurrentRenderingProject();
if (!currentProject) {
return sendError('Aucun projet en cours de capture trouvé', res, null, 404);
}
// Met à jour le statut du projet en cours d'arrêt
await Project.updateProjectStatus(currentProject.id, config.projectStatus.stopping);
// Marque le drapeau d'arrêt
await Camera.updateCamera(1, { stop_flag: true });
console.log(`[CAMERA] Arrêt de la caméra demandé pour le projet ${currentProject.id}, en attente de confirmation...`);
res.json({
message: 'Procédure d\'arrêt de la caméra initiée avec succès',
project_id: currentProject.id
});
} 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,
idle: true // idle = true
};
await Camera.updateCamera(1, newSettings);
// Recherche le projet en cours d'arrêt
const stoppingProject = await Project.findStoppingProject();
if (stoppingProject) {
// Mettre à jour le statut du projet en cours d'arrêt vers idle
await Project.updateProjectStatus(stoppingProject.id, config.projectStatus.idle);
console.log(`[CAMERA] Projet : ${stoppingProject.id} arrêté avec succès.`);
res.json({
message: 'Caméra arrêtée avec succès',
project_id: stoppingProject.id,
status: config.projectStatus.idle
});
} else {
// Vérifier s'il y a un projet en cours de capture qui n'aurait pas été marqué comme stopping
const currentProject = await Project.findCurrentRenderingProject();
if (currentProject) {
await Project.updateProjectStatus(currentProject.id, config.projectStatus.idle);
console.log(`[CAMERA] Projet : ${currentProject.id} arrêté (était en capture).`);
res.json({
message: 'Caméra arrêtée avec succès (projet était en capture)',
project_id: currentProject.id,
status: config.projectStatus.idle
});
} else {
console.log('[CAMERA] Aucun projet en cours d\'arrêt ou de capture trouvé.');
res.json({ message: 'Caméra arrêtée avec succès mais aucun projet à mettre à jour' });
}
}
console.log('[CAMERA] Caméra arrêtée.');
} 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;

View File

@@ -0,0 +1,183 @@
// 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
* Ne nécessite plus l'ID du projet, utilise automatiquement le projet actif
*/
static uploadImage = asyncHandler(async (req, res) => {
const { timestamp, temperature, humidity } = req.body;
const image = req.file;
if (!image || !timestamp || !temperature || !humidity) {
return sendError('Tous les champs sont requis', res, null, 400);
}
try {
// Récupération du projet actif (en cours de capture)
const Project = require('../models/Project');
const activeProject = await Project.findCurrentRenderingProject();
if (!activeProject) {
return sendError('Aucun projet actif en cours de capture', res, null, 400);
}
const projectId = activeProject.id;
// Obtention du prochain ordre ID
const nextOrderId = await Measurement.getNextOrderId(projectId);
// 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',
project_id: projectId,
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;

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

View 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.projectStatus.brouillon;
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;

View 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.videoStatus.rendering
);
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.videoStatus.rendering || video.status === config.videoStatus.rendering) {
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;

View File

@@ -38,7 +38,7 @@ async function checkAndRemoveInvalidEntries() {
// Run the check periodically // Run the check periodically
console.log('[INFO] Activation du FileWatcher pour surveiller les fichiers invalides...') console.log('[INFO] Activation du FileWatcher pour surveiller les fichiers invalides...')
setInterval(checkAndRemoveInvalidEntries, 10000); // Every 10 seconds setInterval(checkAndRemoveInvalidEntries, 1000); // Every 10 seconds
// Initial run // Initial run
checkAndRemoveInvalidEntries(); checkAndRemoveInvalidEntries();

View File

@@ -1,196 +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 PROJECTS_DIR = path.join('.'); * Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
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) {
let Buffer=Buffer.from(content, 'base64');
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 = `${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 = `${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 = `${projectId}`;
const imagePath = `${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 = `${projectId}`;
const imagePath = `${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 = database_manager.measurement.get_measurement_by_project_and_order_id(projectId, orderId);
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 = `${projectId}`;
const videoPath = `${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
};

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

View File

@@ -1,285 +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]);
})
}; };
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 camera = {
get_camera: handleDatabaseOperation(async () => {
const query = `SELECT * FROM camera;`;
return (await db.query(query)).rows;
}),
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]);
})
};
// Export des modules
module.exports = {
project,
measurement,
video,
camera
};

72
src/models/Camera.js Normal file
View 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, idle)
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
View 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;

109
src/models/Project.js Normal file
View File

@@ -0,0 +1,109 @@
// 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;
});
/**
* Met à jour le statut d'un projet
* @param {number} id - ID du projet
* @param {number} status - Nouveau statut du projet
* @returns {Promise<Object|null>} Projet mis à jour ou null si non trouvé
*/
static updateProjectStatus = wrapDatabaseOperation(async (id, status) => {
const query = `UPDATE projects SET status = $1 WHERE id = $2 RETURNING *;`;
const result = await db.query(query, [status, id]);
// Ajout du statut stopping pour les projets en cours d'arrêt
if (status === config.projectStatus.stopping) {
console.log(`[PROJECT] Projet ${id} mis à jour avec le statut 'stopping'.`);
}
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 = capturing)
* @returns {Promise<Object|null>} Projet en cours de rendu ou null
*/
static findCurrentRenderingProject = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM projects WHERE status = ${config.projectStatus.capturing};`;
const result = await db.query(query);
return result.rows[0] || null;
});
/**
* Trouve le projet en cours d'arrêt (status = stopping)
* @returns {Promise<Object|null>} Projet en cours d'arrêt ou null
*/
static findStoppingProject = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM projects WHERE status = ${config.projectStatus.stopping};`;
const result = await db.query(query);
return result.rows[0] || null;
});
}
module.exports = Project;

147
src/models/Video.js Normal file
View File

@@ -0,0 +1,147 @@
// 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.videoStatus.rendering
) => {
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;
});
/**
* Met à jour le statut d'une vidéo
* @param {number} id - ID de la vidéo
* @param {number} status - Nouveau statut de la vidéo
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
*/
static async updateVideoStatus(id, status) {
const query = `UPDATE videos SET status = $1 WHERE id = $2 RETURNING *;`;
const result = await db.query(query, [status, 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.videoStatus.rendering},
${config.videoStatus.error},
0
);
`;
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
View 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;

194
src/routes/cameraRoutes.js Normal file
View File

@@ -0,0 +1,194 @@
// src/routes/cameraRoutes.js
const express = require('express');
const router = express.Router();
const CameraController = require('../controllers/cameraController');
/**
* @swagger
* /camera/status:
* get:
* tags:
* - Caméra
* summary: Récupère le statut actuel de la caméra
* description: Retourne les paramètres et l'état actuel du système de caméra
* responses:
* 200:
* description: Statut de la caméra
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Camera'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/camera/status', CameraController.getCameraStatus);
/**
* @swagger
* /procedure/start:
* post:
* tags:
* - Caméra
* summary: Démarre une procédure de capture
* description: Démarre une nouvelle procédure de capture d'images à intervalle régulier
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* description: ID du projet pour lequel les images seront capturées
* interval:
* type: integer
* description: Intervalle entre deux captures (en secondes)
* example: 30
* nb_images:
* type: integer
* description: Nombre total d'images à capturer
* example: 48
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* required:
* - project_id
* - interval
* - nb_images
* responses:
* 200:
* description: Procédure démarrée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Procédure de capture démarrée avec succès
* settings:
* type: object
* properties:
* interval:
* type: integer
* example: 30
* nb_images:
* type: integer
* example: 48
* 400:
* $ref: '#/components/responses/BadRequest'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/procedure/start', CameraController.startProcedure);
/**
* @swagger
* /procedure/stop:
* post:
* tags:
* - Caméra
* summary: Initie l'arrêt d'une procédure de capture
* description: Indique au système de caméra d'arrêter la procédure de capture en cours dès que possible
* responses:
* 200:
* description: Procédure d'arrêt initiée
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Procédure d'arrêt de la caméra initiée avec succès
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/procedure/stop', CameraController.stopProcedure);
/**
* @swagger
* /camera/stop:
* post:
* tags:
* - Caméra
* summary: Confirme l'arrêt de la caméra
* description: Confirme que la caméra a bien été arrêtée (appelé par la caméra)
* responses:
* 200:
* description: Caméra arrêtée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Caméra arrêtée avec succès
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/camera/stop', CameraController.confirmStopProcedure);
/**
* @swagger
* /camera/maintenance:
* post:
* tags:
* - Caméra
* summary: Active le mode maintenance
* description: Place la caméra en mode maintenance pour empêcher les captures programmées
* responses:
* 200:
* description: Mode maintenance activé
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Caméra en mode maintenance
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/camera/maintenance', CameraController.activateMaintenance);
/**
* @swagger
* /camera/maintenance/deactivate:
* post:
* tags:
* - Caméra
* summary: Désactive le mode maintenance
* description: Désactive le mode maintenance et permet à la caméra de reprendre les captures programmées
* responses:
* 200:
* description: Mode maintenance désactivé
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Caméra sortie du mode maintenance
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/camera/maintenance/deactivate', CameraController.deactivateMaintenance);
module.exports = router;

215
src/routes/imageRoutes.js Normal file
View File

@@ -0,0 +1,215 @@
// 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:
* tags:
* - Images
* - Système
* summary: Récupère une image de test (smile)
* description: Endpoint de test qui retourne une image statique
* responses:
* 200:
* description: Image de test
* content:
* image/png:
* schema:
* type: string
* format: binary
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/smile', ImageController.getSmileImage);
/**
* @swagger
* /images/{projectId}/{orderId}:
* get:
* tags:
* - Images
* summary: Récupère une image par projet ID et ordre ID
* description: Télécharge l'image associée à un projet et un numéro d'ordre spécifique
* 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 séquentiel de l'image
* responses:
* 200:
* description: Image (fichier)
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/images/:projectId/:orderId', ImageController.getImageByProjectAndOrderId);
/**
* @swagger
* /images/{measurementId}:
* get:
* tags:
* - Images
* summary: Récupère une image par ID de mesure
* description: Télécharge l'image associée à une mesure spécifique
* parameters:
* - in: path
* name: measurementId
* required: true
* schema:
* type: integer
* description: ID de la mesure
* responses:
* 200:
* description: Image (fichier)
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/images/:measurementId', ImageController.getImageByMeasurementId);
/**
* @swagger
* /preview/{projectId}/{orderId}:
* get:
* tags:
* - Images
* summary: Récupère un aperçu d'une image
* description: Retourne une version redimensionnée (miniature) 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 séquentiel de l'image
* responses:
* 200:
* description: Aperçu d'image redimensionné
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/preview/:projectId/:orderId', ImageController.getImagePreview);
/**
* @swagger
* /camera/upload:
* post:
* tags:
* - Images
* - Caméra
* summary: Télécharge une image avec données de mesure
* description: Enregistre une nouvelle image capturée avec les données de mesure associées. L'API détecte automatiquement le projet actif en cours de capture.
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* timestamp:
* type: string
* format: date-time
* description: Horodatage de la capture
* temperature:
* type: number
* description: Température mesurée
* example: 22.5
* humidity:
* type: number
* description: Humidité mesurée
* example: 45.2
* required:
* - image
* - timestamp
* - temperature
* - humidity
* responses:
* 200:
* description: Image téléchargée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Mesure téléchargée avec succès
* project_id:
* type: integer
* example: 1
* description: ID du projet actif détecté automatiquement
* path:
* type: string
* example: /storage/1/images/42.jpg
* id:
* type: integer
* example: 123
* 400:
* description: Erreur de requête - paramètres manquants ou aucun projet actif
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: Aucun projet actif en cours de capture
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/camera/upload', upload.single('image'), ImageController.uploadImage);
module.exports = router;

34
src/routes/index.js Normal file
View 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;

View File

@@ -0,0 +1,136 @@
// src/routes/measurementRoutes.js
const express = require('express');
const router = express.Router();
const MeasurementController = require('../controllers/measurementController');
/**
* @swagger
* /measurements:
* get:
* tags:
* - Mesures
* summary: Récupère toutes les mesures
* description: Retourne la liste complète des mesures de tous les projets
* responses:
* 200:
* description: Liste de toutes les mesures
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Measurement'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/measurements', MeasurementController.getAllMeasurements);
/**
* @swagger
* /measurements/{id}:
* get:
* tags:
* - Mesures
* summary: Récupère une mesure par ID
* description: Retourne les détails d'une mesure spécifique
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID de la mesure
* responses:
* 200:
* description: Détails de la mesure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Measurement'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/measurements/:id', MeasurementController.getMeasurementById);
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* get:
* tags:
* - Mesures
* summary: Récupère une mesure par projet ID et ordre ID
* description: Retourne les détails d'une mesure en fonction du projet et de son numéro d'ordre
* 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 séquentiel de la mesure dans le projet
* responses:
* 200:
* description: Détails de la mesure
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Measurement'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/measurements/:projectId/:orderId', MeasurementController.getMeasurementByProjectAndOrderId);
/**
* @swagger
* /measurements/{id}:
* delete:
* tags:
* - Mesures
* summary: Supprime une mesure
* description: Supprime une mesure et l'image associée
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID de la mesure à supprimer
* responses:
* 200:
* description: Mesure supprimée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Mesure supprimée avec succès
* id:
* type: integer
* example: 123
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.delete('/measurements/:id', MeasurementController.deleteMeasurement);
module.exports = router;

211
src/routes/projectRoutes.js Normal file
View File

@@ -0,0 +1,211 @@
// src/routes/projectRoutes.js
const express = require('express');
const router = express.Router();
const ProjectController = require('../controllers/projectController');
/**
* @swagger
* /projects:
* get:
* tags:
* - Projets
* summary: Récupère tous les projets
* description: Retourne la liste complète des projets de timelapse
* responses:
* 200:
* description: Liste de tous les projets
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Project'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/projects', ProjectController.getAllProjects);
/**
* @swagger
* /projects/{id}:
* get:
* tags:
* - Projets
* summary: Récupère un projet par ID
* description: Retourne les détails d'un projet spécifique
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID du projet
* responses:
* 200:
* description: Détails du projet
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Project'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/projects/:id', ProjectController.getProjectById);
/**
* @swagger
* /projects/{id}/videos:
* get:
* tags:
* - Projets
* - Vidéos
* summary: Récupère les vidéos d'un projet
* description: Retourne toutes les vidéos associées à un projet spécifique
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID du projet
* responses:
* 200:
* description: Liste des vidéos du projet
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Video'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/projects/:id/videos', ProjectController.getProjectVideos);
/**
* @swagger
* /projects/{id}/measurements:
* get:
* tags:
* - Projets
* - Mesures
* summary: Récupère les mesures d'un projet
* description: Retourne toutes les mesures associées à un projet spécifique
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID du projet
* responses:
* 200:
* description: Liste des mesures du projet
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Measurement'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/projects/:id/measurements', ProjectController.getProjectMeasurements);
/**
* @swagger
* /projects:
* post:
* tags:
* - Projets
* summary: Crée un nouveau projet
* description: Crée un nouveau projet de timelapse et son répertoire de stockage
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: Nom du projet
* description:
* type: string
* description: Description détaillée du projet
* status:
* type: integer
* description: "Statut du projet: 0=brouillon, 1=capturing, 2=idle, 3=stopping"
* required:
* - name
* - description
* responses:
* 201:
* description: Projet créé avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Projet ajouté avec succès
* id:
* type: integer
* example: 42
* 400:
* $ref: '#/components/responses/BadRequest'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/projects', ProjectController.createProject);
/**
* @swagger
* /projects/{id}:
* delete:
* tags:
* - Projets
* summary: Supprime un projet
* description: Supprime un projet, toutes ses mesures, images et vidéos associées
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID du projet à supprimer
* responses:
* 200:
* description: Projet supprimé avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Projet supprimé avec succès
* id:
* type: integer
* example: 42
* 400:
* $ref: '#/components/responses/BadRequest'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.delete('/projects/:id', ProjectController.deleteProject);
module.exports = router;

233
src/routes/videoRoutes.js Normal file
View File

@@ -0,0 +1,233 @@
// src/routes/videoRoutes.js
const express = require('express');
const router = express.Router();
const VideoController = require('../controllers/videoController');
/**
* @swagger
* /videos:
* get:
* tags:
* - Vidéos
* summary: Récupère toutes les vidéos
* description: Retourne la liste complète des vidéos de tous les projets
* responses:
* 200:
* description: Liste de toutes les vidéos
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Video'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/videos', VideoController.getAllVideos);
/**
* @swagger
* /videos/{id}:
* get:
* tags:
* - Vidéos
* summary: Récupère une vidéo par ID
* description: Retourne les détails d'une vidéo spécifique
* 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
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Video'
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/videos/:id', VideoController.getVideoById);
/**
* @swagger
* /videos:
* post:
* tags:
* - Vidéos
* summary: Crée une nouvelle vidéo
* description: Crée une nouvelle vidéo à partir d'une liste de mesures et démarre le processus de rendu
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* description: ID du projet
* measurement_ids:
* type: string
* description: Tableau JSON d'IDs de mesures
* example: "[1,2,3,4,5]"
* name:
* type: string
* description: Nom de la vidéo
* resolution:
* type: string
* description: Résolution de la vidéo (format LARGEURxHAUTEUR)
* example: "1920x1080"
* duration:
* type: integer
* description: Durée souhaitée en secondes
* required:
* - project_id
* - measurement_ids
* - name
* - resolution
* - duration
* responses:
* 200:
* description: Vidéo créée avec succès et rendu démarré
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Vidéo créée avec succès et le rendu a démarré
* id:
* type: integer
* example: 42
* 400:
* $ref: '#/components/responses/BadRequest'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.post('/videos', VideoController.createVideo);
/**
* @swagger
* /videos/{id}:
* delete:
* tags:
* - Vidéos
* summary: Supprime une vidéo
* description: Supprime une vidéo et le fichier vidéo associé
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID de la vidéo à supprimer
* responses:
* 200:
* description: Vidéo supprimée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example: Vidéo supprimée avec succès
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.delete('/videos/:id', VideoController.deleteVideo);
/**
* @swagger
* /videos/file/{video_id}:
* get:
* tags:
* - Vidéos
* summary: Récupère le fichier vidéo
* description: Télécharge ou diffuse le fichier vidéo avec support du streaming HTTP
* parameters:
* - in: path
* name: video_id
* required: true
* schema:
* type: integer
* description: ID de la vidéo
* responses:
* 200:
* description: Fichier vidéo (stream)
* content:
* video/mp4:
* schema:
* type: string
* format: binary
* 206:
* description: Fichier vidéo partiel (range request)
* 400:
* $ref: '#/components/responses/BadRequest'
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/videos/file/:video_id', VideoController.getVideoFile);
/**
* @swagger
* /videos/progress/{video_id}:
* get:
* tags:
* - Vidéos
* summary: Récupère la progression du rendu d'une vidéo
* description: Donne des informations sur l'état actuel 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
* description: Pourcentage de progression (0-100)
* example: 45.2
* elapsed:
* type: number
* description: Temps écoulé depuis le début du rendu (secondes)
* example: 120
* eta:
* type: number
* description: Temps estimé restant (secondes)
* example: 150
* status:
* type: integer
* description: "Statut de la vidéo: 0=rendering, 1=completed, 2=error"
* example: 1
* 404:
* $ref: '#/components/responses/NotFound'
* 500:
* $ref: '#/components/responses/ServerError'
*/
router.get('/videos/progress/:video_id', VideoController.getVideoProgress);
module.exports = router;

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

View 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.videoStatus.rendering,
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.videoStatus.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.videoStatus.error,
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.videoStatus.rendering]: 'En cours',
[config.videoStatus.completed]: 'Terminé',
[config.videoStatus.error]: 'Échec',
0: 'En attente'
};
return statusMap[status] || 'Inconnu';
}
}
module.exports = VideoService;

58
src/utils/errorHandler.js Normal file
View 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
};

View File

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

View File

@@ -1,30 +1,31 @@
Routes : Workflow Caméra
- /projects = liste des projets Côté Caméra
- /projects/:id = détail d'un projet
- /projects/:id/edit = édition d'un projet
- /projects/:id/delete = suppression d'un projet
- /projects/new = création d'un projet
- /projects/:id/measurements = liste des mesures d'un projet
- /projects/:id/measurements/:id = détail d'une mesure
- /projects/:id/measurements/:id/edit = édition d'une mesure
- /projects/:id/measurements/:id/delete = suppression d'une mesure
- /projects/:id/videos = liste des vidéos d'un projet
- /projects/:id/videos/:id = détail d'une vidéo
/camera/status // récupérer le statut de la caméra (GET)
- /measurements = liste des mesures si stop :
- /measurements/:id = détail d'une mesure /camera/stop // arrêter la caméra (POST)
- /measurements/:id/edit = édition d'une mesure
- /measurements/:id/delete = suppression d'une mesure
- /measurements/new = création d'une mesure
- /cameras = liste des caméras si upload :
- /cameras/:id = détail d'une caméra /camera/upload // uploader la vidéo (POST)
- /cameras/:id/edit = édition d'une caméra
- /cameras/:id/delete = suppression d'une caméra
- /cameras/new = création d'une caméra
- /data/image/:id = image depuis le pool de stockage Côté Backend
- /data/video/:id = vidéo depuis le pool de stockage
- /procedure/start // démarrer une procédure (POST)
/procedure/stop // arrêter la procédure courante (POST) (doit attendre la confirmation de /camera/stop)
/procedure/delete // supprimer la procédure courante (POST) (doit attendre la confirmation de /camera/delete)
Modèle de données :
table camera (paramètres de la caméra et procédure courante)
id (int, PK) - Toujours 1
interval(int) - Intervalle de la caméra (en minutes), peut être null
nb_image(int) - Nombre d'images à prendre, peut être null
maintenance(bool) - Indique si la caméra est en mode maintenance ou non (true/false)
stop_flag(bool) - Indique si la caméra doit être arrêtée ou non (true/false)
idle(bool) - Indique si la caméra est inactive ou non (true/false)
MDP Portainer système :
user : timelapse
password : timelapse_kerboul