Compare commits
220 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3df02703e8 | |||
| e6fd5b3a87 | |||
| 3d65ccb7fc | |||
| aa7f901442 | |||
| 8480686fd4 | |||
| 98128253d9 | |||
| 11c8951b6f | |||
| 4427e6dde0 | |||
| 2533eacf5e | |||
| 98bb822673 | |||
| fde6a0454c | |||
| 65fa693986 | |||
| 1890051a0f | |||
| d8b2cf63a3 | |||
| 4513af3aa0 | |||
| 792bdca965 | |||
| d55180e048 | |||
| 32094d702b | |||
| 83ac64262a | |||
| fd92aa067e | |||
| 19bfde36a7 | |||
| 0277975cee | |||
| e7cb4582b0 | |||
| f5d73c5c3f | |||
| 66dd0e0835 | |||
| 9aedbdd127 | |||
| cb97bfb718 | |||
| 2ce3eafb79 | |||
| 166bd53beb | |||
| 1deb11d6aa | |||
| b7715df51c | |||
| f3c8176733 | |||
| 4b6382cc98 | |||
| 45223bc670 | |||
| 70ec69ba84 | |||
| 8961c366d3 | |||
| 09d756bf93 | |||
| 55cba1f3ea | |||
| eea117bc70 | |||
| 7f1269bd2f | |||
| e0fa309b21 | |||
| 7d01ea28ce | |||
| 265d1c5f18 | |||
| cedd9949bd | |||
| f958e9d491 | |||
| 44d846b01c | |||
| 99fb5331ed | |||
| 4d1bfac99b | |||
| 48b105be13 | |||
| 7b4a032249 | |||
| f5fda050ed | |||
| 401deb3e69 | |||
| 03ec179590 | |||
| 6077dfd716 | |||
| c3b2059428 | |||
| 0d0c101e20 | |||
| 915146c140 | |||
| 242bbcd597 | |||
| a33e517a8a | |||
| ed853ab0f7 | |||
| 12898d67c0 | |||
| 4642c8cca6 | |||
| daca488532 | |||
| 3d00f6afbf | |||
| 15692a3fc8 | |||
| dd03db42a9 | |||
| a0b1eaf109 | |||
| b65230d5e7 | |||
| 55b4c04187 | |||
| 61cdb25398 | |||
| c4d62c473e | |||
| 7dafdcecde | |||
| d1b75329ea | |||
| 90e036b150 | |||
| aa9a21c638 | |||
| 90ce92b90b | |||
| 647dd72b5b | |||
| 73922d8afc | |||
| 293245d457 | |||
| 368abfbeca | |||
| 38864a68d8 | |||
| 71cb9898bb | |||
| 9101497a7f | |||
| 6c48612554 | |||
| bb51208d06 | |||
| c2dcf3fa13 | |||
| eb47639397 | |||
| 582fd87f32 | |||
| 2c9f81975f | |||
| 5c7116af7a | |||
| c91d11567c | |||
| 5024859b6c | |||
| fda18fb1c6 | |||
| 7536d98330 | |||
| fb1bdbd182 | |||
| e745c78b25 | |||
| 8c35aab855 | |||
| 559ef44cb3 | |||
| 411ea7a904 | |||
| 7942a025e8 | |||
| 3849042869 | |||
| 6747062f0b | |||
| c93eed9d52 | |||
| 884e312ef7 | |||
| 5ffa1ec839 | |||
| df219bfc06 | |||
| c3e78b248f | |||
| a069acfce7 | |||
| c90ff42961 | |||
| 2e552be9db | |||
| 4bda54b529 | |||
| d93b2c6b7c | |||
| dab93cfdf9 | |||
| 62e8aee6bd | |||
| 6c77d267e6 | |||
| 9d9868e26b | |||
| 217f0b4fd3 | |||
| d8f1d353c6 | |||
| 30f05ffcbe | |||
| ef90f77a11 | |||
| e38718b1fa | |||
| f85cead1dd | |||
| 3469c757ec | |||
| 9ec8ff73f3 | |||
| 55697fc032 | |||
| 7baac5dcb7 | |||
| 848c50bf33 | |||
| 81c4470464 | |||
| 29f198cd85 | |||
| 3d560cfb77 | |||
| 553a934563 | |||
| 9e850f0090 | |||
| e9fd9dfaa1 | |||
| 8f69705ae9 | |||
| 5979cded02 | |||
| 37d82d1133 | |||
| a15ebb0697 | |||
| d17c96479f | |||
| 727c28d312 | |||
| d790626a1a | |||
| 9cd1b230fd | |||
| a6a2492842 | |||
| 65fcf1fc68 | |||
| 3bf001bb58 | |||
| 961b72b24b | |||
| a39bb6e6c0 | |||
| b696897cfc | |||
| 1457711d8f | |||
| e446724ecd | |||
| 557be4a58b | |||
| 0c56fd79bc | |||
| 44d1d6a24e | |||
| 8319ae9685 | |||
| 4807579846 | |||
| ac0bd807df | |||
| 39a7b897bf | |||
| 7785bfa10f | |||
| 23295f13d7 | |||
| fe884cb8e7 | |||
| 8ffde922fa | |||
| 6b95665974 | |||
| 6ecd573751 | |||
| f3ed511543 | |||
| 348509fddb | |||
| 98e74d22f2 | |||
| 51db325dad | |||
| 7c5041b5c4 | |||
| 2ee5897426 | |||
| 6178e7cdbf | |||
| a65fcf0c47 | |||
| a8494ad382 | |||
| dcbf2a1f00 | |||
| 2450359710 | |||
| 7c342c3b69 | |||
| 3f5317ad18 | |||
| 7652a1ea64 | |||
| 41c877f072 | |||
| f99b0c60ce | |||
| aa571e5149 | |||
| ef09fdb1b4 | |||
| e61f1e9773 | |||
| a63e79e26e | |||
| 269ad2283d | |||
| c17c939b9c | |||
| b2e14b169f | |||
| 27f06daaaf | |||
| 208b6d5b28 | |||
| eb63c84443 | |||
| 8b0de65272 | |||
| bc2159f5f9 | |||
| 1e59f5ead1 | |||
| f833f21b01 | |||
| 1e7ae35c8a | |||
| c0215643ea | |||
| 25c056c3d8 | |||
| 8b45c5feb8 | |||
| a09805c5f1 | |||
| 7179d94527 | |||
| b752595781 | |||
| 3b4d8a9e5a | |||
| 2766a1d788 | |||
| 78708e4eaa | |||
| 27ada11471 | |||
| 7aae1aaf34 | |||
| f9de2227dc | |||
| ed4a37e259 | |||
| 0c91f7d3c3 | |||
| afe3c163f1 | |||
| 0f31b5019f | |||
| 0a6fbb22bf | |||
| 6fedbe10c8 | |||
| cec3a10b2b | |||
| 2a24864003 | |||
| 6ee50ee7b4 | |||
| 158a288dec | |||
| cd1f91589b | |||
| 0600fb44c2 | |||
| 1f21c288ff | |||
| 152f4ee508 | |||
| c050a1744f |
@@ -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
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ info.log
|
|||||||
storage/
|
storage/
|
||||||
uploads/
|
uploads/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
deploy.log
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
stages:
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
deploy_timelapse:
|
|
||||||
stage: deploy
|
|
||||||
script:
|
|
||||||
- apt-get update && apt-get install -y openssh-client
|
|
||||||
- mkdir -p ~/.ssh
|
|
||||||
- chmod 700 ~/.ssh
|
|
||||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
|
||||||
- ssh-keyscan 172.17.0.1 >> ~/.ssh/known_hosts
|
|
||||||
- ssh kerboul@172.17.0.1 "cd /home/kerboul/scripts/timelapse && ./update_timelapse.sh"
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Utiliser une image de base officielle de Node.js
|
||||||
|
FROM node:latest
|
||||||
|
|
||||||
|
# Installer ffmpeg
|
||||||
|
RUN apt-get update && apt-get install -y ffmpeg
|
||||||
|
|
||||||
|
# Définir le répertoire de travail dans le conteneur
|
||||||
|
WORKDIR /backend
|
||||||
|
|
||||||
|
# Copier le fichier package.json et package-lock.json (si disponible)
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Installer les dépendances Node.js
|
||||||
|
RUN npm install
|
||||||
|
RUN npm install -g pm2
|
||||||
|
|
||||||
|
# Copier le reste de l'application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Exposer le port sur lequel l'application va tourner
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Commande pour démarrer l'application avec PM2
|
||||||
|
CMD ["pm2-runtime", "start", "server.js"]
|
||||||
86
README.md
Normal file
86
README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Timelapse Backend
|
||||||
|
|
||||||
|
Ce projet est une API de backend pour gérer une solution de timelapse avec capture, stockage et transformation d'images en vidéos.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
L'application a été refactorisée pour suivre un modèle MVC (Modèle-Vue-Contrôleur) et respecter le principe de séparation des préoccupations. Voici la structure du projet :
|
||||||
|
|
||||||
|
```
|
||||||
|
/src
|
||||||
|
/config - Configuration centralisée de l'application
|
||||||
|
/controllers - Logique de traitement des requêtes HTTP
|
||||||
|
/database - Connexion à la base de données
|
||||||
|
/data - Compatibilité avec l'ancienne version (à terme, à supprimer)
|
||||||
|
/middlewares - Middlewares Express
|
||||||
|
/models - Modèles de données et logique d'accès à la BDD
|
||||||
|
/routes - Définition des routes de l'API
|
||||||
|
/services - Services métier et logique complexe
|
||||||
|
/utils - Utilitaires partagés
|
||||||
|
/video - Compatibilité avec l'ancienne version (à terme, à supprimer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Points d'entrée
|
||||||
|
|
||||||
|
- `server.js` - Point d'entrée principal du serveur Express
|
||||||
|
- `api.js` - Routes principales de l'API
|
||||||
|
|
||||||
|
## Composants principaux
|
||||||
|
|
||||||
|
### Modèles
|
||||||
|
|
||||||
|
Les modèles encapsulent la logique d'accès aux données et les règles métier :
|
||||||
|
|
||||||
|
- `Project.js` - Gestion des projets de timelapse
|
||||||
|
- `Measurement.js` - Gestion des mesures et images
|
||||||
|
- `Video.js` - Gestion des vidéos générées
|
||||||
|
- `Camera.js` - Gestion de la caméra et des paramètres de capture
|
||||||
|
|
||||||
|
### Contrôleurs
|
||||||
|
|
||||||
|
Les contrôleurs gèrent le traitement des requêtes HTTP et interagissent avec les modèles et services :
|
||||||
|
|
||||||
|
- `projectController.js` - Gestion des projets
|
||||||
|
- `measurementController.js` - Gestion des mesures
|
||||||
|
- `videoController.js` - Gestion des vidéos
|
||||||
|
- `imageController.js` - Gestion des images et téléchargements
|
||||||
|
- `cameraController.js` - Gestion des paramètres de la caméra
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
Les services implémentent la logique métier complexe :
|
||||||
|
|
||||||
|
- `storageService.js` - Gestion du stockage des fichiers
|
||||||
|
- `videoService.js` - Service de création de vidéos à partir d'images
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
Les routes définissent les points d'accès HTTP de l'API :
|
||||||
|
|
||||||
|
- `projectRoutes.js` - Routes pour les projets
|
||||||
|
- `measurementRoutes.js` - Routes pour les mesures
|
||||||
|
- `videoRoutes.js` - Routes pour les vidéos
|
||||||
|
- `imageRoutes.js` - Routes pour les images
|
||||||
|
- `cameraRoutes.js` - Routes pour la caméra
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
Le déploiement est géré via Docker Compose, avec une configuration dans `docker-compose.yml`. Le script `deploy.sh` gère le déploiement automatisé.
|
||||||
|
|
||||||
|
## Phase de Transition
|
||||||
|
|
||||||
|
L'application est actuellement en phase de transition de l'ancienne architecture vers la nouvelle. Les fichiers suivants sont des ponts de compatibilité qui seront progressivement supprimés :
|
||||||
|
|
||||||
|
- `src/database/database_manager.js` - Redirige vers les nouveaux modèles
|
||||||
|
- `src/data/storage_manager.js` - Redirige vers le nouveau service de stockage
|
||||||
|
- `src/video/videoManager.js` - Redirige vers le nouveau service vidéo
|
||||||
|
|
||||||
|
Pour supprimer les fichiers obsolètes une fois toutes les références mises à jour, utilisez :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node cleanup.js delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation API
|
||||||
|
|
||||||
|
La documentation de l'API est disponible via Swagger à l'adresse `/api-docs`.
|
||||||
22
api.js
22
api.js
@@ -1,24 +1,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const cors = require('cors');
|
|
||||||
const projectRoutes = require('./routes/projectRoutes');
|
|
||||||
const measurementRoutes = require('./routes/measurementRoutes');
|
|
||||||
const videoRoutes = require('./routes/videoRoutes');
|
|
||||||
const imageRoutes = require('./routes/imageRoutes');
|
|
||||||
const uploadRoutes = require('./routes/uploadRoutes');
|
|
||||||
const fileWatcher = require('./src/data/filewatcher.js');
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
9
backend.config.js
Normal file
9
backend.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Ce fichier de configuration est maintenu pour compatibilité
|
||||||
|
* mais redirige vers notre nouvelle architecture centralisée.
|
||||||
|
* À terme, toutes les références à ce fichier devraient être remplacées
|
||||||
|
* par des importations directes de src/config/index.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const config = require('./src/config');
|
||||||
|
module.exports = config;
|
||||||
77
cleanup.js
Normal file
77
cleanup.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Script de nettoyage pour supprimer les fichiers obsolètes après refactoring
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Pour lister les fichiers obsolètes sans les supprimer : node cleanup.js list
|
||||||
|
* - Pour supprimer les fichiers obsolètes : node cleanup.js delete
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Liste des fichiers à considérer comme obsolètes
|
||||||
|
const deprecatedFiles = [
|
||||||
|
// Anciens fichiers de routes qui ont été remplacés par src/routes/*
|
||||||
|
'routes/uploadRoutes.js',
|
||||||
|
'routes/projectRoutes.js',
|
||||||
|
'routes/measurementRoutes.js',
|
||||||
|
'routes/videoRoutes.js',
|
||||||
|
'routes/capture_system.js',
|
||||||
|
'routes/imageRoutes.js',
|
||||||
|
|
||||||
|
// Utilitaires remplacés
|
||||||
|
'utils/serverError.js',
|
||||||
|
|
||||||
|
// Fichiers de backend qui ont été refactorisés
|
||||||
|
'ffmpeg.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fonction pour lister les fichiers obsolètes
|
||||||
|
function listDeprecatedFiles() {
|
||||||
|
console.log('====== Fichiers obsolètes ======');
|
||||||
|
deprecatedFiles.forEach(file => {
|
||||||
|
const filePath = path.join(__dirname, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.log(`✓ ${file} (existe)`);
|
||||||
|
} else {
|
||||||
|
console.log(`✗ ${file} (déjà supprimé)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('==============================');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour supprimer les fichiers obsolètes
|
||||||
|
function deleteDeprecatedFiles() {
|
||||||
|
console.log('====== Suppression des fichiers obsolètes ======');
|
||||||
|
deprecatedFiles.forEach(file => {
|
||||||
|
const filePath = path.join(__dirname, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
console.log(`✅ ${file} supprimé avec succès`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Erreur lors de la suppression de ${file}:`, error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ ${file} n'existe pas ou a déjà été supprimé`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('==============================================');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traitement des arguments
|
||||||
|
const action = process.argv[2];
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
listDeprecatedFiles();
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
listDeprecatedFiles();
|
||||||
|
console.log('\nConfirmation de suppression...');
|
||||||
|
deleteDeprecatedFiles();
|
||||||
|
} else {
|
||||||
|
console.log(`
|
||||||
|
Usage:
|
||||||
|
- Pour lister les fichiers obsolètes : node cleanup.js list
|
||||||
|
- Pour supprimer les fichiers obsolètes : node cleanup.js delete
|
||||||
|
`);
|
||||||
|
}
|
||||||
30
db.js
30
db.js
@@ -1,25 +1,9 @@
|
|||||||
const { Client } = require('pg');
|
/**
|
||||||
|
* Ce fichier est maintenu pour des raisons de compatibilité
|
||||||
|
* mais redirige vers la nouvelle structure de connexion à la base de données.
|
||||||
|
* À terme, toutes les références devraient utiliser src/database/connection.js
|
||||||
|
*/
|
||||||
|
|
||||||
const local = false;
|
const db = require('./src/database/connection');
|
||||||
// Connexion à la base de données PostgreSQL
|
|
||||||
const client = new Client({
|
|
||||||
host: local ? 'mikoshi' : '172.30.0.2',
|
|
||||||
port: local ? 54322 : 5432,
|
|
||||||
user: 'timelapse',
|
|
||||||
password: 'timelapse',
|
|
||||||
database: 'timelapse'
|
|
||||||
});
|
|
||||||
|
|
||||||
function connectWithRetry() {
|
module.exports = db;
|
||||||
client.connect(err => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Erreur de connexion à la base de données:', err);
|
|
||||||
setTimeout(connectWithRetry, 30000); // Réessayer après 30 secondes
|
|
||||||
} else {
|
|
||||||
console.log('[DB] Connecté à la base de données PostgreSQL.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectWithRetry();
|
|
||||||
module.exports = client;
|
|
||||||
53
deploy.sh
Executable file
53
deploy.sh
Executable 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 "$@"
|
||||||
40
docker-compose.yml
Normal file
40
docker-compose.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
timelapse-api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: timelapse-api
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./:/backend
|
||||||
|
- ./storage:/storage
|
||||||
|
- node_modules:/backend/node_modules
|
||||||
|
environment:
|
||||||
|
- NODE_VERSION=22.9.0
|
||||||
|
- YARN_VERSION=1.22.22
|
||||||
|
working_dir: /backend
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- bridge
|
||||||
|
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:
|
||||||
|
bridge:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node_modules:
|
||||||
11
eslint.config.mjs
Normal file
11
eslint.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import globals from "globals";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{ files: ["**/*.{js,mjs,cjs}"] },
|
||||||
|
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
|
||||||
|
{ files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.browser } },
|
||||||
|
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"] },
|
||||||
|
]);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
exec('ffmpeg -version', (error, stdout, stderr) => {
|
exec('ffmpeg -version', (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log('FFmpeg is not installed. Installing FFmpeg...');
|
console.log('FFmpeg is not installed. Installing FFmpeg...');
|
||||||
exec('apt update && apt install -y ffmpeg', (installError, installStdout, installStderr) => {
|
exec('apt update && apt install -y ffmpeg', (installError) => {
|
||||||
if (installError) {
|
if (installError) {
|
||||||
console.error(`Error installing FFmpeg: ${installError}`);
|
console.error(`Error installing FFmpeg: ${installError}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
1540
package-lock.json
generated
1540
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js"
|
"dev": "nodemon server.js",
|
||||||
|
"local" : "nodemon server_local.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -16,11 +17,16 @@
|
|||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.23.0",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"eslint": "^9.23.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"nodemon": "^3.1.7"
|
"nodemon": "^3.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const dbTester = require('../test/tester');
|
|
||||||
const db = require('../db');
|
|
||||||
const serverError = require('../utils/serverError');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const db = require('../db');
|
|
||||||
const measureManager = require('../src/measure/measureManager');
|
|
||||||
const serverError = require('../utils/serverError');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.get('/measurements', (req, res) => {
|
|
||||||
const query = 'SELECT * FROM public.measurements';
|
|
||||||
db.query(query, (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de la récupération des mesures:', res, err);
|
|
||||||
}
|
|
||||||
res.json(results.rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.get('/measurements/:id', (req, res) => {
|
|
||||||
const measurementId = req.params.id;
|
|
||||||
if (!measurementId || isNaN(measurementId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid measurement ID' });
|
|
||||||
}
|
|
||||||
const query = 'SELECT * FROM public.measurements WHERE id = $1';
|
|
||||||
db.query(query, [measurementId], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de la récupération de la mesure:', res, err);
|
|
||||||
}
|
|
||||||
res.json(results.rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.get('/measurements/:projectId/:orderId', async (req, res) => {
|
|
||||||
const projectId = req.params.projectId;
|
|
||||||
const orderId = req.params.orderId;
|
|
||||||
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid project ID or order ID' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const measurement = await measureManager.getMeasurement(projectId, orderId);
|
|
||||||
res.json(measurement);
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error getting measurement:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.post('/measurements', (req, res) => {
|
|
||||||
const { project_id, timestamp, image_path, temperature, humidity } = req.body;
|
|
||||||
if (!project_id || !timestamp || !image_path || !temperature || !humidity) {
|
|
||||||
return res.status(400).json({ error: 'All fields are required' });
|
|
||||||
}
|
|
||||||
const query = 'INSERT INTO public.measurements (project_id, timestamp, image_path, temperature, humidity) VALUES ($1, $2, $3, $4, $5) RETURNING id';
|
|
||||||
db.query(query, [project_id, timestamp, image_path, temperature, humidity], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de l\'ajout de la mesure:', res, err);
|
|
||||||
}
|
|
||||||
res.status(201).json({ message: 'Mesure ajoutée avec succès', id: results.rows[0].id });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.delete('/measurements/:id', async (req, res) => {
|
|
||||||
const measurementId = req.params.id;
|
|
||||||
if (!measurementId || isNaN(measurementId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid measurement ID' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const measurement = await measureManager.deleteMeasurement(measurementId);
|
|
||||||
res.status(200).json({ message: 'Measurement deleted successfully', id: measurementId });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error deleting measurement:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.delete('/measurements/:projectId/:orderId', async (req, res) => {
|
|
||||||
const projectId = req.params.projectId;
|
|
||||||
const orderId = req.params.orderId;
|
|
||||||
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) {
|
|
||||||
return res.status(400).json({ error: 'Invalid project ID or order ID' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const measurement = await measureManager.deleteMeasurementByOrderId(projectId, orderId);
|
|
||||||
res.status(200).json({ message: 'Measurement deleted successfully', id: measurement.id });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error deleting measurement:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const projectManager = require('../src/project/projectManager');
|
|
||||||
const serverError = require('../utils/serverError');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.get('/projects', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const projects = await projectManager.getAllProjects();
|
|
||||||
res.json(projects);
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error getting all projects:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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 projectManager.getProjectById(projectId);
|
|
||||||
res.json(project);
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error getting project by ID:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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 projectManager.getVideosByProjectId(projectId);
|
|
||||||
res.json(videos);
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error getting videos by project ID:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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 projectManager.getMeasurementsByProjectId(projectId);
|
|
||||||
res.json(measurements);
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error getting measurements by project ID:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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 project = await projectManager.createProject(name, description, new Date(), 0);
|
|
||||||
projectManager.createProjectDirectory(project.id);
|
|
||||||
res.status(201).json({ message: 'Project added successfully', id: project.id });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error creating project:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /projects/{id}:
|
|
||||||
* delete:
|
|
||||||
* summary: Supprimer un projet par 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.
|
|
||||||
*/
|
|
||||||
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 {
|
|
||||||
projectManager.deleteProjectDirectory(projectId);
|
|
||||||
projectManager.deleteProjectById(projectId);
|
|
||||||
res.status(200).json({ message: 'Project deleted successfully', id: projectId });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error deleting project:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const multer = require('multer');
|
|
||||||
const measureManager = require('../src/measure/measureManager');
|
|
||||||
const serverError = require('../utils/serverError');
|
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /upload:
|
|
||||||
* post:
|
|
||||||
* summary: Télécharger une image
|
|
||||||
* description: Télécharge une image pour un projet et un ordre spécifiques.
|
|
||||||
* 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
|
|
||||||
* orderId:
|
|
||||||
* type: integer
|
|
||||||
* description: ID de la commande
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Image téléchargée avec succès.
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* path:
|
|
||||||
* type: string
|
|
||||||
* 400:
|
|
||||||
* description: Tous les champs sont requis.
|
|
||||||
* 500:
|
|
||||||
* description: Erreur serveur.
|
|
||||||
*/
|
|
||||||
router.post('/upload', upload.single('image'), async (req, res) => {
|
|
||||||
const { projectId, orderId } = req.body;
|
|
||||||
const image = req.file;
|
|
||||||
|
|
||||||
if (!image || !projectId || !orderId) {
|
|
||||||
return res.status(400).json({ error: 'All fields are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const imagePath = await measureManager.uploadMeasureImage(image, projectId, orderId);
|
|
||||||
res.json({ message: 'Image uploaded successfully', path: imagePath });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error uploading image:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
router.post('/uploadmeasurement', upload.single('image'), async (req, res) => {
|
|
||||||
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 measureManager.getNextOrderId(projectId);
|
|
||||||
const imagePath = await measureManager.uploadMeasureImage(image, projectId, nextOrderId);
|
|
||||||
const measurement = await measureManager.addMeasureToProject(projectId, timestamp, imagePath, temperature, humidity, nextOrderId);
|
|
||||||
res.json({ message: 'Measurement uploaded successfully', path: imagePath, id: measurement.id });
|
|
||||||
} catch (error) {
|
|
||||||
serverError.sendError('Error uploading measurement:', res, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const db = require('../db');
|
|
||||||
const serverError = require('../utils/serverError');
|
|
||||||
const videoManager = require('../src/video/videoManager');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
res.json(results.rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
res.json(results.rows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /videos:
|
|
||||||
* post:
|
|
||||||
* summary: Ajouter une nouvelle vidéo
|
|
||||||
* description: Ajoute une nouvelle vidéo à la base de données.
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* project_id:
|
|
||||||
* type: integer
|
|
||||||
* measurement_ids:
|
|
||||||
* type: string
|
|
||||||
* video_path:
|
|
||||||
* type: string
|
|
||||||
* duration:
|
|
||||||
* type: number
|
|
||||||
* resolution:
|
|
||||||
* type: string
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: Vidéo ajoutée avec succès.
|
|
||||||
* 400:
|
|
||||||
* description: Tous les champs sont requis.
|
|
||||||
* 500:
|
|
||||||
* description: Erreur serveur.
|
|
||||||
*/
|
|
||||||
router.post('/videos', (req, res) => {
|
|
||||||
const { project_id, measurement_ids, video_path, duration, resolution, name } = req.body;
|
|
||||||
if (!project_id || !measurement_ids || !video_path || !duration || !resolution || !name) {
|
|
||||||
return res.status(400).json({ error: 'All fields are required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const list_ids = measurement_ids.split(',');
|
|
||||||
const image_count = list_ids.length;
|
|
||||||
const videoPath = '/videos/' + name + '.mp4';
|
|
||||||
|
|
||||||
const query_first = 'SELECT timestamp FROM public.measurements WHERE id = $1';
|
|
||||||
const query_last = 'SELECT timestamp FROM public.measurements WHERE id = $1';
|
|
||||||
|
|
||||||
db.query(query_first, [list_ids[0]], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de la récupération du timestamp de la première image:', res, err);
|
|
||||||
}
|
|
||||||
const start_timestamp = results.rows[0].timestamp;
|
|
||||||
|
|
||||||
db.query(query_last, [list_ids[image_count - 1]], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de la récupération du timestamp de la dernière image:', res, err);
|
|
||||||
}
|
|
||||||
const end_timestamp = results.rows[0].timestamp;
|
|
||||||
const fps = image_count / duration;
|
|
||||||
|
|
||||||
const query = 'INSERT INTO public.videos (project_id, measurement_ids, video_path, start_timestamp, end_timestamp, image_count, resolution, duration, fps, status, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id';
|
|
||||||
db.query(query, [project_id, measurement_ids, videoPath, start_timestamp, end_timestamp, image_count, resolution, duration, fps, 0, name], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de l\'ajout de la vidéo:', res, err);
|
|
||||||
}
|
|
||||||
res.status(201).json({ message: 'Vidéo ajoutée avec succès', id: results.rows[0].id });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
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 = 'DELETE FROM public.videos WHERE id = $1 RETURNING id';
|
|
||||||
db.query(query, [videoId], (err, results) => {
|
|
||||||
if (err) {
|
|
||||||
serverError.sendError('Erreur lors de la suppression de la vidéo:', res, err);
|
|
||||||
}
|
|
||||||
if (results.rowCount === 0) {
|
|
||||||
return res.status(404).json({ error: 'Aucune vidéo trouvée avec cet ID.' });
|
|
||||||
}
|
|
||||||
res.status(200).json({ message: 'Vidéo supprimée avec succès', id: videoId });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
BIN
sample/cat.mp4
Normal file
BIN
sample/cat.mp4
Normal file
Binary file not shown.
78
server.js
78
server.js
@@ -2,46 +2,30 @@
|
|||||||
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`);
|
||||||
});
|
});
|
||||||
|
|||||||
182
src/config/index.js
Normal file
182
src/config/index.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
187
src/controllers/cameraController.js
Normal file
187
src/controllers/cameraController.js
Normal 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;
|
||||||
183
src/controllers/imageController.js
Normal file
183
src/controllers/imageController.js
Normal 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;
|
||||||
135
src/controllers/measurementController.js
Normal file
135
src/controllers/measurementController.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// src/controllers/measurementController.js
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux mesures
|
||||||
|
*/
|
||||||
|
class MeasurementController {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures
|
||||||
|
*/
|
||||||
|
static getAllMeasurements = asyncHandler(async (req, res) => {
|
||||||
|
const measurements = await Measurement.getAllMeasurements();
|
||||||
|
|
||||||
|
if (!measurements || measurements.length === 0) {
|
||||||
|
return sendError('Aucune mesure trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurements);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son ID
|
||||||
|
*/
|
||||||
|
static getMeasurementById = asyncHandler(async (req, res) => {
|
||||||
|
const measurementId = req.params.id;
|
||||||
|
|
||||||
|
if (!measurementId || isNaN(measurementId)) {
|
||||||
|
return sendError('ID de mesure invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = await Measurement.getMeasurementById(measurementId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son projet ID et son ordre ID
|
||||||
|
*/
|
||||||
|
static getMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurement);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par son ID
|
||||||
|
*/
|
||||||
|
static deleteMeasurement = asyncHandler(async (req, res) => {
|
||||||
|
const measurementId = req.params.id;
|
||||||
|
|
||||||
|
if (!measurementId || isNaN(measurementId)) {
|
||||||
|
return sendError('ID de mesure invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les informations de la mesure avant suppression
|
||||||
|
const measurement = await Measurement.getMeasurementById(measurementId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime le fichier image associé si existant
|
||||||
|
if (measurement.path) {
|
||||||
|
try {
|
||||||
|
await fs.access(measurement.path);
|
||||||
|
await fs.unlink(measurement.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime l'entrée de la base de données
|
||||||
|
await Measurement.deleteMeasurement(measurementId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Mesure supprimée avec succès',
|
||||||
|
id: measurementId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par projet ID et ordre ID
|
||||||
|
*/
|
||||||
|
static deleteMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
|
||||||
|
const { projectId, orderId } = req.params;
|
||||||
|
|
||||||
|
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
|
||||||
|
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupère les informations de la mesure avant suppression
|
||||||
|
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return sendError('Mesure non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime le fichier image associé si existant
|
||||||
|
if (measurement.path) {
|
||||||
|
try {
|
||||||
|
await fs.access(measurement.path);
|
||||||
|
await fs.unlink(measurement.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprime l'entrée de la base de données
|
||||||
|
await Measurement.deleteMeasurement(measurement.id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Mesure supprimée avec succès',
|
||||||
|
id: measurement.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MeasurementController;
|
||||||
123
src/controllers/projectController.js
Normal file
123
src/controllers/projectController.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// src/controllers/projectController.js
|
||||||
|
const Project = require('../models/Project');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux projets
|
||||||
|
*/
|
||||||
|
class ProjectController {
|
||||||
|
/**
|
||||||
|
* Récupère tous les projets
|
||||||
|
*/
|
||||||
|
static getAllProjects = asyncHandler(async (req, res) => {
|
||||||
|
const projects = await Project.getAllProjects();
|
||||||
|
res.json(projects);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un projet par son ID
|
||||||
|
*/
|
||||||
|
static getProjectById = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = await Project.getProjectById(projectId);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return sendError('Projet non trouvé', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les vidéos d'un projet
|
||||||
|
*/
|
||||||
|
static getProjectVideos = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const videos = await Video.getVideosByProjectId(projectId);
|
||||||
|
|
||||||
|
if (videos.length === 0) {
|
||||||
|
return sendError('Aucune vidéo trouvée pour ce projet', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(videos);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les mesures d'un projet
|
||||||
|
*/
|
||||||
|
static getProjectMeasurements = asyncHandler(async (req, res) => {
|
||||||
|
const projectId = req.params.id;
|
||||||
|
|
||||||
|
if (!projectId || isNaN(projectId)) {
|
||||||
|
return sendError('ID de projet invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurements = await Measurement.getMeasurementsByProjectId(projectId);
|
||||||
|
|
||||||
|
if (measurements.length === 0) {
|
||||||
|
return sendError('Aucune mesure trouvée pour ce projet', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(measurements);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouveau projet
|
||||||
|
*/
|
||||||
|
static createProject = asyncHandler(async (req, res) => {
|
||||||
|
const { name, description } = req.body;
|
||||||
|
|
||||||
|
if (!name || !description) {
|
||||||
|
return sendError('Le nom et la description sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const defaultStatus = config.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;
|
||||||
256
src/controllers/videoController.js
Normal file
256
src/controllers/videoController.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
// src/controllers/videoController.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const rangeParser = require('range-parser');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
const VideoService = require('../services/videoService');
|
||||||
|
const { sendError, asyncHandler } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrôleur pour les opérations liées aux vidéos
|
||||||
|
*/
|
||||||
|
class VideoController {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les vidéos
|
||||||
|
*/
|
||||||
|
static getAllVideos = asyncHandler(async (req, res) => {
|
||||||
|
const videos = await Video.getAllVideos();
|
||||||
|
res.json(videos);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une vidéo par son ID
|
||||||
|
*/
|
||||||
|
static getVideoById = asyncHandler(async (req, res) => {
|
||||||
|
const videoId = req.params.id;
|
||||||
|
|
||||||
|
if (!videoId || isNaN(videoId)) {
|
||||||
|
return sendError('ID de vidéo invalide', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = await Video.getVideoById(videoId);
|
||||||
|
|
||||||
|
if (!video) {
|
||||||
|
return sendError('Vidéo non trouvée', res, null, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(video);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle vidéo
|
||||||
|
*/
|
||||||
|
static createVideo = asyncHandler(async (req, res) => {
|
||||||
|
const { project_id, measurement_ids, name, resolution, duration } = req.body;
|
||||||
|
|
||||||
|
if (!project_id || !measurement_ids || !name || !resolution || !duration) {
|
||||||
|
return sendError('Tous les champs sont requis', res, null, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[VIDEO] Création de vidéo avec les mesures:', measurement_ids);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Crée l'entrée vidéo dans la base de données
|
||||||
|
const video = await Video.createVideo(
|
||||||
|
project_id,
|
||||||
|
measurement_ids,
|
||||||
|
name,
|
||||||
|
resolution,
|
||||||
|
duration,
|
||||||
|
config.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;
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
import db from '../../db.js';
|
const db = require('../../db.js');
|
||||||
import path from 'path';
|
const storage_manager = require('./storage_manager.js');
|
||||||
import storageManager from '../data/storageManager.js';
|
const fs = require('fs');
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
let localCounter = 0;
|
let localCounter = 0;
|
||||||
|
|
||||||
async function checkAndRemoveInvalidEntries() {
|
async function checkAndRemoveInvalidEntries() {
|
||||||
localCounter = 0;
|
localCounter = 0;
|
||||||
console.log('[INFO] Vérification et suppression des entrées invalides...');
|
|
||||||
try {
|
try {
|
||||||
const measurementsRes = await db.query('SELECT id, path FROM measurements');
|
const measurementsRes = await db.query('SELECT id, path FROM measurements');
|
||||||
//console.log('Fetched measurements:', measurementsRes.rows);
|
|
||||||
for (const row of measurementsRes.rows) {
|
for (const row of measurementsRes.rows) {
|
||||||
//console.log('Checking file path:', row.path);
|
|
||||||
if (!fs.existsSync(row.path)) {
|
if (!fs.existsSync(row.path)) {
|
||||||
await db.query('DELETE FROM measurements WHERE id = $1', [row.id]);
|
await db.query('DELETE FROM measurements WHERE id = $1', [row.id]);
|
||||||
console.log(`Deleted invalid measurement entry with id: ${row.id}`);
|
console.log(`Deleted invalid measurement entry with id: ${row.id}`);
|
||||||
@@ -20,22 +16,18 @@ async function checkAndRemoveInvalidEntries() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan all images in storage
|
const allImages = await storage_manager.scanAllImages();
|
||||||
const allImages = await storageManager.scanAllImages();
|
|
||||||
//console.log('Scanned all images:', allImages);
|
|
||||||
for (const imagePath of allImages) {
|
for (const imagePath of allImages) {
|
||||||
const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]);
|
const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]);
|
||||||
if (entryRes.rows.length === 0) {
|
if (entryRes.rows.length === 0) {
|
||||||
// Remove the file if the entry does not exist
|
|
||||||
fs.unlinkSync(imagePath);
|
fs.unlinkSync(imagePath);
|
||||||
console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`);
|
console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`);
|
||||||
localCounter++; // Increment counter if entry is deleted
|
localCounter++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localCounter > 0) {
|
if (localCounter > 0) {
|
||||||
console.log(`[INFO] ${localCounter} entrées ont été modifiées`);
|
console.log(`[INFO] ${localCounter} entrées ont été modifiées`);
|
||||||
} else {
|
localCounter = 0; // Reset the counter after logging
|
||||||
console.log('[INFO] Aucune entrée n\'a été modifiée.');
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error checking and removing invalid entries:', err);
|
console.error('Error checking and removing invalid entries:', err);
|
||||||
@@ -46,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();
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
const PROJECTS_DIR = path.join('.');
|
|
||||||
|
|
||||||
async function createFolder(name) {
|
|
||||||
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) {
|
|
||||||
const projectDir = path.join(PROJECTS_DIR, `${name}`);
|
|
||||||
try {
|
|
||||||
await fs.access(projectDir);
|
|
||||||
await fs.rm(projectDir, { recursive: true, force: true });
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scanAllImages(dir = 'storage') {
|
|
||||||
const projectDir = path.join(PROJECTS_DIR, dir);
|
|
||||||
let results = [];
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createFolder,
|
|
||||||
deleteFolder,
|
|
||||||
scanAllImages,
|
|
||||||
saveFile,
|
|
||||||
getFile,
|
|
||||||
deleteFile
|
|
||||||
};
|
|
||||||
30
src/data/storage_manager.js
Normal file
30
src/data/storage_manager.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Ce fichier est conservé pour la rétrocompatibilité mais redirige vers le nouveau service de stockage.
|
||||||
|
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const StorageService = require('../services/storageService');
|
||||||
|
|
||||||
|
// Structure de redirection pour la compatibilité
|
||||||
|
const storage_manager = {
|
||||||
|
project: {
|
||||||
|
create_project_directory: StorageService.project.createProjectDirectory,
|
||||||
|
delete_project_directory: StorageService.project.deleteProjectDirectory
|
||||||
|
},
|
||||||
|
measurement: {
|
||||||
|
get_measurement_image: StorageService.measurement.getMeasurementImage,
|
||||||
|
upload_measurement_image: StorageService.measurement.uploadMeasurementImage
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
get_video: StorageService.video.getVideo,
|
||||||
|
delete_video: StorageService.video.deleteVideo
|
||||||
|
},
|
||||||
|
scan_images: StorageService.scanImages,
|
||||||
|
create_directory: StorageService.createDirectory,
|
||||||
|
delete_directory: StorageService.deleteDirectory,
|
||||||
|
save_file: StorageService.saveFile,
|
||||||
|
get_file: StorageService.getFile,
|
||||||
|
delete_file: StorageService.deleteFile
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = storage_manager;
|
||||||
44
src/database/connection.js
Normal file
44
src/database/connection.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// src/database/connection.js
|
||||||
|
const { Client } = require('pg');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Création du client PostgreSQL avec la configuration centralisée
|
||||||
|
const client = new Client({
|
||||||
|
host: config.database.host,
|
||||||
|
port: config.database.port,
|
||||||
|
user: config.database.user,
|
||||||
|
password: config.database.password,
|
||||||
|
database: config.database.database
|
||||||
|
});
|
||||||
|
|
||||||
|
let isConnecting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la connexion à la base de données
|
||||||
|
* Réessaie automatiquement si la connexion échoue
|
||||||
|
*/
|
||||||
|
function initDatabase() {
|
||||||
|
if (isConnecting) {
|
||||||
|
console.log('[DB] Tentative de connexion déjà en cours, ignorer...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DB] Initialisation de la connexion à PostgreSQL...');
|
||||||
|
isConnecting = true;
|
||||||
|
|
||||||
|
client.connect(err => {
|
||||||
|
isConnecting = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error('[DB] Erreur de connexion à la base de données:', err);
|
||||||
|
setTimeout(initDatabase, config.database.reconnectInterval);
|
||||||
|
} else {
|
||||||
|
console.log('[DB] Connecté à la base de données PostgreSQL.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise la connexion lors de l'importation de ce module
|
||||||
|
initDatabase();
|
||||||
|
|
||||||
|
module.exports = client;
|
||||||
47
src/database/database_manager.js
Normal file
47
src/database/database_manager.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Project = require('../models/Project');
|
||||||
|
const Measurement = require('../models/Measurement');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
const Camera = require('../models/Camera');
|
||||||
|
|
||||||
|
// Structure de redirection pour la compatibilité
|
||||||
|
const database_manager = {
|
||||||
|
project: {
|
||||||
|
get_all_projects: Project.getAllProjects,
|
||||||
|
get_project_by_id: Project.getProjectById,
|
||||||
|
create_project: Project.createProject,
|
||||||
|
edit_project_by_id: Project.updateProject,
|
||||||
|
delete_project: Project.deleteProject,
|
||||||
|
find_current_rendering_project: Project.findCurrentRenderingProject
|
||||||
|
},
|
||||||
|
measurement: {
|
||||||
|
get_all_measurements: Measurement.getAllMeasurements,
|
||||||
|
get_measurement_by_id: Measurement.getMeasurementById,
|
||||||
|
get_measurement_by_project_id_and_order_id: Measurement.getMeasurementByProjectAndOrderId,
|
||||||
|
get_measurements_by_project_id: Measurement.getMeasurementsByProjectId,
|
||||||
|
create_measurement: Measurement.createMeasurement,
|
||||||
|
edit_measurement_by_id: Measurement.updateMeasurement,
|
||||||
|
delete_measurement: Measurement.deleteMeasurement,
|
||||||
|
get_next_order_id: Measurement.getNextOrderId
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
get_all_videos: Video.getAllVideos,
|
||||||
|
get_video_by_id: Video.getVideoById,
|
||||||
|
get_videos_by_project_id: Video.getVideosByProjectId,
|
||||||
|
create_video: Video.createVideo,
|
||||||
|
edit_video_by_id: Video.updateVideo,
|
||||||
|
delete_video: Video.deleteVideo
|
||||||
|
},
|
||||||
|
capture: {
|
||||||
|
get_camera: Camera.getCamera,
|
||||||
|
edit_camera: Camera.updateCamera,
|
||||||
|
init_camera: Camera.initializeCamera
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = database_manager;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import db from '../../db.js';
|
|
||||||
import path from 'path';
|
|
||||||
import storageManager from '../data/storageManager.js';
|
|
||||||
|
|
||||||
async function uploadMeasureImage(image, projectId, orderId) {
|
|
||||||
const projectDir = storageManager.createFolder('./storage/' + projectId.toString());
|
|
||||||
const imagesDir = storageManager.createFolder(path.join(projectDir, 'images'));
|
|
||||||
var imagePath = path.join(imagesDir, `${orderId}.jpg`);
|
|
||||||
storageManager.saveFile(imagePath, image.buffer);
|
|
||||||
console.log("[FILE] uploadMeasureImage - Image saved to: " + imagePath);
|
|
||||||
return imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMeasureImage(projectId, orderId) {
|
|
||||||
const projectPath = `${projectId}`;
|
|
||||||
const imagePath = `${projectPath}/${orderId}.jpg`;
|
|
||||||
console.log("[FILE] getMeasureImage - Image path: " + imagePath);
|
|
||||||
return storageManager.getFile(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNextOrderId(projectId) {
|
|
||||||
const query = 'SELECT MAX(order_id) FROM public.measurements WHERE project_id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] getNextOrderId - Max order_id: " + res.rows[0].max);
|
|
||||||
return res.rows[0].max + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addMeasureToProject(projectId, orderId, timestamp, path, temperature, humidity) {
|
|
||||||
const query = 'INSERT INTO public.measurements (project_id, timestamp, path, temperature, humidity, order_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *';
|
|
||||||
const values = [projectId, orderId, timestamp, path, temperature, humidity];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMeasurements(projectId) {
|
|
||||||
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMeasurement(projectId, orderId) {
|
|
||||||
const query = 'SELECT * FROM public.measurements WHERE project_id = $1 AND order_id = $2';
|
|
||||||
const values = [projectId, orderId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMeasurementById(id) {
|
|
||||||
const query = 'SELECT * FROM public.measurements WHERE id = $1';
|
|
||||||
const values = [id];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMeasurement(projectId, orderId, timestamp, path, temperature, humidity) {
|
|
||||||
const query = 'UPDATE public.measurements SET timestamp = $3, path = $4, temperature = $5, humidity = $6 WHERE project_id = $1 AND order_id = $2 RETURNING *';
|
|
||||||
const values = [projectId, orderId, timestamp, path, temperature, humidity];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateMeasurementById(id, timestamp, path, temperature, humidity) {
|
|
||||||
const query = 'UPDATE public.measurements SET timestamp = $2, path = $3, temperature = $4, humidity = $5 WHERE id = $1 RETURNING *';
|
|
||||||
const values = [id, timestamp, path, temperature, humidity];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteMeasurement(id) {
|
|
||||||
const query = 'DELETE FROM public.measurements WHERE id = $1';
|
|
||||||
const values = [id];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPathFromIds(projectId, orderId) {
|
|
||||||
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
|
|
||||||
const values = [projectId, orderId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
return res.rows[0].path;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
uploadMeasureImage,
|
|
||||||
addMeasureToProject,
|
|
||||||
getNextOrderId,
|
|
||||||
getMeasurements,
|
|
||||||
getMeasurement,
|
|
||||||
updateMeasurement,
|
|
||||||
deleteMeasurement,
|
|
||||||
getMeasureImage,
|
|
||||||
getMeasurementById,
|
|
||||||
updateMeasurementById,
|
|
||||||
getPathFromIds
|
|
||||||
}
|
|
||||||
72
src/models/Camera.js
Normal file
72
src/models/Camera.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// src/models/Camera.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion de la caméra
|
||||||
|
*/
|
||||||
|
class Camera {
|
||||||
|
/**
|
||||||
|
* Récupère les paramètres de la caméra
|
||||||
|
* @returns {Promise<Object|null>} Paramètres de la caméra ou null si non trouvés
|
||||||
|
*/
|
||||||
|
static getCamera = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM camera WHERE id = 1;`;
|
||||||
|
const result = await db.query(query);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les paramètres de la caméra
|
||||||
|
* @param {number} id - ID de l'entrée caméra (normalement 1)
|
||||||
|
* @param {Object} updates - Paramètres à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Paramètres mis à jour ou null si non trouvés
|
||||||
|
*/
|
||||||
|
static updateCamera = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE camera SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les paramètres de la caméra
|
||||||
|
* @param {number} id - ID de l'entrée caméra à supprimer
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteCamera = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM camera WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les paramètres de la caméra par défaut
|
||||||
|
* @returns {Promise<Object>} Paramètres de caméra créés
|
||||||
|
*/
|
||||||
|
static initializeCamera = wrapDatabaseOperation(async () => {
|
||||||
|
const { defaultSettings } = config.camera;
|
||||||
|
const query = `
|
||||||
|
INSERT INTO camera (id, interval, maintenance, 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
126
src/models/Measurement.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// src/models/Measurement.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle pour la gestion des mesures (photos avec données)
|
||||||
|
*/
|
||||||
|
class Measurement {
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures
|
||||||
|
* @returns {Promise<Array>} Liste de toutes les mesures
|
||||||
|
*/
|
||||||
|
static getAllMeasurements = wrapDatabaseOperation(async () => {
|
||||||
|
const query = `SELECT * FROM measurements;`;
|
||||||
|
return (await db.query(query)).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son ID
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static getMeasurementById = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE id = $1;`;
|
||||||
|
const result = await db.query(query, [id]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une mesure par son projet ID et son ordre ID
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static getMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE project_id = $1 AND order_id = $2;`;
|
||||||
|
const result = await db.query(query, [projectId, orderId]);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les mesures d'un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @returns {Promise<Array>} Liste des mesures du projet
|
||||||
|
*/
|
||||||
|
static getMeasurementsByProjectId = wrapDatabaseOperation(async (projectId) => {
|
||||||
|
const query = `SELECT * FROM measurements WHERE project_id = $1 ORDER BY order_id;`;
|
||||||
|
return (await db.query(query, [projectId])).rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle mesure
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {string} timestamp - Horodatage de la mesure
|
||||||
|
* @param {string} path - Chemin vers l'image
|
||||||
|
* @param {number} temperature - Température mesurée
|
||||||
|
* @param {number} humidity - Humidité mesurée
|
||||||
|
* @param {number} orderId - Ordre séquentiel de la mesure
|
||||||
|
* @returns {Promise<Object>} Mesure créée
|
||||||
|
*/
|
||||||
|
static createMeasurement = wrapDatabaseOperation(async (
|
||||||
|
projectId, timestamp, path, temperature, humidity, orderId
|
||||||
|
) => {
|
||||||
|
const query = `
|
||||||
|
INSERT INTO measurements (project_id, timestamp, path, temperature, humidity, order_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING *;
|
||||||
|
`;
|
||||||
|
const result = await db.query(
|
||||||
|
query, [projectId, timestamp, path, temperature, humidity, orderId]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une mesure existante
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateMeasurement = wrapDatabaseOperation(async (id, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
|
||||||
|
const values = [id, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE measurements SET ${fields} WHERE id = $1 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une mesure par projet ID et ordre ID
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @param {Object} updates - Champs à mettre à jour
|
||||||
|
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
|
||||||
|
*/
|
||||||
|
static updateMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId, updates) => {
|
||||||
|
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 3}`).join(', ');
|
||||||
|
const values = [projectId, orderId, ...Object.values(updates)];
|
||||||
|
const query = `UPDATE measurements SET ${fields} WHERE project_id = $1 AND order_id = $2 RETURNING *;`;
|
||||||
|
const result = await db.query(query, values);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une mesure par son ID
|
||||||
|
* @param {number} id - ID de la mesure
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static deleteMeasurement = wrapDatabaseOperation(async (id) => {
|
||||||
|
const query = `DELETE FROM measurements WHERE id = $1;`;
|
||||||
|
await db.query(query, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient le prochain ordre ID disponible pour un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @returns {Promise<number>} Prochain ordre ID
|
||||||
|
*/
|
||||||
|
static getNextOrderId = wrapDatabaseOperation(async (projectId) => {
|
||||||
|
const query = `SELECT COALESCE(MAX(order_id), 0) + 1 AS next_order_id FROM measurements WHERE project_id = $1;`;
|
||||||
|
const result = await db.query(query, [projectId]);
|
||||||
|
return result.rows[0].next_order_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Measurement;
|
||||||
109
src/models/Project.js
Normal file
109
src/models/Project.js
Normal 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
147
src/models/Video.js
Normal 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
144
src/models/database.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// src/models/database.js
|
||||||
|
const db = require('../database/connection');
|
||||||
|
const { wrapDatabaseOperation } = require('../utils/errorHandler');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module gérant l'initialisation et la maintenance de la structure de la base de données
|
||||||
|
*/
|
||||||
|
class DatabaseManager {
|
||||||
|
/**
|
||||||
|
* Crée les tables de la base de données si elles n'existent pas déjà
|
||||||
|
*/
|
||||||
|
static async createDatabase() {
|
||||||
|
const queries = [
|
||||||
|
`CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
start_date DATE,
|
||||||
|
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3]))
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE projects OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS measurements (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
||||||
|
timestamp TIMESTAMP NOT NULL,
|
||||||
|
path VARCHAR(255),
|
||||||
|
temperature DOUBLE PRECISION,
|
||||||
|
humidity DOUBLE PRECISION,
|
||||||
|
order_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT unique_project_photo_order UNIQUE (project_id, order_id)
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE measurements OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS videos (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
|
||||||
|
measurement_ids TEXT NOT NULL,
|
||||||
|
video_file VARCHAR(255),
|
||||||
|
resolution VARCHAR(255),
|
||||||
|
duration INTEGER,
|
||||||
|
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3])),
|
||||||
|
name VARCHAR(255),
|
||||||
|
progress DOUBLE PRECISION,
|
||||||
|
started_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
eta DOUBLE PRECISION
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE videos OWNER TO timelapse;`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS camera (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
interval INTEGER NOT NULL,
|
||||||
|
maintenance INTEGER NOT NULL,
|
||||||
|
active INTEGER DEFAULT 0 NOT NULL
|
||||||
|
);`,
|
||||||
|
`ALTER TABLE camera OWNER TO timelapse;`
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const query of queries) {
|
||||||
|
await db.query(query);
|
||||||
|
}
|
||||||
|
console.log('[DB] Tables créées ou vérifiées avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la création des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie l'existence des tables requises dans la base de données
|
||||||
|
*/
|
||||||
|
static async checkDatabaseExistence() {
|
||||||
|
const query = `
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('projects', 'measurements', 'videos', 'camera');
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(query);
|
||||||
|
const existingTables = result.rows.map(row => row.table_name);
|
||||||
|
|
||||||
|
const requiredTables = ['projects', 'measurements', 'videos', 'camera'];
|
||||||
|
const missingTables = requiredTables.filter(table => !existingTables.includes(table));
|
||||||
|
|
||||||
|
if (missingTables.length > 0) {
|
||||||
|
console.error('[DB] Tables manquantes ou incorrectement construites:', missingTables);
|
||||||
|
throw new Error(`Les tables suivantes sont manquantes ou incorrectement construites: ${missingTables.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log('[DB] Toutes les tables requises existent et sont correctement construites.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la vérification des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime toutes les tables de la base de données
|
||||||
|
*/
|
||||||
|
static async deleteDatabase() {
|
||||||
|
const queries = [
|
||||||
|
`DROP TABLE IF EXISTS videos;`,
|
||||||
|
`DROP TABLE IF EXISTS measurements;`,
|
||||||
|
`DROP TABLE IF EXISTS projects;`,
|
||||||
|
`DROP TABLE IF EXISTS camera;`
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const query of queries) {
|
||||||
|
await db.query(query);
|
||||||
|
}
|
||||||
|
console.log('[DB] Tables supprimées avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de la suppression des tables:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise la base de données en vérifiant et en créant les tables si nécessaire
|
||||||
|
*/
|
||||||
|
static async initialize() {
|
||||||
|
try {
|
||||||
|
await this.checkDatabaseExistence();
|
||||||
|
console.log('[DB] Structure de base de données validée');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Vérification de la base de données échouée:', err);
|
||||||
|
try {
|
||||||
|
await this.deleteDatabase();
|
||||||
|
await this.createDatabase();
|
||||||
|
console.log('[DB] Base de données initialisée avec succès.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[DB] Erreur lors de l\'initialisation de la base de données:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
console.log('[DB] Processus d\'initialisation de la base de données terminé.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DatabaseManager;
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import storageManager from '../data/storageManager.js';
|
|
||||||
import db from '../../db.js';
|
|
||||||
|
|
||||||
function createProjectDirectory(projectId) {
|
|
||||||
const projectPath = `${projectId}`;
|
|
||||||
storageManager.createFolder(projectPath);
|
|
||||||
storageManager.createFolder(`${projectPath}/images`);
|
|
||||||
storageManager.createFolder(`${projectPath}/videos`);
|
|
||||||
console.log("[FILE] createProjectDirectory : " + projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteProjectDirectory(projectId) {
|
|
||||||
const projectPath = `${projectId}`;
|
|
||||||
storageManager.deleteFolder(projectPath);
|
|
||||||
console.log("[FILE] deleteProjectDirectory : " + projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllProjects() {
|
|
||||||
const query = 'SELECT * FROM public.projects';
|
|
||||||
const res = await db.query(query);
|
|
||||||
console.log("[DB] getAllProjects : ", res.rows);
|
|
||||||
return res.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProjectById(projectId) {
|
|
||||||
const query = 'SELECT * FROM public.projects WHERE id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] getProjectById : ", res.rows[0]);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createProject(name, description, start_date, status) {
|
|
||||||
const query = 'INSERT INTO public.projects (name, description, start_date, status) VALUES ($1, $2, $3, $4) RETURNING *';
|
|
||||||
const values = [name, description, start_date, status];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] createProject : ", res.rows[0]);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function editProjectById(projectID, name, description, startDate, status) {
|
|
||||||
const query = 'UPDATE public.projects SET name = $1, description = $2, start_date = $3, status = $4 WHERE id = $5 RETURNING *';
|
|
||||||
const values = [name, description, startDate, status, projectID];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] editProjectById : ", res.rows[0]);
|
|
||||||
return res.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteProjectById(projectId) {
|
|
||||||
const query = 'DELETE FROM public.projects WHERE id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
console.log("[DB] deleteProjectById : ", values);
|
|
||||||
await db.query(query, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getVideosByProjectId(projectId) {
|
|
||||||
const query = 'SELECT * FROM public.videos WHERE project_id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] getVideosByProjectId : ", res.rows);
|
|
||||||
return res.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMeasurementsByProjectId(projectId) {
|
|
||||||
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
|
|
||||||
const values = [projectId];
|
|
||||||
const res = await db.query(query, values);
|
|
||||||
console.log("[DB] getMeasurementsByProjectId : ", res.rows);
|
|
||||||
return res.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
createProjectDirectory,
|
|
||||||
deleteProjectDirectory,
|
|
||||||
getAllProjects,
|
|
||||||
getProjectById,
|
|
||||||
createProject,
|
|
||||||
editProjectById,
|
|
||||||
deleteProjectById,
|
|
||||||
getVideosByProjectId,
|
|
||||||
getMeasurementsByProjectId
|
|
||||||
};
|
|
||||||
194
src/routes/cameraRoutes.js
Normal file
194
src/routes/cameraRoutes.js
Normal 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
215
src/routes/imageRoutes.js
Normal 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
34
src/routes/index.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/routes/index.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const cors = require('cors');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Importe toutes les routes modulaires
|
||||||
|
const projectRoutes = require('./projectRoutes');
|
||||||
|
const measurementRoutes = require('./measurementRoutes');
|
||||||
|
const videoRoutes = require('./videoRoutes');
|
||||||
|
const imageRoutes = require('./imageRoutes');
|
||||||
|
const cameraRoutes = require('./cameraRoutes');
|
||||||
|
|
||||||
|
// Configuration CORS
|
||||||
|
router.use(cors({
|
||||||
|
origin: config.server.cors.origins,
|
||||||
|
methods: config.server.cors.methods,
|
||||||
|
allowedHeaders: config.server.cors.allowedHeaders,
|
||||||
|
credentials: config.server.cors.credentials
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Enregistre toutes les routes
|
||||||
|
router.use(projectRoutes);
|
||||||
|
router.use(measurementRoutes);
|
||||||
|
router.use(videoRoutes);
|
||||||
|
router.use(imageRoutes);
|
||||||
|
router.use(cameraRoutes);
|
||||||
|
|
||||||
|
// Route de test/santé de l'API
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', uptime: process.uptime() });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
136
src/routes/measurementRoutes.js
Normal file
136
src/routes/measurementRoutes.js
Normal 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
211
src/routes/projectRoutes.js
Normal 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
233
src/routes/videoRoutes.js
Normal 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;
|
||||||
209
src/services/storageService.js
Normal file
209
src/services/storageService.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
// src/services/storageService.js
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion du stockage des fichiers
|
||||||
|
*/
|
||||||
|
class StorageService {
|
||||||
|
/**
|
||||||
|
* Crée un dossier s'il n'existe pas déjà
|
||||||
|
* @param {string} dirPath - Chemin du dossier à créer
|
||||||
|
* @returns {Promise<string>} Chemin du dossier créé
|
||||||
|
*/
|
||||||
|
static async createDirectory(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un dossier et son contenu
|
||||||
|
* @param {string} dirPath - Chemin du dossier à supprimer
|
||||||
|
*/
|
||||||
|
static async deleteDirectory(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
await fs.rm(dirPath, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cherche toutes les images dans un dossier
|
||||||
|
* @param {string} dirPath - Dossier à scanner
|
||||||
|
* @returns {Promise<Array<string>>} Liste des chemins d'images trouvées
|
||||||
|
*/
|
||||||
|
static async scanImages(dirPath = 'storage') {
|
||||||
|
const basePath = path.join(config.paths.storage, dirPath);
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(basePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
await fs.mkdir(basePath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanDirectory(directory) {
|
||||||
|
const files = await fs.readdir(directory);
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(directory, file);
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await scanDirectory(filePath);
|
||||||
|
} else if (file.endsWith('.jpg')) {
|
||||||
|
results.push(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await scanDirectory(basePath);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre un contenu dans un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier
|
||||||
|
* @param {Buffer} content - Contenu à enregistrer
|
||||||
|
*/
|
||||||
|
static async saveFile(filePath, content) {
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
await this.createDirectory(dirPath);
|
||||||
|
if (Buffer.isBuffer(content)) {
|
||||||
|
await fs.writeFile(filePath, content);
|
||||||
|
} else {
|
||||||
|
throw new Error('Le contenu doit être un buffer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le contenu d'un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier
|
||||||
|
* @returns {Promise<Buffer>} Contenu du fichier
|
||||||
|
*/
|
||||||
|
static async getFile(filePath) {
|
||||||
|
return await fs.readFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime un fichier
|
||||||
|
* @param {string} filePath - Chemin du fichier à supprimer
|
||||||
|
* @returns {Promise<string>} Message de confirmation
|
||||||
|
*/
|
||||||
|
static async deleteFile(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
await fs.rm(filePath);
|
||||||
|
return `Fichier ${filePath} supprimé avec succès.`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return `Fichier ${filePath} inexistant.`;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de projet
|
||||||
|
*/
|
||||||
|
static project = {
|
||||||
|
/**
|
||||||
|
* Crée le répertoire d'un projet
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
*/
|
||||||
|
createProjectDirectory: async function(projectId) {
|
||||||
|
const projectPath = path.join(config.paths.storage, `${projectId}`);
|
||||||
|
await StorageService.createDirectory(projectPath);
|
||||||
|
await StorageService.createDirectory(path.join(projectPath, 'images'));
|
||||||
|
await StorageService.createDirectory(path.join(projectPath, 'videos'));
|
||||||
|
console.log(`[STORAGE] Répertoire créé : ${projectPath}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime le répertoire d'un projet et son contenu
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
*/
|
||||||
|
deleteProjectDirectory: async function(projectId) {
|
||||||
|
const projectPath = path.join(config.paths.storage, `${projectId}`);
|
||||||
|
await StorageService.deleteDirectory(projectPath);
|
||||||
|
console.log(`[STORAGE] Répertoire supprimé : ${projectPath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de mesures (images)
|
||||||
|
*/
|
||||||
|
static measurement = {
|
||||||
|
/**
|
||||||
|
* Récupère l'image d'une mesure
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<Buffer>} Contenu de l'image
|
||||||
|
*/
|
||||||
|
getMeasurementImage: async function(projectId, orderId) {
|
||||||
|
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
|
||||||
|
console.log(`[STORAGE] Récupération de l'image : ${imagePath}`);
|
||||||
|
return await StorageService.getFile(imagePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre l'image d'une mesure
|
||||||
|
* @param {Object} image - Objet image avec buffer
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} orderId - ID d'ordre de la mesure
|
||||||
|
* @returns {Promise<string>} Chemin de l'image enregistrée
|
||||||
|
*/
|
||||||
|
uploadMeasurementImage: async function(image, projectId, orderId) {
|
||||||
|
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
|
||||||
|
console.log(`[STORAGE] Enregistrement de l'image : ${imagePath}`);
|
||||||
|
await StorageService.saveFile(imagePath, image.buffer);
|
||||||
|
return imagePath;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire pour les opérations de vidéos
|
||||||
|
*/
|
||||||
|
static video = {
|
||||||
|
/**
|
||||||
|
* Récupère une vidéo
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {number} videoId - ID de la vidéo
|
||||||
|
* @returns {Promise<Buffer>} Contenu de la vidéo
|
||||||
|
*/
|
||||||
|
getVideo: async function(projectId, videoId) {
|
||||||
|
const videoPath = path.join(config.paths.storage, `${projectId}`, 'videos', `${videoId}.mp4`);
|
||||||
|
console.log(`[STORAGE] Récupération de la vidéo : ${videoPath}`);
|
||||||
|
return await StorageService.getFile(videoPath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une vidéo
|
||||||
|
* @param {string} videoPath - Chemin de la vidéo à supprimer
|
||||||
|
* @returns {Promise<string>} Message de confirmation
|
||||||
|
*/
|
||||||
|
deleteVideo: async function(videoPath) {
|
||||||
|
console.log(`[STORAGE] Suppression de la vidéo : ${videoPath}`);
|
||||||
|
return await StorageService.deleteFile(videoPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StorageService;
|
||||||
218
src/services/videoService.js
Normal file
218
src/services/videoService.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// src/services/videoService.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const config = require('../config');
|
||||||
|
const Video = require('../models/Video');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des opérations vidéo
|
||||||
|
*/
|
||||||
|
class VideoService {
|
||||||
|
/**
|
||||||
|
* Crée une vidéo à partir d'une liste d'images
|
||||||
|
* @param {number} projectId - ID du projet
|
||||||
|
* @param {Array<string>} pathList - Liste des chemins d'images
|
||||||
|
* @param {number} duration - Durée souhaitée en secondes
|
||||||
|
* @param {number} videoId - ID de la vidéo
|
||||||
|
* @param {number} resWidth - Largeur de la résolution
|
||||||
|
* @param {number} resHeight - Hauteur de la résolution
|
||||||
|
* @returns {Promise<string>} Chemin du fichier vidéo créé
|
||||||
|
*/
|
||||||
|
static async createVideoFromImages(projectId, pathList, duration, videoId, resWidth, resHeight) {
|
||||||
|
const tempFile = path.join('temp.txt');
|
||||||
|
let ffmpegProcess;
|
||||||
|
let cleanupDone = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Configuration des chemins
|
||||||
|
const workdir = path.join(config.paths.storage, projectId.toString());
|
||||||
|
if (!fs.existsSync(workdir)) {
|
||||||
|
fs.mkdirSync(workdir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifie que des images ont été fournies
|
||||||
|
if (!Array.isArray(pathList) || pathList.length === 0) {
|
||||||
|
throw new Error('Liste d\'images vide ou invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tri des images par ordre numérique
|
||||||
|
const sortedImages = pathList.sort((a, b) => {
|
||||||
|
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
|
||||||
|
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
|
||||||
|
return numA - numB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Création du fichier temporaire pour FFmpeg
|
||||||
|
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
|
||||||
|
|
||||||
|
// Calcul des paramètres vidéo
|
||||||
|
const totalFrames = sortedImages.length;
|
||||||
|
const frameRate = Math.ceil(totalFrames / parseInt(duration));
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0];
|
||||||
|
const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0];
|
||||||
|
const outputVideo = path.join(
|
||||||
|
workdir,
|
||||||
|
`${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mise à jour initiale du statut vidéo
|
||||||
|
await Video.updateVideo(videoId, {
|
||||||
|
status: config.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
58
src/utils/errorHandler.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// src/utils/errorHandler.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie une réponse d'erreur standardisée
|
||||||
|
* @param {string} message - Message d'erreur à afficher
|
||||||
|
* @param {Object} res - Objet Response d'Express
|
||||||
|
* @param {Error|null} error - Objet d'erreur original (facultatif)
|
||||||
|
* @param {number} statusCode - Code HTTP d'erreur (défaut: 500)
|
||||||
|
*/
|
||||||
|
function sendError(message, res, error = null, statusCode = 500) {
|
||||||
|
console.error(`[ERROR] ${message}`, error);
|
||||||
|
|
||||||
|
res.status(statusCode).json({
|
||||||
|
error: {
|
||||||
|
message,
|
||||||
|
statusCode,
|
||||||
|
details: error ? (error.message || String(error)) : null,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper pour les contrôleurs qui gère automatiquement les erreurs
|
||||||
|
* @param {Function} controller - Fonction de contrôleur à exécuter
|
||||||
|
* @returns {Function} - Middleware Express qui gère les erreurs
|
||||||
|
*/
|
||||||
|
function asyncHandler(controller) {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await controller(req, res, next);
|
||||||
|
} catch (error) {
|
||||||
|
sendError('Une erreur est survenue lors du traitement de la requête', res, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un wrapper pour les opérations de base de données qui gère les erreurs
|
||||||
|
* @param {Function} operation - Fonction à exécuter
|
||||||
|
* @returns {Function} - Fonction qui execute l'opération et gère les erreurs
|
||||||
|
*/
|
||||||
|
function wrapDatabaseOperation(operation) {
|
||||||
|
return async (...args) => {
|
||||||
|
try {
|
||||||
|
return await operation(...args);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[DB ERROR] Erreur lors de l'opération ${operation.name || 'database'}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendError,
|
||||||
|
asyncHandler,
|
||||||
|
wrapDatabaseOperation
|
||||||
|
};
|
||||||
@@ -1,111 +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 { execSync } = require('child_process');
|
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||||
|
*/
|
||||||
|
|
||||||
const serverError = require('../../utils/serverError');
|
const VideoService = require('../services/videoService');
|
||||||
const db = require('../../db');
|
|
||||||
const storageManager = require('../data/storageManager');
|
|
||||||
const measureManager = require('../measure/measureManager');
|
|
||||||
|
|
||||||
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) {
|
return await VideoService.createVideoFromImages(projectId, pathList, duration, videoId, res_width, res_height);
|
||||||
//pathList étant la liste des chemins déjà triés
|
|
||||||
const tempFile = path.join('temp.txt');
|
|
||||||
try {
|
|
||||||
// Trouver tous les fichiers image pour le projet donné
|
|
||||||
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images');
|
|
||||||
console.log('dir:', dir);
|
|
||||||
const images = pathList;
|
|
||||||
console.log('images:', images);
|
|
||||||
|
|
||||||
// Trier les images numériquement
|
|
||||||
const sortedImages = images.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// En déduire l'id de la première et dernière image utilisée
|
|
||||||
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
|
|
||||||
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
|
|
||||||
|
|
||||||
console.log('firstImageId:', firstImageId);
|
|
||||||
console.log('lastImageId:', lastImageId);
|
|
||||||
|
|
||||||
// Créer un fichier temporaire pour la liste des images
|
|
||||||
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
|
|
||||||
|
|
||||||
const frameRate = 10;
|
|
||||||
|
|
||||||
// le fichier final prend cette forme : {projectId}_{firstImageId}_{lastImageId}-{timestamp}.mp4
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const outputVideo = path.join(workdir, `${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`);
|
|
||||||
|
|
||||||
// Commande ffmpeg pour créer la vidéo
|
|
||||||
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
|
|
||||||
console.log('Running ffmpeg command:', ffmpegCommand);
|
|
||||||
execSync(ffmpegCommand);
|
|
||||||
console.log('Video created successfully:', outputVideo);
|
|
||||||
return outputVideo;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating video:', error);
|
|
||||||
serverError(error);
|
|
||||||
} finally {
|
|
||||||
// Supprimer le fichier temporaire
|
|
||||||
if (fs.existsSync(tempFile)) {
|
|
||||||
fs.unlinkSync(tempFile);
|
|
||||||
console.log('Temporary file deleted:', tempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createVideo(projectId) {
|
module.exports = { createVideoWithList };
|
||||||
const tempFile = path.join('temp.txt');
|
|
||||||
try {
|
|
||||||
// Trouver tous les fichiers image pour le projet donné
|
|
||||||
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
|
|
||||||
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images');
|
|
||||||
console.log('dir:', dir);
|
|
||||||
const images = storageManager.scanAllImages(dir);
|
|
||||||
console.log('images:', images);
|
|
||||||
|
|
||||||
// Trier les images numériquement
|
|
||||||
const sortedImages = images.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// En déduire l'id de la première et dernière image utilisée
|
|
||||||
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
|
|
||||||
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
|
|
||||||
|
|
||||||
console.log('firstImageId:', firstImageId);
|
|
||||||
console.log('lastImageId:', lastImageId);
|
|
||||||
|
|
||||||
// Créer un fichier temporaire pour la liste des images
|
|
||||||
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
|
|
||||||
|
|
||||||
const frameRate = 10;
|
|
||||||
const outputVideo = path.join(workdir, 'video.mp4');
|
|
||||||
|
|
||||||
// Commande ffmpeg pour créer la vidéo
|
|
||||||
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
|
|
||||||
console.log('Running ffmpeg command:', ffmpegCommand);
|
|
||||||
execSync(ffmpegCommand);
|
|
||||||
console.log('Video created successfully:', outputVideo);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating video:', error);
|
|
||||||
serverError(error);
|
|
||||||
} finally {
|
|
||||||
// Supprimer le fichier temporaire
|
|
||||||
if (fs.existsSync(tempFile)) {
|
|
||||||
fs.unlinkSync(tempFile);
|
|
||||||
console.log('Temporary file deleted:', tempFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { createVideo, createVideoWithList };
|
|
||||||
|
|||||||
50
stuff.md
50
stuff.md
@@ -1,29 +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
|
||||||
@@ -1,51 +1,14 @@
|
|||||||
const storageManager = require('../src/data/storageManager');
|
import path from 'path';
|
||||||
const videoManager = require('../src/video/videoManager');
|
import { fileURLToPath } from 'url';
|
||||||
const measureManager = require('../src/measure/measureManager');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// console.log('Testing database functions...');
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
try {
|
|
||||||
storageManager.createFolder('test_folder');
|
|
||||||
console.log('1 - Folder created');
|
|
||||||
storageManager.deleteFolder('test_folder');
|
|
||||||
console.log('2 - Folder deleted');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error testing database functions:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSmileImage() {
|
function getSmileImage() {
|
||||||
return path.join(__dirname, '../sample/smile.png');
|
return path.join(__dirname, '../sample/smile.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
//test de lancement d'une création de vidéo sur le projet 1
|
function getCatVideo() {
|
||||||
// videoManager.createVideo(1).then(res => {
|
return path.join(__dirname, '../sample/cat.mp4');
|
||||||
// console.log('3 - Video created:', res);
|
}
|
||||||
// }).catch(err => {
|
|
||||||
// console.error('Error creating video:', err);
|
|
||||||
// });
|
|
||||||
// async function run() {
|
|
||||||
// var Path = await measureManager.getPathFromIds(1, 1);
|
|
||||||
// console.log(Path);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// run().catch(err => {
|
export { getSmileImage, getCatVideo };
|
||||||
// console.error('Error:', err);
|
|
||||||
// });
|
|
||||||
|
|
||||||
var pathList = [
|
|
||||||
'storage/1/images/1.jpg',
|
|
||||||
'storage/1/images/10.jpg',
|
|
||||||
'storage/1/images/20.jpg',
|
|
||||||
'storage/1/images/30.jpg',
|
|
||||||
];
|
|
||||||
videoManager.createVideoWithList(1, pathList).then(res => {
|
|
||||||
console.log('3 - Video created:', res);
|
|
||||||
return storageManager.deleteFile(res);
|
|
||||||
}).then(res => {
|
|
||||||
console.log('4 - Video deleted:', res);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
exports.getSmileImage = getSmileImage;
|
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
function sendError(comment, res, err) {
|
function sendError(comment, res = { status: () => ({ json: () => {} }) }, err = null, code = 500) {
|
||||||
console.error(comment, err);
|
console.error(comment, err);
|
||||||
res.status(500).send('Server error');
|
res.status(code).json({
|
||||||
|
error: {
|
||||||
|
message: comment,
|
||||||
|
code: code,
|
||||||
|
error: err
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user