Compare commits
232 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 | |||
| efaa49912e | |||
| bd9a9b70a1 | |||
| cbb18e0ca2 | |||
| a80193dadc | |||
| 33b55e0dc0 | |||
| f56c35c5f9 | |||
| 66d51f24d9 | |||
| 042ea5cc50 | |||
| 08fa489f4c | |||
| 83b7f14778 | |||
| 3f34fdeef3 | |||
| 4df3dae224 |
@@ -1,45 +1,22 @@
|
||||
name: SSH Backend Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Déclenche l'action pour la branche principale
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ssh-connect:
|
||||
runs-on: ubuntu-latest # Utilisation de l'image Ubuntu pour l'environnement de job
|
||||
ssh-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Étape 1: Setup SSH
|
||||
- name: Setup SSH and Add Private Key
|
||||
- name: Write SSH Key
|
||||
run: |
|
||||
# Créez un dossier pour stocker les clés SSH
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > id_rsa
|
||||
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
|
||||
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
|
||||
- name: Run SSH Deploy Script
|
||||
run: |
|
||||
# Testez la connexion SSH avec l'hôte distant
|
||||
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
|
||||
ssh -i id_rsa -o StrictHostKeyChecking=no ${{ vars.SSH_USER }}@${{ vars.SSH_HOST }} "cd /root/timelapse-backend && ./deploy.sh"
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
node_modules/
|
||||
info.log
|
||||
storage/
|
||||
uploads/
|
||||
uploads/
|
||||
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 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({
|
||||
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type'],
|
||||
credentials: true,
|
||||
}));
|
||||
// Importe toutes les routes depuis notre nouvelle structure
|
||||
const apiRoutes = require('./src/routes');
|
||||
|
||||
router.use('/', projectRoutes);
|
||||
router.use('/', measurementRoutes);
|
||||
router.use('/', videoRoutes);
|
||||
router.use('/', imageRoutes);
|
||||
router.use('/', uploadRoutes);
|
||||
// Utilise directement toutes les routes définies dans src/routes/index.js
|
||||
router.use('/', apiRoutes);
|
||||
|
||||
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 = true;
|
||||
// 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'
|
||||
});
|
||||
const db = require('./src/database/connection');
|
||||
|
||||
function connectWithRetry() {
|
||||
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('Connecté à la base de données PostgreSQL.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectWithRetry();
|
||||
module.exports = client;
|
||||
module.exports = db;
|
||||
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');
|
||||
|
||||
exec('ffmpeg -version', (error, stdout, stderr) => {
|
||||
exec('ffmpeg -version', (error) => {
|
||||
if (error) {
|
||||
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) {
|
||||
console.error(`Error installing FFmpeg: ${installError}`);
|
||||
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",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
"dev": "nodemon server.js",
|
||||
"local" : "nodemon server_local.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -16,11 +17,16 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.11.3",
|
||||
"pg": "^8.13.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"sharp": "^0.33.5",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.23.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"eslint": "^9.23.0",
|
||||
"globals": "^16.0.0",
|
||||
"nodemon": "^3.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +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: Smile image retrieved successfully
|
||||
* 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 ID and order ID
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: projectId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The ID of the project
|
||||
* - in: path
|
||||
* name: orderId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The ID of the order
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Image retrieved successfully
|
||||
* content:
|
||||
* image/jpeg:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* 404:
|
||||
* description: Image not found
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
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,240 +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:
|
||||
* $ref: '#/components/schemas/Measurement'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Measurement'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Measurement'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Project'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Project'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Video'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Measurement'
|
||||
* 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.
|
||||
* consumes:
|
||||
* - multipart/form-data
|
||||
* parameters:
|
||||
* - in: formData
|
||||
* name: image
|
||||
* type: file
|
||||
* description: Fichier image à télécharger
|
||||
* - in: formData
|
||||
* name: projectId
|
||||
* type: integer
|
||||
* description: ID du projet
|
||||
* - in: formData
|
||||
* name: 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.
|
||||
* consumes:
|
||||
* - multipart/form-data
|
||||
* parameters:
|
||||
* - in: formData
|
||||
* name: image
|
||||
* type: file
|
||||
* description: Fichier image à télécharger
|
||||
* - in: formData
|
||||
* name: projectId
|
||||
* type: integer
|
||||
* description: ID du projet
|
||||
* - in: formData
|
||||
* name: timestamp
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: Horodatage de la mesure
|
||||
* - in: formData
|
||||
* name: temperature
|
||||
* type: number
|
||||
* description: Température mesurée
|
||||
* - in: formData
|
||||
* name: 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,183 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const serverError = require('../utils/serverError');
|
||||
|
||||
/**
|
||||
* @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:
|
||||
* $ref: '#/components/schemas/Video'
|
||||
* 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:
|
||||
* $ref: '#/components/schemas/Video'
|
||||
* 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.
82
server.js
82
server.js
@@ -1,47 +1,31 @@
|
||||
// server.js
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const cors = require('cors');
|
||||
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
|
||||
app.use(express.json());
|
||||
|
||||
// Cors accès à tout
|
||||
app.use(cors({
|
||||
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type'],
|
||||
credentials: true,
|
||||
}));
|
||||
// Configuration CORS
|
||||
app.use(cors(config.server.cors));
|
||||
|
||||
// Importer les routes
|
||||
const apiRoutes = require('./api');
|
||||
// Initialisation de la base de données
|
||||
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);
|
||||
|
||||
// Swagger dependencies
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerJsdoc = require('swagger-jsdoc');
|
||||
|
||||
// Configuration de Swagger
|
||||
const swaggerOptions = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'API Documentation',
|
||||
version: '1.0.0',
|
||||
description: 'Documentation de l\'API avec Swagger',
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'https://timelapse.kerboul.me/api',
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:3000/api',
|
||||
}
|
||||
],
|
||||
},
|
||||
apis: ['./routes/*.js'], // Prend en compte tous les fichiers de routes pour générer la documentation
|
||||
definition: config.swagger.definition,
|
||||
apis: config.swagger.apis
|
||||
};
|
||||
|
||||
// Initialisation de swagger-jsdoc
|
||||
@@ -52,14 +36,36 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
|
||||
|
||||
// Route de base pour tester le serveur
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.send('Bienvenue sur mon API Node.js!');
|
||||
res.send('Bienvenue sur l\'API Timelapse!');
|
||||
});
|
||||
|
||||
// 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
|
||||
const port = config.server.port;
|
||||
app.listen(port, () => {
|
||||
console.log(`Serveur démarré sur http://localhost:${port}`);
|
||||
console.log(`Swagger documentation disponible sur http://localhost:${port}/api-docs`);
|
||||
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
|
||||
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,33 +1,34 @@
|
||||
import db from '../../db.js';
|
||||
import path from 'path';
|
||||
import storageManager from '../data/storageManager.js';
|
||||
import fs from 'fs';
|
||||
const db = require('../../db.js');
|
||||
const storage_manager = require('./storage_manager.js');
|
||||
const fs = require('fs');
|
||||
|
||||
let localCounter = 0;
|
||||
|
||||
async function checkAndRemoveInvalidEntries() {
|
||||
console.log('Checking for invalid entries...');
|
||||
localCounter = 0;
|
||||
try {
|
||||
const measurementsRes = await db.query('SELECT id, path FROM measurements');
|
||||
//console.log('Fetched measurements:', measurementsRes.rows);
|
||||
for (const row of measurementsRes.rows) {
|
||||
//console.log('Checking file path:', row.path);
|
||||
if (!fs.existsSync(row.path)) {
|
||||
// Remove invalid entry
|
||||
await db.query('DELETE FROM measurements WHERE id = $1', [row.id]);
|
||||
console.log(`Deleted invalid measurement entry with id: ${row.id}`);
|
||||
localCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all images in storage
|
||||
const allImages = await storageManager.scanAllImages();
|
||||
//console.log('Scanned all images:', allImages);
|
||||
const allImages = await storage_manager.scanAllImages();
|
||||
for (const imagePath of allImages) {
|
||||
const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]);
|
||||
if (entryRes.rows.length === 0) {
|
||||
// Remove the file if the entry does not exist
|
||||
fs.unlinkSync(imagePath);
|
||||
console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`);
|
||||
localCounter++;
|
||||
}
|
||||
}
|
||||
if (localCounter > 0) {
|
||||
console.log(`[INFO] ${localCounter} entrées ont été modifiées`);
|
||||
localCounter = 0; // Reset the counter after logging
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking and removing invalid entries:', err);
|
||||
}
|
||||
@@ -36,7 +37,8 @@ async function checkAndRemoveInvalidEntries() {
|
||||
|
||||
|
||||
// Run the check periodically
|
||||
setInterval(checkAndRemoveInvalidEntries, 10000); // Every second
|
||||
console.log('[INFO] Activation du FileWatcher pour surveiller les fichiers invalides...')
|
||||
setInterval(checkAndRemoveInvalidEntries, 1000); // Every 10 seconds
|
||||
|
||||
// Initial run
|
||||
checkAndRemoveInvalidEntries();
|
||||
@@ -1,71 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PROJECTS_DIR = path.join('.');
|
||||
|
||||
function createFolder(name){
|
||||
const projectDir = path.join(PROJECTS_DIR, `${name}`);
|
||||
if (!fs.existsSync(projectDir)) {
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
}
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
function deleteFolder(name){
|
||||
const projectDir = path.join(PROJECTS_DIR, `${name
|
||||
}`);
|
||||
if (fs.existsSync
|
||||
(projectDir)) {
|
||||
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function scanAllImages(dir = 'storage') {
|
||||
const projectDir = path.join(PROJECTS_DIR, dir);
|
||||
let results = [];
|
||||
|
||||
function scanDirectory(directory) {
|
||||
const files = fs.readdirSync(directory);
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(directory, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
scanDirectory(filePath);
|
||||
} else if (file.endsWith('.jpg')) {
|
||||
results.push(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scanDirectory(projectDir);
|
||||
return results;
|
||||
}
|
||||
|
||||
function saveFile(filePath, content) {
|
||||
// Ensure content is a buffer
|
||||
if (Buffer.isBuffer(content)) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
} else {
|
||||
throw new Error('Content must be a buffer');
|
||||
}
|
||||
}
|
||||
|
||||
function getFile(name){
|
||||
const filePath = path.join(PROJECTS_DIR, `${name}`);
|
||||
return fs
|
||||
.readFileSync(filePath);
|
||||
}
|
||||
|
||||
function deleteFile(name){
|
||||
const filePath = path.join(PROJECTS_DIR, `${name}`);
|
||||
if (fs.existsSync(filePath))
|
||||
fs.rmSync(filePath);
|
||||
}
|
||||
|
||||
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,87 +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);
|
||||
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
async function getMeasureImage(projectId, orderId) {
|
||||
const projectPath = `${projectId}`;
|
||||
const imagePath = `${projectPath}/${orderId}.jpg`;
|
||||
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);
|
||||
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];
|
||||
}
|
||||
|
||||
export {
|
||||
uploadMeasureImage,
|
||||
addMeasureToProject,
|
||||
getNextOrderId,
|
||||
getMeasurements,
|
||||
getMeasurement,
|
||||
updateMeasurement,
|
||||
deleteMeasurement,
|
||||
getMeasureImage,
|
||||
getMeasurementById,
|
||||
updateMeasurementById
|
||||
}
|
||||
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,74 +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`);
|
||||
}
|
||||
|
||||
function deleteProjectDirectory(projectId) {
|
||||
const projectPath = `${projectId}`;
|
||||
storageManager.deleteFolder(projectPath);
|
||||
}
|
||||
|
||||
async function getAllProjects() {
|
||||
const query = 'SELECT * FROM public.projects';
|
||||
const res = await db.query(query);
|
||||
console.log('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);
|
||||
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);
|
||||
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);
|
||||
return res.rows[0];
|
||||
}
|
||||
|
||||
async function deleteProjectById(projectId) {
|
||||
const query = 'DELETE FROM public.projects WHERE id = $1';
|
||||
const values = [projectId];
|
||||
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);
|
||||
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);
|
||||
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
|
||||
};
|
||||
13
src/video/videoManager.js
Normal file
13
src/video/videoManager.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Ce fichier est conservé pour la rétrocompatibilité mais redirige vers le nouveau service vidéo.
|
||||
* Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
|
||||
*/
|
||||
|
||||
const VideoService = require('../services/videoService');
|
||||
|
||||
// Fonction de pont pour maintenir la compatibilité avec l'ancien code
|
||||
async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) {
|
||||
return await VideoService.createVideoFromImages(projectId, pathList, duration, videoId, res_width, res_height);
|
||||
}
|
||||
|
||||
module.exports = { createVideoWithList };
|
||||
50
stuff.md
50
stuff.md
@@ -1,29 +1,31 @@
|
||||
Routes :
|
||||
Workflow Caméra
|
||||
|
||||
- /projects = liste des projets
|
||||
- /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
|
||||
Côté Caméra
|
||||
|
||||
/camera/status // récupérer le statut de la caméra (GET)
|
||||
|
||||
- /measurements = liste des mesures
|
||||
- /measurements/:id = détail d'une mesure
|
||||
- /measurements/:id/edit = édition d'une mesure
|
||||
- /measurements/:id/delete = suppression d'une mesure
|
||||
- /measurements/new = création d'une mesure
|
||||
si stop :
|
||||
/camera/stop // arrêter la caméra (POST)
|
||||
|
||||
- /cameras = liste des caméras
|
||||
- /cameras/:id = détail d'une caméra
|
||||
- /cameras/:id/edit = édition d'une caméra
|
||||
- /cameras/:id/delete = suppression d'une caméra
|
||||
- /cameras/new = création d'une caméra
|
||||
si upload :
|
||||
/camera/upload // uploader la vidéo (POST)
|
||||
|
||||
- /data/image/:id = image depuis le pool de stockage
|
||||
- /data/video/:id = vidéo depuis le pool de stockage
|
||||
Côté Backend
|
||||
|
||||
/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,19 +1,14 @@
|
||||
const storageManager = require('../src/data/storageManager');
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
console.log('Testing database functions...');
|
||||
|
||||
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);
|
||||
}
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function getSmileImage() {
|
||||
return path.join(__dirname, '../sample/smile.png');
|
||||
}
|
||||
|
||||
exports.getSmileImage = getSmileImage;
|
||||
function getCatVideo() {
|
||||
return path.join(__dirname, '../sample/cat.mp4');
|
||||
}
|
||||
|
||||
export { getSmileImage, getCatVideo };
|
||||
@@ -1,6 +1,12 @@
|
||||
function sendError(comment, res, err) {
|
||||
function sendError(comment, res = { status: () => ({ json: () => {} }) }, err = null, code = 500) {
|
||||
console.error(comment, err);
|
||||
res.status(500).send('Server error');
|
||||
res.status(code).json({
|
||||
error: {
|
||||
message: comment,
|
||||
code: code,
|
||||
error: err
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const serverError = require('../utils/serverError');
|
||||
|
||||
async function createVideo(projectId) {
|
||||
const imageDir = `/storage/${projectId}`;
|
||||
const outputVideo = `/storage/videos/output_${projectId}_video.mp4`;
|
||||
const frameRate = 24;
|
||||
const tempFile = `/storage/${projectId}/temp_file.txt`;
|
||||
|
||||
try {
|
||||
const images = fs.readdirSync(imageDir).filter(file => file.endsWith('.jpg'));
|
||||
if (images.length === 0) {
|
||||
throw new Error('No images found for this project');
|
||||
}
|
||||
|
||||
const tempFileContent = images.map(img => `file '${path.join(imageDir, img)}'`).join('\n');
|
||||
fs.writeFileSync(tempFile, tempFileContent);
|
||||
|
||||
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
|
||||
execSync(ffmpegCommand);
|
||||
|
||||
fs.unlinkSync(tempFile);
|
||||
return { message: 'Video created successfully', videoPath: outputVideo };
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating video: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createVideo };
|
||||
Reference in New Issue
Block a user