220 Commits
dev3 ... main

Author SHA1 Message Date
3df02703e8 feat(image): Mettre à jour la description de l'API pour détecter automatiquement le projet actif lors de l'enregistrement d'une image
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 16:15:01 +02:00
e6fd5b3a87 feat(image): Supprimer la nécessité de l'ID du projet lors du téléchargement d'une image, en utilisant le projet actif
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 15:06:54 +02:00
3d65ccb7fc fix(routes): Mettre à jour les descriptions des statuts des projets et des vidéos pour plus de clarté
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 14:51:40 +02:00
aa7f901442 feat(camera): Ajouter un log pour afficher l'identifiant du projet lors du démarrage de la procédure
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 12:41:34 +02:00
8480686fd4 refactor(video): Mettre à jour les statuts vidéo pour utiliser la nouvelle configuration des statuts
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 11s
2025-04-27 12:26:34 +02:00
98128253d9 feat(camera): Améliorer la gestion de l'arrêt de la caméra en ajoutant la recherche de projets en cours d'arrêt et en mettant à jour les statuts appropriés
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:54:29 +02:00
11c8951b6f fix(camera): Renommer la propriété 'active' en 'idle' dans les paramètres de la caméra et mettre à jour la documentation des routes de projet
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:47:58 +02:00
4427e6dde0 feat(camera): Ajouter le statut 'stopping' pour gérer l'arrêt des projets et mettre à jour la documentation des routes
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:41:58 +02:00
2533eacf5e fix(camera): Mettre à jour le statut du projet en 'idle' lors de l'arrêt de la caméra
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:36:15 +02:00
98bb822673 fix(camera): Renommer la propriété 'active' en 'idle' pour clarifier l'état de la caméra
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 11:34:39 +02:00
fde6a0454c feat(status): Refactor project and video status management with centralized configuration and update related controllers and routes
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 11:21:36 +02:00
65fa693986 feat(api): Améliorer la documentation Swagger pour les routes de l'API, ajout de descriptions détaillées et de schémas pour les entités Project, Measurement, Video, Camera, et Error.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 10s
2025-04-27 01:20:27 +02:00
1890051a0f Remove deprecated routes and functionalities related to camera, image, measurement, project, upload, and video management. This includes the deletion of route handlers and associated logic to streamline the codebase and improve maintainability.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 01:11:47 +02:00
d8b2cf63a3 fix(Dockerfile): Corriger la commande de démarrage pour utiliser server.js au lieu de backend.config.js
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 01:09:34 +02:00
4513af3aa0 feat(database): Implement DatabaseManager for managing database structure and initialization
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 1m51s
feat(routes): Add camera, image, measurement, project, and video routes with Swagger documentation

feat(services): Create storageService and videoService for file management and video processing

fix(errorHandler): Enhance error handling with standardized responses and database operation wrappers
2025-04-27 01:02:33 +02:00
792bdca965 Remplacer 'git checkout' par 'git reset --hard' pour une mise à jour plus précise du dépôt.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-27 00:11:07 +02:00
d55180e048 Améliorer la mise à jour du dépôt en remplaçant 'git pull' par 'git fetch' et 'git checkout' pour une gestion plus précise des branches.
Some checks failed
SSH Backend Deploy / ssh-deploy (push) Failing after 2s
2025-04-27 00:09:27 +02:00
32094d702b Mettre à jour la documentation Swagger pour l'upload de mesures avec image, en précisant les paramètres et les réponses.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-26 19:23:54 +02:00
83ac64262a Ajouter la documentation Swagger pour l'upload de mesures avec image
Some checks failed
SSH Backend Deploy / ssh-deploy (push) Failing after 0s
2025-04-26 18:59:24 +02:00
fd92aa067e Renommer le workflow en 'SSH Backend Deploy' et mettre à jour le script de déploiement pour exécuter './deploy.sh' au lieu d'un message d'accueil.
All checks were successful
SSH Backend Deploy / ssh-deploy (push) Successful in 9s
2025-04-24 00:27:36 +02:00
19bfde36a7 Ajouter des espaces pour améliorer la lisibilité dans la fonction de nettoyage du script de déploiement
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:23:34 +02:00
0277975cee Corriger la commande de démarrage des conteneurs Docker dans le script de déploiement pour utiliser 'docker compose' au lieu de 'docker-compose'.
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:22:33 +02:00
e7cb4582b0 Améliorer le script de déploiement en ajoutant des commandes pour construire et démarrer les conteneurs Docker avec gestion des erreurs.
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:21:12 +02:00
f5d73c5c3f Ajouter un script de déploiement avec gestion des erreurs et journalisation
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:20:11 +02:00
66dd0e0835 Mettre à jour les ports et les volumes dans docker-compose.yml pour une configuration locale
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-24 00:01:36 +02:00
9aedbdd127 Ajouter les informations d'identification du système Portainer dans le fichier stuff.md
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:59:13 +02:00
cb97bfb718 Supprimer le fichier server_local.js et ses dépendances
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:48:33 +02:00
2ce3eafb79 Refactor le workflow de déploiement et simplifie la connexion à la base de données PostgreSQL
All checks were successful
SSH Hello / ssh-hello (push) Successful in 2s
2025-04-23 23:47:36 +02:00
166bd53beb Actualiser db.js 2025-04-08 11:57:03 +00:00
1deb11d6aa revert b7715df51c
revert Actualiser db.js
2025-04-08 11:56:06 +00:00
b7715df51c Actualiser db.js 2025-04-08 11:44:50 +00:00
f3c8176733 Renommage du job de déploiement dans le workflow pour une meilleure clarté 2025-04-03 16:13:00 +02:00
4b6382cc98 Suppression des dépendances inutilisées dans le système de capture 2025-04-03 16:10:15 +02:00
45223bc670 Ajout de la gestion des projets dans les procédures de démarrage et d'arrêt de la caméra, incluant la vérification de l'état du projet en cours et la mise à jour du statut du projet. 2025-04-03 16:00:02 +02:00
70ec69ba84 Mise à jour des appels à la fonction edit_camera pour inclure l'identifiant de la caméra dans le système de capture 2025-04-03 15:41:04 +02:00
8961c366d3 Ajout de l'exportation du routeur dans le système de capture 2025-04-03 15:28:12 +02:00
09d756bf93 Ajout de la gestion du système de capture dans les routes de l'API 2025-04-03 15:26:50 +02:00
55cba1f3ea Ajout de la documentation Swagger pour les routes de gestion de la caméra, y compris l'état, le démarrage, l'arrêt et le mode de maintenance. 2025-04-03 15:25:49 +02:00
eea117bc70 Ajout de la gestion du système de capture, y compris l'initialisation de la caméra, les procédures de démarrage et d'arrêt, ainsi que la gestion des modes de maintenance. 2025-04-03 15:25:09 +02:00
7f1269bd2f Ajout de la gestion des paramètres de capture dans le système de caméra et mise à jour de la documentation associée. 2025-04-03 15:02:29 +02:00
e0fa309b21 Renommage de la fonction de suppression de projet pour une meilleure clarté 2025-04-03 14:50:39 +02:00
7d01ea28ce Suppression de la documentation Swagger pour l'ajout de mesures et conversion des routes de mesures en appels asynchrones. 2025-04-03 14:44:54 +02:00
265d1c5f18 Ajout de la gestion des routes pour le système de capture, mise à jour des chemins de fichiers et amélioration de la logique de gestion des mesures et vidéos. 2025-04-03 14:42:51 +02:00
cedd9949bd Correction de l'appel asynchrone dans la fonction get_path_from_id pour récupérer correctement le chemin à partir de l'ID du projet et de l'ordre. 2025-04-03 13:47:24 +02:00
f958e9d491 Ajout de logs pour le débogage dans la fonction get_path_from_id et suppression de logs redondants dans get_path_list 2025-04-03 13:45:08 +02:00
44d846b01c Ajout d'un log pour afficher la liste des chemins récupérés dans la fonction get_path_list 2025-04-03 13:41:25 +02:00
99fb5331ed Correction de la création de vidéos pour récupérer l'ID de la vidéo à partir de la réponse de la base de données. 2025-04-03 11:54:48 +02:00
4d1bfac99b Mise à jour de la route de création de vidéos pour utiliser le gestionnaire de base de données, ajout de la récupération des informations de vidéo et amélioration de la gestion des erreurs. 2025-04-03 11:52:19 +02:00
48b105be13 Refactor la gestion des vidéos en remplaçant le gestionnaire de vidéos par le gestionnaire de base de données. Ajout de la gestion des erreurs lors de la création et de la suppression des vidéos. 2025-04-03 11:45:36 +02:00
7b4a032249 Merge pull request 'Refactor des fonctions primaires' (#5) from refactor into main
Reviewed-on: https://gitea.kerboul.me/timelapse/timelapse-backend/pulls/5
2025-04-03 09:41:15 +00:00
f5fda050ed Supprimer l'importation du gestionnaire de stockage dans le gestionnaire de vidéos 2025-04-03 11:40:22 +02:00
401deb3e69 Ajout de la gestion de l'environnement de développement avec un fichier devlock, mise à jour des scripts de démarrage et création d'un serveur local pour le développement. 2025-04-03 11:38:21 +02:00
03ec179590 Ajout de la gestion des vidéos inachevées et mise à jour des fonctions de création et de mise à jour des vidéos dans le gestionnaire de base de données. 2025-04-03 11:27:11 +02:00
6077dfd716 Refactor la gestion des mesures en remplaçant le gestionnaire de mesures par le gestionnaire de stockage. Ajouter des fonctions pour gérer les images et les chemins des mesures. Améliorer la gestion des erreurs et nettoyer le code. 2025-04-03 11:03:10 +02:00
c3b2059428 Refactor le gestionnaire de stockage pour encapsuler les fonctions de création et de suppression de répertoires de projet dans un objet. Mettre à jour les routes pour utiliser la nouvelle structure. 2025-04-03 10:31:29 +02:00
0d0c101e20 Renommer le gestionnaire de stockage et mettre à jour les références dans les fichiers concernés. Supprimer les fichiers obsolètes et ajouter un nouveau fichier de gestion de stockage. 2025-04-03 10:29:17 +02:00
915146c140 Ajouter la gestion de la base de données avec des fonctions pour créer, vérifier et supprimer des tables. Mettre à jour la connexion à la base de données pour un environnement de développement. Améliorer l'initialisation de la caméra et corriger l'appel des fonctions asynchrones. 2025-04-03 09:54:29 +02:00
242bbcd597 Renommer la fonction de connexion à la base de données et améliorer la gestion des erreurs. Nettoyer le code en supprimant les commentaires inutiles et réinitialiser le compteur après la journalisation des modifications. 2025-04-03 09:07:45 +02:00
a33e517a8a Nettoyer le code en supprimant les importations inutilisées et en décommentant des fonctions pour améliorer la lisibilité. 2025-04-03 09:02:34 +02:00
ed853ab0f7 Rendre plusieurs fonctions asynchrones pour améliorer la gestion des erreurs et assurer un traitement correct des opérations liées à la caméra et aux projets. 2025-04-03 08:41:25 +02:00
12898d67c0 Activer le changement de statut du projet dans la fonction stopProcedure pour marquer le projet comme terminé. 2025-04-02 11:07:20 +02:00
4642c8cca6 Modifier la fonction resetProjectStatus pour changer le statut des projets de 1 à 2 et décommenter son appel dans stopProcedure. 2025-04-02 11:07:00 +02:00
daca488532 Rendre la fonction changeProjectStatus asynchrone pour améliorer la gestion des erreurs et l'intégrer dans la fonction stopProcedure. 2025-04-02 11:04:59 +02:00
3d00f6afbf Rendre la fonction stopProcedure asynchrone pour améliorer la gestion des erreurs lors de l'arrêt de la procédure. 2025-04-02 11:02:21 +02:00
15692a3fc8 Commenter la réinitialisation du statut du projet dans la fonction stopProcedure pour éviter des réinitialisations non souhaitées. 2025-04-02 11:02:04 +02:00
dd03db42a9 Rendre la fonction startProcedure asynchrone dans la route de démarrage de la procédure pour améliorer la gestion des erreurs. 2025-04-02 10:54:03 +02:00
a0b1eaf109 Modification de la fonction startProcedure pour la rendre asynchrone et améliorer la gestion de l'occupation de la caméra. 2025-04-02 10:52:17 +02:00
b65230d5e7 Ajout de la documentation Swagger pour les procédures de capture et restauration, et réintégration de la fonction de démarrage de la procédure avec gestion des erreurs. 2025-04-02 10:48:56 +02:00
55b4c04187 Modification de la fonction getCamera pour utiliser une exécution asynchrone et amélioration de la gestion des erreurs 2025-04-02 10:46:29 +02:00
61cdb25398 Modification de la fonction getCamera pour utiliser une exécution synchrone avec querySync 2025-04-02 10:42:00 +02:00
c4d62c473e Mise à jour de la requête de suppression des vidéos inachevées pour inclure les statuts 0 et 2 2025-04-02 10:40:16 +02:00
7dafdcecde Mise à jour du statut de la vidéo lors de la création d'un projet vidéo : ajout de nouveaux états pour refléter les différentes phases de création. 2025-04-02 10:39:32 +02:00
d1b75329ea Ajout de la fonction de nettoyage des fichiers vidéo non associés et appel de la fonction de suppression des vidéos inachevées 2025-04-02 10:34:27 +02:00
90e036b150 Supprimer les vidéos inachevées au démarrage du backend 2025-04-02 10:33:42 +02:00
aa9a21c638 Ajout de la vérification de l'occupation de la caméra et réinitialisation des statuts des projets lors du redémarrage du backend. Mise à jour des paramètres de la caméra et modification du statut des vidéos. 2025-04-02 10:28:59 +02:00
90ce92b90b Désactiver les logs d'information dans la fonction de vérification des entrées invalides 2025-04-02 09:56:35 +02:00
647dd72b5b Nettoyage des routes : suppression des anciennes définitions Swagger et des variables inutilisées dans cameraRoutes.js 2025-04-02 09:50:08 +02:00
73922d8afc Ajouter une route pour récupérer l'état actuel de la caméra 2025-04-02 09:28:11 +02:00
293245d457 Ajouter l'option de combinaison des logs dans la configuration du backend 2025-04-02 09:27:28 +02:00
368abfbeca Initialisation de la caméra avec des valeurs par défaut et ajout de la récupération de l'état de la caméra 2025-04-02 09:23:34 +02:00
38864a68d8 Supprimer l'ancienne route pour obtenir les procédures dans uploadRoutes.js 2025-03-31 11:47:20 +02:00
71cb9898bb Résolution de la gestion vidéo #3 2025-03-31 11:35:37 +02:00
9101497a7f Ajouter la prise en charge de la résolution personnalisée lors de la création de vidéos 2025-03-31 11:22:19 +02:00
6c48612554 Ajouter des options de redimensionnement et de bitrate pour la création de vidéos 2025-03-31 11:03:38 +02:00
bb51208d06 Merge branch 'main' of gitea.kerboul.me:timelapse/timelapse-backend 2025-03-31 10:48:08 +02:00
c2dcf3fa13 Simplifier la commande de démarrage dans le Dockerfile pour utiliser uniquement backend.config.js et modifier la réponse de création de vidéo pour inclure uniquement l'ID. 2025-03-31 10:47:56 +02:00
eb47639397 Afficher le corps de la requête lors du téléchargement de mesures 2025-03-31 08:26:06 +00:00
582fd87f32 Ajouter des journaux pour la suppression de vidéos et gérer le cas où aucun fichier vidéo n'est trouvé 2025-03-31 08:19:50 +00:00
2c9f81975f Modifier la commande de démarrage dans le Dockerfile pour utiliser backend.config.js avec PM2 2025-03-31 10:10:44 +02:00
5c7116af7a Modifier la commande de démarrage dans le Dockerfile pour utiliser un fichier de configuration PM2 et ajouter un fichier de configuration backend. 2025-03-31 10:08:39 +02:00
c91d11567c Corriger la syntaxe de la commande de démarrage dans le Dockerfile pour le délai de redémarrage. 2025-03-31 10:00:40 +02:00
5024859b6c Modifier la commande de démarrage dans le Dockerfile pour exécuter server.js avec PM2. 2025-03-31 09:58:14 +02:00
fda18fb1c6 Modifier la commande de démarrage de l'application pour utiliser PM2 avec une politique de redémarrage et ajuster le fichier docker-compose pour supprimer l'ancienne commande. 2025-03-31 09:55:04 +02:00
7536d98330 Installer pm2 globalement dans le Dockerfile pour la gestion des processus 2025-03-31 09:50:42 +02:00
fb1bdbd182 Ajouter une commande pour démarrer le serveur avec pm2-runtime, incluant la surveillance et un délai de redémarrage. 2025-03-31 09:48:04 +02:00
e745c78b25 Corriger une erreur de typographie dans la définition de la route pour récupérer les procédures. 2025-03-31 09:46:36 +02:00
8c35aab855 Ajouter une route pour récupérer les procédures, retournant un JSON avec les paramètres de la procédure. 2025-03-31 09:45:41 +02:00
559ef44cb3 Corriger la suppression de vidéo pour ne pas tenter de supprimer un fichier si le chemin est nul, améliorant ainsi la gestion des erreurs. 2025-03-31 09:43:52 +02:00
411ea7a904 Ajouter une vérification pour le chemin du fichier vidéo, en utilisant une vidéo de secours si le chemin est nul ou indéfini, améliorant ainsi la robustesse du service. 2025-03-31 09:41:43 +02:00
7942a025e8 Refactor la gestion des fichiers vidéo pour utiliser un chemin de vidéo par défaut en cas d'absence de fichier, améliorant ainsi la robustesse du service. 2025-03-31 09:39:30 +02:00
3849042869 Refactor la gestion des vidéos pour utiliser une vidéo de secours en cas d'absence de fichier, améliorant ainsi la robustesse du service. 2025-03-31 09:36:05 +02:00
6747062f0b Actualiser stuff.md
C'est pour lancer la pipeline
2025-03-31 06:22:44 +00:00
c93eed9d52 Refactor la route de prévisualisation d'image pour améliorer la gestion des erreurs et intégrer le redimensionnement d'image dans des fonctions séparées 2025-03-13 12:13:48 +01:00
884e312ef7 Refactor la fonction de prévisualisation d'image pour intégrer le redimensionnement directement dans la route, améliorant ainsi la lisibilité et la gestion des erreurs. 2025-03-13 12:12:38 +01:00
5ffa1ec839 Ajouter une fonction de prévisualisation d'image pour redimensionner et renvoyer une image JPEG 2025-03-13 12:10:21 +01:00
df219bfc06 Améliorer la création de vidéos en démarrant le rendu immédiatement et en ajoutant une réponse immédiate avec l'état de traitement. 2025-03-13 12:01:52 +01:00
c3e78b248f Ajouter une route pour récupérer la progression de la création de vidéos et améliorer la gestion des erreurs dans la fonction createVideoWithList 2025-03-13 11:50:31 +01:00
a069acfce7 Modifier le champ de la base de données pour mettre à jour le chemin du fichier vidéo dans la fonction createVideoWithList 2025-03-13 11:33:44 +01:00
c90ff42961 Refactor la création de vidéos pour utiliser des promesses et améliorer la gestion des erreurs, avec une réponse immédiate au démarrage du rendu. 2025-03-13 11:28:17 +01:00
2e552be9db Modifier la création de vidéos pour utiliser des promesses et gérer les erreurs lors de l'exécution de ffmpeg 2025-03-13 10:06:48 +01:00
4bda54b529 Ajouter une fonction pour vérifier si la caméra est occupée et mettre à jour la gestion des fichiers vidéo 2025-03-13 10:02:49 +01:00
d93b2c6b7c Aucune modification apportée au code 2025-03-13 09:37:43 +01:00
dab93cfdf9 Améliorer la gestion des erreurs en ajoutant un code d'état HTTP aux réponses d'erreur dans plusieurs routes et en modifiant la fonction sendError pour accepter un code d'état personnalisé. 2025-03-13 09:32:02 +01:00
62e8aee6bd Ajouter une fonction pour écrire les paramètres de capture dans un fichier camera.txt 2025-03-13 09:24:23 +01:00
6c77d267e6 Supprimer les tests et les commentaires obsolètes dans tester.js 2025-03-13 09:07:56 +01:00
9d9868e26b Supprimer la documentation Swagger obsolète dans plusieurs fichiers de routes 2025-03-13 09:06:38 +01:00
217f0b4fd3 Corriger la description de l'API pour la suppression d'un projet en précisant "par son ID" 2025-03-13 08:55:31 +01:00
d8f1d353c6 Déplacer la documentation Swagger pour la suppression de projet vers un fichier séparé et supprimer les commentaires obsolètes dans projectRoutes.js 2025-03-13 08:53:19 +01:00
30f05ffcbe Modifier l'importation de child_process pour utiliser execSync au lieu de spawn dans videoManager.js 2025-03-10 18:01:52 +01:00
ef90f77a11 Refactor la fonction createVideoWithList pour simplifier la création de vidéos et mettre à jour le statut de la vidéo à "completed" 2025-03-10 18:00:30 +01:00
e38718b1fa videoManager.js: add videoManager.js 2025-03-10 17:56:56 +01:00
f85cead1dd Commenter les logs de la base de données dans les fonctions de gestion des projets 2025-03-10 17:53:04 +01:00
3469c757ec Modifier la route de rendu vidéo pour utiliser GET, ajouter la gestion de l'accès au fichier vidéo et renvoyer le contenu vidéo en fonction de la plage demandée 2025-03-10 17:45:42 +01:00
9ec8ff73f3 Modifier la route de rendu vidéo pour utiliser POST, ajouter la gestion des erreurs et démarrer le processus de rendu vidéo 2025-03-10 17:42:32 +01:00
55697fc032 Modifier la fonction createVideoWithList pour inclure l'ID de la vidéo et mettre à jour le statut de la vidéo après le rendu 2025-03-10 17:36:39 +01:00
7baac5dcb7 Modifier la fonction createVideoWithList pour utiliser spawn au lieu de execSync pour l'exécution de ffmpeg en arrière-plan 2025-03-10 17:31:11 +01:00
848c50bf33 Ajouter la gestion des erreurs lors de l'exécution de la commande ffmpeg pour la création de vidéos 2025-03-10 17:25:59 +01:00
81c4470464 Démarrer la création de vidéo en arrière-plan pour améliorer la réactivité 2025-03-10 17:23:52 +01:00
29f198cd85 Ajouter la durée à la requête de sélection des vidéos lors du rendu 2025-03-10 17:22:07 +01:00
3d560cfb77 Render video débug 2025-03-10 17:19:02 +01:00
553a934563 Modifier la fonction createVideoWithList pour convertir la durée en entier avant de calculer le frame rate 2025-03-10 17:16:23 +01:00
9e850f0090 Modifier la fonction createVideoWithList pour accepter la durée et ajuster le frame rate en conséquence 2025-03-10 17:08:11 +01:00
e9fd9dfaa1 Modifier la route de téléchargement de vidéo pour gérer le streaming avec prise en charge des plages de fichiers 2025-03-10 16:55:59 +01:00
8f69705ae9 Modifier la route de téléchargement de vidéo pour vérifier l'existence du fichier avant de le télécharger 2025-03-10 16:54:13 +01:00
5979cded02 Modifier la route de téléchargement de vidéo pour utiliser un flux de fichiers et gérer les erreurs de streaming 2025-03-10 16:49:33 +01:00
37d82d1133 Modifier la taille de l'image redimensionnée à un septième de ses dimensions d'origine et ajuster la qualité JPEG 2025-03-10 16:41:50 +01:00
a15ebb0697 Modifier la route de création de vidéo pour utiliser async/await et retourner l'ID de la vidéo créée 2025-03-10 16:36:06 +01:00
d17c96479f Modifier la requête d'insertion pour retourner l'ID de la nouvelle vidéo créée 2025-03-10 16:34:53 +01:00
727c28d312 Modifier la taille de l'image redimensionnée à un cinquième de ses dimensions d'origine 2025-03-10 16:13:49 +01:00
d790626a1a Ajouter une route pour prévisualiser une image redimensionnée par ID de projet et ID de commande 2025-03-10 16:12:16 +01:00
9cd1b230fd Ajouter la bibliothèque sharp et configurer les volumes pour node_modules dans docker-compose 2025-03-10 16:07:49 +01:00
a6a2492842 Ajouter la bibliothèque sharp pour le traitement d'images 2025-03-10 16:01:47 +01:00
65fcf1fc68 Mettre à jour le statut du projet dans la base de données lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:25:08 +01:00
3bf001bb58 Modifier la signature de la fonction stopProcedure pour inclure l'objet req dans la route 2025-03-10 15:20:13 +01:00
961b72b24b Ajouter des logs pour afficher les anciens et nouveaux ID de projet et intervalles lors de l'arrêt de la procédure de capture 2025-03-10 15:18:57 +01:00
a39bb6e6c0 Ajouter la vérification d'un projet en cours et améliorer les messages de retour lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:15:26 +01:00
b696897cfc Commenter la vérification de l'intervalle maximum dans la procédure de capture 2025-03-10 15:13:09 +01:00
1457711d8f Ajouter la vérification de l'existence du projet et améliorer la gestion des erreurs dans la procédure de capture 2025-03-10 15:11:42 +01:00
e446724ecd Améliorer la gestion des erreurs lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:08:08 +01:00
557be4a58b Modifier la méthode HTTP de la route /procedure/stop/ de GET à POST 2025-03-10 15:04:22 +01:00
0c56fd79bc Déplacer la logique de capture de cameraRoutes.js vers imageRoutes.js et ajouter les routes pour démarrer et arrêter la procédure de capture 2025-03-10 15:02:50 +01:00
44d1d6a24e Déplacer la logique de capture de cameraRoutes.js vers imageRoutes.js 2025-03-10 14:55:32 +01:00
8319ae9685 Supprimer l'utilisation de cameraRoutes dans api.js 2025-03-10 14:54:21 +01:00
4807579846 Supprimer l'importation de cameraRoutes dans api.js 2025-03-10 14:53:34 +01:00
ac0bd807df Export Default Router 2025-03-10 14:52:26 +01:00
39a7b897bf Configurer Docker avec un Dockerfile et mettre à jour docker-compose.yml pour utiliser la construction d'image 2025-03-10 14:46:29 +01:00
7785bfa10f Corriger l'importation de la base de données dans cameraRoutes.js 2025-03-10 14:42:01 +01:00
23295f13d7 Réorganiser les importations dans api.js et cameraRoutes.js 2025-03-10 14:40:06 +01:00
fe884cb8e7 Corriger le chemin d'importation de la base de données dans cameraRoutes.js 2025-03-10 14:36:47 +01:00
8ffde922fa Ajouter les routes de caméra dans le fichier api.js 2025-03-10 14:34:58 +01:00
6b95665974 Ajouter l'importation de ffmpeg dans le fichier api.js 2025-03-10 14:30:01 +01:00
6ecd573751 Ajouter un log pour le chemin à partir des IDs de projet et de commande 2025-03-10 14:22:35 +01:00
f3ed511543 Ajouter des routes pour démarrer et arrêter la procédure de capture 2025-03-10 14:19:48 +01:00
348509fddb Ajouter une route pour récupérer une image par ID de mesure 2025-03-10 13:26:46 +01:00
98e74d22f2 Modifier le nom du conteneur pour l'environnement de développement de l'API Timelapse 2025-03-10 12:05:52 +01:00
51db325dad Modifier le nom du conteneur pour l'environnement de développement de l'API Timelapse 2025-03-10 12:02:38 +01:00
7c5041b5c4 Supprimer la version de Docker Compose dans le fichier de configuration 2025-03-10 11:23:46 +01:00
2ee5897426 Modifier l'adresse IP du serveur de base de données dans la configuration 2025-03-10 11:19:08 +01:00
6178e7cdbf Supprimer le fichier de configuration GitLab CI pour le déploiement de l'API Timelapse 2025-03-10 11:03:29 +01:00
a65fcf0c47 Ajouter un fichier docker-compose pour configurer l'environnement de développement de l'API Timelapse 2025-03-10 10:54:07 +01:00
a8494ad382 Supprimer la configuration locale de la connexion à la base de données et nettoyer le code de la route de téléchargement d'images 2025-02-12 14:22:16 +01:00
dcbf2a1f00 Modifier la gestion des erreurs lors de la création de vidéos et commenter le code de test associé 2025-02-12 14:04:28 +01:00
2450359710 Ajouter une route pour réinitialiser le statut d'une vidéo par ID et supprimer le fichier vidéo associé 2025-02-12 11:50:18 +01:00
7c342c3b69 Modifier la route de récupération de vidéo pour utiliser res.download au lieu de res.sendFile 2025-02-12 11:42:15 +01:00
3f5317ad18 Corriger la récupération du chemin vidéo en utilisant video_file au lieu de video_path 2025-02-12 11:40:42 +01:00
7652a1ea64 Ajouter un log pour afficher le chemin de la vidéo dans la route de récupération 2025-02-12 11:38:57 +01:00
41c877f072 Ajouter la mise à jour du statut de la vidéo après le rendu et améliorer la gestion des erreurs 2025-02-12 11:37:33 +01:00
f99b0c60ce Refactor la route de rendu vidéo pour utiliser async/await et améliorer la gestion des erreurs 2025-02-12 11:35:33 +01:00
aa571e5149 Améliorer la gestion des erreurs lors de la mise à jour du fichier vidéo dans la route de rendu 2025-02-12 11:33:16 +01:00
ef09fdb1b4 Réorganiser la logique de rendu vidéo pour gérer les erreurs et améliorer la lisibilité du code 2025-02-12 11:26:57 +01:00
e61f1e9773 Remplacer l'appel à createVideoWithList par videoManager.createVideoWithList dans la route de rendu vidéo 2025-02-12 11:24:22 +01:00
a63e79e26e Ajouter la mise à jour du fichier vidéo et gérer les erreurs lors du rendu 2025-02-12 11:23:30 +01:00
269ad2283d Ajouter la gestion des erreurs lors de la conversion de la chaîne d'identifiants en tableau dans getPathList 2025-02-12 11:18:47 +01:00
c17c939b9c Parser les identifiants dans getPathList pour assurer un traitement correct des valeurs 2025-02-12 11:16:12 +01:00
b2e14b169f Modifier le type de contenu de la réponse pour la route /cat et simplifier le gestionnaire en supprimant le paramètre req 2025-02-12 11:09:51 +01:00
27f06daaaf Ajouter les modules fs et path dans videoRoutes pour la gestion des fichiers 2025-02-12 11:07:05 +01:00
208b6d5b28 Ajouter dbTester aux routes vidéo pour les tests 2025-02-12 11:06:17 +01:00
eb63c84443 Modifier la fonction getPathList pour utiliser orderId au lieu de id lors de la récupération des chemins 2025-02-12 11:05:25 +01:00
8b0de65272 Ajouter le project_id à la requête de récupération des vidéos et mettre à jour la fonction getPathList pour l'utiliser 2025-02-12 10:59:54 +01:00
bc2159f5f9 Modifier le gestionnaire de vidéo pour utiliser measureManager lors de la récupération de la liste des chemins 2025-02-12 10:55:31 +01:00
1e59f5ead1 Modifier la requête pour récupérer le chemin du fichier vidéo dans la route GET /videos/file/:video_id 2025-02-12 10:53:48 +01:00
f833f21b01 Ajouter une route pour rendre une vidéo par ID avec gestion des erreurs 2025-02-12 10:52:49 +01:00
1e7ae35c8a Ajouter une route pour récupérer un fichier vidéo par ID avec gestion des erreurs 2025-02-12 10:51:40 +01:00
c0215643ea Modifier le type de measurement_ids en chaîne et simplifier le traitement lors de la création d'une vidéo 2025-02-12 10:44:57 +01:00
25c056c3d8 Formater les IDs de mesure lors de la création d'une vidéo dans videoManager 2025-02-12 10:40:43 +01:00
8b45c5feb8 Ajouter un traitement d'erreur pour la création de vidéos dans la route POST /videos 2025-02-12 10:29:47 +01:00
a09805c5f1 Ajouter le champ de statut lors de la création d'une vidéo dans videoManager 2025-02-12 10:26:24 +01:00
7179d94527 Ajouter un champ de statut lors de la création d'une vidéo dans la base de données 2025-02-12 10:25:17 +01:00
b752595781 Renommer la fonction createVideo en createVideoProject dans la route POST /videos pour plus de clarté 2025-02-12 10:21:37 +01:00
3b4d8a9e5a Remplacer le traitement des erreurs par des logs dans les routes de création et de suppression de vidéos 2025-02-12 10:19:28 +01:00
2766a1d788 Ajouter des fonctions pour créer et supprimer des vidéos dans videoManager et mettre à jour la route POST /videos pour utiliser ces nouvelles fonctions 2025-02-12 10:17:08 +01:00
78708e4eaa Ajouter des logs pour le parsing des IDs de mesures dans la route POST /videos 2025-02-12 10:11:27 +01:00
27ada11471 Améliorer le parsing des IDs de mesures dans la route POST /videos pour gérer les erreurs de format 2025-02-12 10:10:54 +01:00
7aae1aaf34 Supprimer le parsing des IDs de mesures dans la route POST /videos 2025-02-12 10:07:58 +01:00
f9de2227dc Ajouter un logging pour afficher les détails de la création d'une vidéo dans la route POST /videos 2025-02-12 10:07:02 +01:00
ed4a37e259 Modifier les types de project_id, measurement_ids et duration dans la route POST /videos et ajouter le parsing des IDs de mesures 2025-02-12 10:05:57 +01:00
0c91f7d3c3 Ajouter un logging pour afficher les détails de la création d'une vidéo dans la route POST /videos 2025-02-12 10:02:39 +01:00
afe3c163f1 Modifier le type de project_id et duration en chaîne de caractères dans la documentation de la route GET /videos 2025-02-12 09:59:51 +01:00
0f31b5019f Modifier le type de measurement_ids en tableau d'entiers et simplifier la validation des IDs dans la route POST /videos 2025-02-12 09:26:57 +01:00
0a6fbb22bf Modifier le logging pour afficher uniquement le premier ID de mesure lors de la création d'une vidéo 2025-02-12 09:19:27 +01:00
6fedbe10c8 Ajouter la validation et le parsing des IDs de mesures dans la route POST /videos 2025-02-12 09:18:27 +01:00
cec3a10b2b Ajouter la documentation Swagger pour la route POST /videos afin de clarifier les paramètres et les réponses 2025-02-12 09:14:18 +01:00
2a24864003 Modifier la route POST /videos pour simplifier la validation des champs et mettre à jour la documentation Swagger 2025-02-12 09:12:11 +01:00
6ee50ee7b4 Ajouter la fonction getPathList pour récupérer les chemins à partir d'une liste d'IDs 2025-02-12 09:09:11 +01:00
158a288dec Décommenter le code de création et de suppression de vidéos dans tester.js 2025-02-12 09:06:06 +01:00
cd1f91589b Améliorer la gestion des erreurs et assurer l'attente lors de la création de dossiers et de l'enregistrement d'images dans uploadMeasureImage 2025-02-12 08:01:11 +00:00
0600fb44c2 Décommenter la fonction getSmileImage dans tester.js 2025-02-12 07:58:17 +00:00
1f21c288ff Merge branch 'main' of gitea.kerboul.me:timelapse/timelapse-backend 2025-02-12 07:57:42 +00:00
152f4ee508 Commenter le code de test dans tester.js pour désactiver les fonctions de création et de suppression de dossiers 2025-02-12 07:57:41 +00:00
c050a1744f Ajout Logs routes 2025-02-12 08:56:49 +01:00
53 changed files with 5256 additions and 1561 deletions

View File

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

3
.gitignore vendored
View File

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

View File

@@ -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
View 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
View File

@@ -0,0 +1,86 @@
# Timelapse Backend
Ce projet est une API de backend pour gérer une solution de timelapse avec capture, stockage et transformation d'images en vidéos.
## Architecture
L'application a été refactorisée pour suivre un modèle MVC (Modèle-Vue-Contrôleur) et respecter le principe de séparation des préoccupations. Voici la structure du projet :
```
/src
/config - Configuration centralisée de l'application
/controllers - Logique de traitement des requêtes HTTP
/database - Connexion à la base de données
/data - Compatibilité avec l'ancienne version (à terme, à supprimer)
/middlewares - Middlewares Express
/models - Modèles de données et logique d'accès à la BDD
/routes - Définition des routes de l'API
/services - Services métier et logique complexe
/utils - Utilitaires partagés
/video - Compatibilité avec l'ancienne version (à terme, à supprimer)
```
### Points d'entrée
- `server.js` - Point d'entrée principal du serveur Express
- `api.js` - Routes principales de l'API
## Composants principaux
### Modèles
Les modèles encapsulent la logique d'accès aux données et les règles métier :
- `Project.js` - Gestion des projets de timelapse
- `Measurement.js` - Gestion des mesures et images
- `Video.js` - Gestion des vidéos générées
- `Camera.js` - Gestion de la caméra et des paramètres de capture
### Contrôleurs
Les contrôleurs gèrent le traitement des requêtes HTTP et interagissent avec les modèles et services :
- `projectController.js` - Gestion des projets
- `measurementController.js` - Gestion des mesures
- `videoController.js` - Gestion des vidéos
- `imageController.js` - Gestion des images et téléchargements
- `cameraController.js` - Gestion des paramètres de la caméra
### Services
Les services implémentent la logique métier complexe :
- `storageService.js` - Gestion du stockage des fichiers
- `videoService.js` - Service de création de vidéos à partir d'images
### Routes
Les routes définissent les points d'accès HTTP de l'API :
- `projectRoutes.js` - Routes pour les projets
- `measurementRoutes.js` - Routes pour les mesures
- `videoRoutes.js` - Routes pour les vidéos
- `imageRoutes.js` - Routes pour les images
- `cameraRoutes.js` - Routes pour la caméra
## Déploiement
Le déploiement est géré via Docker Compose, avec une configuration dans `docker-compose.yml`. Le script `deploy.sh` gère le déploiement automatisé.
## Phase de Transition
L'application est actuellement en phase de transition de l'ancienne architecture vers la nouvelle. Les fichiers suivants sont des ponts de compatibilité qui seront progressivement supprimés :
- `src/database/database_manager.js` - Redirige vers les nouveaux modèles
- `src/data/storage_manager.js` - Redirige vers le nouveau service de stockage
- `src/video/videoManager.js` - Redirige vers le nouveau service vidéo
Pour supprimer les fichiers obsolètes une fois toutes les références mises à jour, utilisez :
```bash
node cleanup.js delete
```
## Documentation API
La documentation de l'API est disponible via Swagger à l'adresse `/api-docs`.

22
api.js
View File

@@ -1,24 +1,10 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const cors = require('cors');
const projectRoutes = require('./routes/projectRoutes');
const measurementRoutes = require('./routes/measurementRoutes');
const videoRoutes = require('./routes/videoRoutes');
const imageRoutes = require('./routes/imageRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const fileWatcher = require('./src/data/filewatcher.js');
router.use(cors({ // Importe toutes les routes depuis notre nouvelle structure
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'], const apiRoutes = require('./src/routes');
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type'],
credentials: true,
}));
router.use('/', projectRoutes); // Utilise directement toutes les routes définies dans src/routes/index.js
router.use('/', measurementRoutes); router.use('/', apiRoutes);
router.use('/', videoRoutes);
router.use('/', imageRoutes);
router.use('/', uploadRoutes);
module.exports = router; module.exports = router;

9
backend.config.js Normal file
View 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
View File

@@ -0,0 +1,77 @@
/**
* Script de nettoyage pour supprimer les fichiers obsolètes après refactoring
*
* Usage:
* - Pour lister les fichiers obsolètes sans les supprimer : node cleanup.js list
* - Pour supprimer les fichiers obsolètes : node cleanup.js delete
*/
const fs = require('fs');
const path = require('path');
// Liste des fichiers à considérer comme obsolètes
const deprecatedFiles = [
// Anciens fichiers de routes qui ont été remplacés par src/routes/*
'routes/uploadRoutes.js',
'routes/projectRoutes.js',
'routes/measurementRoutes.js',
'routes/videoRoutes.js',
'routes/capture_system.js',
'routes/imageRoutes.js',
// Utilitaires remplacés
'utils/serverError.js',
// Fichiers de backend qui ont été refactorisés
'ffmpeg.js',
];
// Fonction pour lister les fichiers obsolètes
function listDeprecatedFiles() {
console.log('====== Fichiers obsolètes ======');
deprecatedFiles.forEach(file => {
const filePath = path.join(__dirname, file);
if (fs.existsSync(filePath)) {
console.log(`${file} (existe)`);
} else {
console.log(`${file} (déjà supprimé)`);
}
});
console.log('==============================');
}
// Fonction pour supprimer les fichiers obsolètes
function deleteDeprecatedFiles() {
console.log('====== Suppression des fichiers obsolètes ======');
deprecatedFiles.forEach(file => {
const filePath = path.join(__dirname, file);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log(`${file} supprimé avec succès`);
} catch (error) {
console.error(`❌ Erreur lors de la suppression de ${file}:`, error.message);
}
} else {
console.log(`⚠️ ${file} n'existe pas ou a déjà été supprimé`);
}
});
console.log('==============================================');
}
// Traitement des arguments
const action = process.argv[2];
if (action === 'list') {
listDeprecatedFiles();
} else if (action === 'delete') {
listDeprecatedFiles();
console.log('\nConfirmation de suppression...');
deleteDeprecatedFiles();
} else {
console.log(`
Usage:
- Pour lister les fichiers obsolètes : node cleanup.js list
- Pour supprimer les fichiers obsolètes : node cleanup.js delete
`);
}

30
db.js
View File

@@ -1,25 +1,9 @@
const { Client } = require('pg'); /**
* Ce fichier est maintenu pour des raisons de compatibilité
* mais redirige vers la nouvelle structure de connexion à la base de données.
* À terme, toutes les références devraient utiliser src/database/connection.js
*/
const local = false; const db = require('./src/database/connection');
// Connexion à la base de données PostgreSQL
const client = new Client({
host: local ? 'mikoshi' : '172.30.0.2',
port: local ? 54322 : 5432,
user: 'timelapse',
password: 'timelapse',
database: 'timelapse'
});
function connectWithRetry() { module.exports = db;
client.connect(err => {
if (err) {
console.error('Erreur de connexion à la base de données:', err);
setTimeout(connectWithRetry, 30000); // Réessayer après 30 secondes
} else {
console.log('[DB] Connecté à la base de données PostgreSQL.');
}
});
}
connectWithRetry();
module.exports = client;

53
deploy.sh Executable file
View File

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

40
docker-compose.yml Normal file
View 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
View 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"] },
]);

View File

@@ -1,9 +1,9 @@
const { exec } = require('child_process'); const { exec } = require('child_process');
exec('ffmpeg -version', (error, stdout, stderr) => { exec('ffmpeg -version', (error) => {
if (error) { if (error) {
console.log('FFmpeg is not installed. Installing FFmpeg...'); console.log('FFmpeg is not installed. Installing FFmpeg...');
exec('apt update && apt install -y ffmpeg', (installError, installStdout, installStderr) => { exec('apt update && apt install -y ffmpeg', (installError) => {
if (installError) { if (installError) {
console.error(`Error installing FFmpeg: ${installError}`); console.error(`Error installing FFmpeg: ${installError}`);
return; return;

1540
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon server.js" "dev": "nodemon server.js",
"local" : "nodemon server_local.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -16,11 +17,16 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.3", "mysql2": "^3.11.3",
"pg": "^8.13.0", "pg": "^8.13.0",
"range-parser": "^1.2.1",
"sharp": "^0.33.5",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1" "swagger-ui-express": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"eslint": "^9.23.0",
"globals": "^16.0.0",
"nodemon": "^3.1.7" "nodemon": "^3.1.7"
} }
} }

View File

@@ -1,87 +0,0 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs');
const dbTester = require('../test/tester');
const db = require('../db');
const serverError = require('../utils/serverError');
/**
* @swagger
* /smile:
* get:
* summary: Retrieve a smile image
* responses:
* 200:
* description: A smile image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
router.get('/smile', (req, res) => {
const imagePath = dbTester.getSmileImage();
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.sendFile(imagePath);
});
});
/**
* @swagger
* /images/{projectId}/{orderId}:
* get:
* summary: Retrieve an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
router.get('/images/:projectId/:orderId', (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
db.query(query, [projectId, orderId], (err, results) => {
if (err) {
return serverError.sendError('Error getting image:', res, err);
}
if (results.rows.length === 0) {
return res.status(404).json({ error: 'Image not found' });
}
const imagePath = results.rows[0].path;
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.download(imagePath);
});
});
});
module.exports = router;

View File

@@ -1,282 +0,0 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const measureManager = require('../src/measure/measureManager');
const serverError = require('../utils/serverError');
/**
* @swagger
* /measurements:
* get:
* summary: Récupérer toutes les mesures
* description: Récupère toutes les mesures de la base de données.
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 500:
* description: Erreur serveur.
*/
router.get('/measurements', (req, res) => {
const query = 'SELECT * FROM public.measurements';
db.query(query, (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération des mesures:', res, err);
}
res.json(results.rows);
});
});
/**
* @swagger
* /measurements/{id}:
* get:
* summary: Récupérer une mesure par ID
* description: Récupère une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/measurements/:id', (req, res) => {
const measurementId = req.params.id;
if (!measurementId || isNaN(measurementId)) {
return res.status(400).json({ error: 'Invalid measurement ID' });
}
const query = 'SELECT * FROM public.measurements WHERE id = $1';
db.query(query, [measurementId], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération de la mesure:', res, err);
}
res.json(results.rows);
});
});
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* get:
* summary: Récupérer une mesure par project ID et order ID
* description: Récupère une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/measurements/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) {
return res.status(400).json({ error: 'Invalid project ID or order ID' });
}
try {
const measurement = await measureManager.getMeasurement(projectId, orderId);
res.json(measurement);
} catch (error) {
serverError.sendError('Error getting measurement:', res, error);
}
});
/**
* @swagger
* /measurements:
* post:
* summary: Ajouter une nouvelle mesure
* description: Ajoute une nouvelle mesure à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* responses:
* 201:
* description: Mesure ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/measurements', (req, res) => {
const { project_id, timestamp, image_path, temperature, humidity } = req.body;
if (!project_id || !timestamp || !image_path || !temperature || !humidity) {
return res.status(400).json({ error: 'All fields are required' });
}
const query = 'INSERT INTO public.measurements (project_id, timestamp, image_path, temperature, humidity) VALUES ($1, $2, $3, $4, $5) RETURNING id';
db.query(query, [project_id, timestamp, image_path, temperature, humidity], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de l\'ajout de la mesure:', res, err);
}
res.status(201).json({ message: 'Mesure ajoutée avec succès', id: results.rows[0].id });
});
});
/**
* @swagger
* /measurements/{id}:
* delete:
* summary: Supprimer une mesure par ID
* description: Supprime une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/measurements/:id', async (req, res) => {
const measurementId = req.params.id;
if (!measurementId || isNaN(measurementId)) {
return res.status(400).json({ error: 'Invalid measurement ID' });
}
try {
const measurement = await measureManager.deleteMeasurement(measurementId);
res.status(200).json({ message: 'Measurement deleted successfully', id: measurementId });
} catch (error) {
serverError.sendError('Error deleting measurement:', res, error);
}
});
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* delete:
* summary: Supprimer une mesure par project ID et order ID
* description: Supprime une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/measurements/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) {
return res.status(400).json({ error: 'Invalid project ID or order ID' });
}
try {
const measurement = await measureManager.deleteMeasurementByOrderId(projectId, orderId);
res.status(200).json({ message: 'Measurement deleted successfully', id: measurement.id });
} catch (error) {
serverError.sendError('Error deleting measurement:', res, error);
}
});
module.exports = router;

View File

@@ -1,225 +0,0 @@
const express = require('express');
const router = express.Router();
const projectManager = require('../src/project/projectManager');
const serverError = require('../utils/serverError');
/**
* @swagger
* /projects:
* get:
* summary: Récupérer tous les projets
* description: Récupère tous les projets disponibles.
* responses:
* 200:
* description: Une liste de projets.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 500:
* description: Erreur serveur.
*/
router.get('/projects', async (req, res) => {
try {
const projects = await projectManager.getAllProjects();
res.json(projects);
} catch (error) {
serverError.sendError('Error getting all projects:', res, error);
}
});
/**
* @swagger
* /projects/{id}:
* get:
* summary: Récupérer un projet par ID
* description: Récupère un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Un projet.
* content:
* application/json:
* schema:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const project = await projectManager.getProjectById(projectId);
res.json(project);
} catch (error) {
serverError.sendError('Error getting project by ID:', res, error);
}
});
/**
* @swagger
* /projects/{id}/videos:
* get:
* summary: Récupérer les vidéos d'un projet par ID
* description: Récupère les vidéos associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id/videos', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const videos = await projectManager.getVideosByProjectId(projectId);
res.json(videos);
} catch (error) {
serverError.sendError('Error getting videos by project ID:', res, error);
}
});
/**
* @swagger
* /projects/{id}/measurements:
* get:
* summary: Récupérer les mesures d'un projet par ID
* description: Récupère les mesures associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id/measurements', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
const measurements = await projectManager.getMeasurementsByProjectId(projectId);
res.json(measurements);
} catch (error) {
serverError.sendError('Error getting measurements by project ID:', res, error);
}
});
/**
* @swagger
* /projects:
* post:
* summary: Ajouter un nouveau projet
* description: Ajoute un nouveau projet à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Projet ajouté avec succès.
* 400:
* description: Le nom et la description sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/projects', async (req, res) => {
const { name, description } = req.body;
if (!name || !description) {
return res.status(400).json({ error: 'Name and description are required' });
}
try {
const project = await projectManager.createProject(name, description, new Date(), 0);
projectManager.createProjectDirectory(project.id);
res.status(201).json({ message: 'Project added successfully', id: project.id });
} catch (error) {
serverError.sendError('Error creating project:', res, error);
}
});
/**
* @swagger
* /projects/{id}:
* delete:
* summary: Supprimer un projet par ID
* description: Supprime un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Projet supprimé avec succès.
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/projects/:id', async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' });
}
try {
projectManager.deleteProjectDirectory(projectId);
projectManager.deleteProjectById(projectId);
res.status(200).json({ message: 'Project deleted successfully', id: projectId });
} catch (error) {
serverError.sendError('Error deleting project:', res, error);
}
});
module.exports = router;

View File

@@ -1,130 +0,0 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const measureManager = require('../src/measure/measureManager');
const serverError = require('../utils/serverError');
const upload = multer({ storage: multer.memoryStorage() });
/**
* @swagger
* /upload:
* post:
* summary: Télécharger une image
* description: Télécharge une image pour un projet et un ordre spécifiques.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* orderId:
* type: integer
* description: ID de la commande
* responses:
* 200:
* description: Image téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/upload', upload.single('image'), async (req, res) => {
const { projectId, orderId } = req.body;
const image = req.file;
if (!image || !projectId || !orderId) {
return res.status(400).json({ error: 'All fields are required' });
}
try {
const imagePath = await measureManager.uploadMeasureImage(image, projectId, orderId);
res.json({ message: 'Image uploaded successfully', path: imagePath });
} catch (error) {
serverError.sendError('Error uploading image:', res, error);
}
});
/**
* @swagger
* /uploadmeasurement:
* post:
* summary: Télécharger une mesure avec une image
* description: Télécharge une mesure avec une image pour un projet spécifique.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* timestamp:
* type: string
* format: date-time
* description: Horodatage de la mesure
* temperature:
* type: number
* description: Température mesurée
* humidity:
* type: number
* description: Humidité mesurée
* responses:
* 200:
* description: Mesure téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* id:
* type: integer
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/uploadmeasurement', upload.single('image'), async (req, res) => {
const { projectId, timestamp, temperature, humidity } = req.body;
const image = req.file;
if (!image || !projectId || !timestamp || !temperature || !humidity) {
return res.status(400).json({ error: 'All fields are required' });
}
try {
const nextOrderId = await measureManager.getNextOrderId(projectId);
const imagePath = await measureManager.uploadMeasureImage(image, projectId, nextOrderId);
const measurement = await measureManager.addMeasureToProject(projectId, timestamp, imagePath, temperature, humidity, nextOrderId);
res.json({ message: 'Measurement uploaded successfully', path: imagePath, id: measurement.id });
} catch (error) {
serverError.sendError('Error uploading measurement:', res, error);
}
});
module.exports = router;

View File

@@ -1,234 +0,0 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const serverError = require('../utils/serverError');
const videoManager = require('../src/video/videoManager');
/**
* @swagger
* /videos:
* get:
* summary: Récupérer toutes les vidéos
* description: Récupère toutes les vidéos de la base de données.
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 500:
* description: Erreur serveur.
*/
router.get('/videos', (req, res) => {
const query = 'SELECT * FROM public.videos';
db.query(query, (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération des vidéos:', res, err);
}
res.json(results.rows);
});
});
/**
* @swagger
* /videos/{id}:
* get:
* summary: Récupérer une vidéo par ID
* description: Récupère une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Une vidéo.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 400:
* description: ID de vidéo invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/videos/:id', (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return res.status(400).json({ error: 'Invalid video ID' });
}
const query = 'SELECT * FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération de la vidéo:', res, err);
}
res.json(results.rows);
});
});
/**
* @swagger
* /videos:
* post:
* summary: Ajouter une nouvelle vidéo
* description: Ajoute une nouvelle vidéo à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* duration:
* type: number
* resolution:
* type: string
* name:
* type: string
* responses:
* 201:
* description: Vidéo ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/videos', (req, res) => {
const { project_id, measurement_ids, video_path, duration, resolution, name } = req.body;
if (!project_id || !measurement_ids || !video_path || !duration || !resolution || !name) {
return res.status(400).json({ error: 'All fields are required' });
}
const list_ids = measurement_ids.split(',');
const image_count = list_ids.length;
const videoPath = '/videos/' + name + '.mp4';
const query_first = 'SELECT timestamp FROM public.measurements WHERE id = $1';
const query_last = 'SELECT timestamp FROM public.measurements WHERE id = $1';
db.query(query_first, [list_ids[0]], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération du timestamp de la première image:', res, err);
}
const start_timestamp = results.rows[0].timestamp;
db.query(query_last, [list_ids[image_count - 1]], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération du timestamp de la dernière image:', res, err);
}
const end_timestamp = results.rows[0].timestamp;
const fps = image_count / duration;
const query = 'INSERT INTO public.videos (project_id, measurement_ids, video_path, start_timestamp, end_timestamp, image_count, resolution, duration, fps, status, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id';
db.query(query, [project_id, measurement_ids, videoPath, start_timestamp, end_timestamp, image_count, resolution, duration, fps, 0, name], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de l\'ajout de la vidéo:', res, err);
}
res.status(201).json({ message: 'Vidéo ajoutée avec succès', id: results.rows[0].id });
});
});
});
});
/**
* @swagger
* /videos/{id}:
* delete:
* summary: Supprimer une vidéo par ID
* description: Supprime une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Vidéo supprimée avec succès.
* 400:
* description: ID de vidéo invalide.
* 404:
* description: Aucune vidéo trouvée avec cet ID.
* 500:
* description: Erreur serveur.
*/
router.delete('/videos/:id', (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return res.status(400).json({ error: 'Invalid video ID' });
}
const query = 'DELETE FROM public.videos WHERE id = $1 RETURNING id';
db.query(query, [videoId], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la suppression de la vidéo:', res, err);
}
if (results.rowCount === 0) {
return res.status(404).json({ error: 'Aucune vidéo trouvée avec cet ID.' });
}
res.status(200).json({ message: 'Vidéo supprimée avec succès', id: videoId });
});
});
module.exports = router;

BIN
sample/cat.mp4 Normal file

Binary file not shown.

View File

@@ -1,47 +1,31 @@
// server.js // server.js
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const app = express(); const app = express();
const port = 3000; const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const config = require('./src/config');
const DatabaseManager = require('./src/models/database');
// Middleware pour gérer les requêtes JSON // Middleware pour gérer les requêtes JSON
app.use(express.json()); app.use(express.json());
// Cors accès à tout // Configuration CORS
app.use(cors({ app.use(cors(config.server.cors));
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type'],
credentials: true,
}));
// Importer les routes // Initialisation de la base de données
const apiRoutes = require('./api'); DatabaseManager.initialize()
.then(() => console.log('[SERVER] Base de données initialisée avec succès'))
.catch(err => console.error('[SERVER] Erreur d\'initialisation de la base de données:', err));
// Importer les routes API
const apiRoutes = require('./src/routes');
app.use('/api', apiRoutes); app.use('/api', apiRoutes);
// Swagger dependencies
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
// Configuration de Swagger // Configuration de Swagger
const swaggerOptions = { const swaggerOptions = {
definition: { definition: config.swagger.definition,
openapi: '3.0.0', apis: config.swagger.apis
info: {
title: 'API Documentation',
version: '1.0.0',
description: 'Documentation de l\'API avec Swagger',
},
servers: [
{
url: 'https://timelapse.kerboul.me/api',
},
{
url: 'http://localhost:3000/api',
}
],
},
apis: ['./routes/*.js'], // Prend en compte tous les fichiers de routes pour générer la documentation
}; };
// Initialisation de swagger-jsdoc // Initialisation de swagger-jsdoc
@@ -52,14 +36,36 @@ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
// Route de base pour tester le serveur // Route de base pour tester le serveur
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500'); res.send('Bienvenue sur l\'API Timelapse!');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); });
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.send('Bienvenue sur mon API Node.js!'); // Gestion des erreurs 404
app.use((req, res) => {
res.status(404).json({
error: {
message: 'Route non trouvée',
path: req.path,
method: req.method,
statusCode: 404
}
});
});
// Gestion des erreurs globales
app.use((err, req, res, next) => {
console.error('[SERVER] Erreur non gérée:', err);
res.status(500).json({
error: {
message: 'Erreur serveur interne',
statusCode: 500,
details: process.env.NODE_ENV === 'production' ? undefined : err.message
}
});
}); });
// Démarrer le serveur // Démarrer le serveur
const port = config.server.port;
app.listen(port, () => { app.listen(port, () => {
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`); console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
console.log(`[SERVER] Swagger documentation disponible sur http://localhost:${port}/api-docs`); console.log(`[SERVER] Documentation Swagger disponible sur http://localhost:${port}/api-docs`);
}); });

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

@@ -0,0 +1,182 @@
// src/config/index.js
const path = require('path');
// Configuration centralisée de l'application
module.exports = {
// Configuration du serveur
server: {
port: process.env.PORT || 3000,
cors: {
origins: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type'],
credentials: true
}
},
// Configuration de la base de données
database: {
host: process.env.DB_HOST || 'timelapse-db',
port: process.env.DB_PORT || 5432,
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'timelapse',
reconnectInterval: 3000
},
// Configuration de Swagger
swagger: {
definition: {
openapi: '3.0.0',
info: {
title: 'API Timelapse',
version: '1.0.0',
description: 'Documentation de l\'API Timelapse pour la gestion des projets, mesures et vidéos',
contact: {
name: 'Support Timelapse',
email: 'support@timelapse.kerboul.me'
}
},
servers: [
{ url: 'https://timelapse.kerboul.me/api', description: 'Serveur de production' },
{ url: 'http://localhost:3000/api', description: 'Serveur de développement local' }
],
components: {
schemas: {
Project: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
description: { type: 'string' },
start_date: { type: 'string', format: 'date' },
status: { type: 'integer' }
}
},
Measurement: {
type: 'object',
properties: {
id: { type: 'integer' },
project_id: { type: 'integer' },
timestamp: { type: 'string', format: 'date-time' },
path: { type: 'string' },
temperature: { type: 'number' },
humidity: { type: 'number' },
order_id: { type: 'integer' }
}
},
Video: {
type: 'object',
properties: {
id: { type: 'integer' },
project_id: { type: 'integer' },
measurement_ids: { type: 'string' },
name: { type: 'string' },
resolution: { type: 'string' },
duration: { type: 'integer' },
status: { type: 'integer' },
progress: { type: 'number' },
video_file: { type: 'string', nullable: true },
started_at: { type: 'string', format: 'date-time', nullable: true },
updated_at: { type: 'string', format: 'date-time', nullable: true },
eta: { type: 'number', nullable: true }
}
},
Camera: {
type: 'object',
properties: {
id: { type: 'integer' },
interval: { type: 'integer', nullable: true },
maintenance: { type: 'integer' },
active: { type: 'integer' }
}
},
Error: {
type: 'object',
properties: {
message: { type: 'string' },
statusCode: { type: 'integer' },
details: { type: 'string', nullable: true },
timestamp: { type: 'string', format: 'date-time' }
}
}
},
responses: {
NotFound: {
description: 'La ressource demandée n\'a pas été trouvée',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
BadRequest: {
description: 'Requête invalide',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
},
ServerError: {
description: 'Erreur serveur',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error'
}
}
}
}
}
},
tags: [
{ name: 'Projets', description: 'Opérations relatives aux projets' },
{ name: 'Mesures', description: 'Opérations relatives aux mesures et images' },
{ name: 'Vidéos', description: 'Opérations relatives aux vidéos' },
{ name: 'Caméra', description: 'Opérations relatives au contrôle de la caméra' },
{ name: 'Images', description: 'Opérations relatives aux images' },
{ name: 'Système', description: 'Opérations relatives au système' }
],
},
apis: ['./src/routes/*.js'] // Chemins pour la documentation
},
// Chemins des répertoires
paths: {
storage: path.join(process.cwd(), 'storage'),
uploads: path.join(process.cwd(), 'uploads'),
samples: path.join(process.cwd(), 'sample')
},
// Statuts pour les projets
projectStatus: {
brouillon: 0,
capturing: 1,
idle: 2,
stopping: 3 // Projet en cours d'arrêt
},
// Statuts pour les vidéos
videoStatus: {
rendering: 0,
completed: 1,
error: 2
},
// Paramètres par défaut pour la caméra
camera: {
defaultSettings: {
id: 1,
interval: null,
nbImages: null,
maintenance: false,
stopFlag: false,
idle: true
}
}
};

View File

@@ -0,0 +1,187 @@
// src/controllers/cameraController.js
const Camera = require('../models/Camera');
const Project = require('../models/Project');
const { sendError, asyncHandler } = require('../utils/errorHandler');
const config = require('../config');
/**
* Contrôleur pour les opérations liées à la caméra
*/
class CameraController {
/**
* Récupère le statut actuel de la caméra
*/
static getCameraStatus = asyncHandler(async (req, res) => {
try {
const settings = await Camera.getCamera();
if (!settings) {
// Initialise la caméra si elle n'existe pas
const cameraSettings = await Camera.initializeCamera();
return res.json(cameraSettings);
}
res.json(settings);
} catch (error) {
sendError('Erreur lors de la récupération du statut de la caméra', res, error, 500);
}
});
/**
* Démarre une procédure de capture
*/
static startProcedure = asyncHandler(async (req, res) => {
const { project_id, interval, nb_images } = req.body;
console.log('project_id:', project_id);
if (!interval || !nb_images) {
return sendError('L\'intervalle et le nombre d\'images sont requis', res, null, 400);
}
try {
// Vérifie qu'aucune procédure n'est déjà en cours
const existingProject = await Project.findCurrentRenderingProject();
if (existingProject) {
return sendError('Un projet est déjà en cours de capture. Veuillez l\'arrêter avant d\'en démarrer un nouveau.', res, null, 400);
}
// Met à jour les paramètres de la caméra
const newSettings = {
interval: interval,
nb_images: nb_images,
stop_flag: false,
idle: false // idle = 1 (idle = 0)
};
await Camera.updateCamera(1, newSettings);
// Met à jour le statut du projet
await Project.updateProject(project_id, { status: config.projectStatus.capturing });
console.log(`[CAMERA] Procédure démarrée pour le projet : ${project_id}`);
res.json({
message: 'Procédure de capture démarrée avec succès',
settings: { interval, nb_images }
});
} catch (error) {
sendError('Erreur lors du démarrage de la procédure de capture', res, error, 500);
}
});
/**
* Initie l'arrêt de la procédure de capture
*/
static stopProcedure = asyncHandler(async (req, res) => {
try {
// Trouve le projet actuellement en cours de capture
const currentProject = await Project.findCurrentRenderingProject();
if (!currentProject) {
return sendError('Aucun projet en cours de capture trouvé', res, null, 404);
}
// Met à jour le statut du projet en cours d'arrêt
await Project.updateProjectStatus(currentProject.id, config.projectStatus.stopping);
// Marque le drapeau d'arrêt
await Camera.updateCamera(1, { stop_flag: true });
console.log(`[CAMERA] Arrêt de la caméra demandé pour le projet ${currentProject.id}, en attente de confirmation...`);
res.json({
message: 'Procédure d\'arrêt de la caméra initiée avec succès',
project_id: currentProject.id
});
} catch (error) {
sendError('Erreur lors de l\'arrêt de la procédure de capture', res, error, 500);
}
});
/**
* Confirme l'arrêt de la caméra (appelé par la caméra)
*/
static confirmStopProcedure = asyncHandler(async (req, res) => {
try {
// Réinitialise les paramètres de la caméra
const newSettings = {
interval: null,
nb_images: null,
stop_flag: false,
idle: true // idle = true
};
await Camera.updateCamera(1, newSettings);
// Recherche le projet en cours d'arrêt
const stoppingProject = await Project.findStoppingProject();
if (stoppingProject) {
// Mettre à jour le statut du projet en cours d'arrêt vers idle
await Project.updateProjectStatus(stoppingProject.id, config.projectStatus.idle);
console.log(`[CAMERA] Projet : ${stoppingProject.id} arrêté avec succès.`);
res.json({
message: 'Caméra arrêtée avec succès',
project_id: stoppingProject.id,
status: config.projectStatus.idle
});
} else {
// Vérifier s'il y a un projet en cours de capture qui n'aurait pas été marqué comme stopping
const currentProject = await Project.findCurrentRenderingProject();
if (currentProject) {
await Project.updateProjectStatus(currentProject.id, config.projectStatus.idle);
console.log(`[CAMERA] Projet : ${currentProject.id} arrêté (était en capture).`);
res.json({
message: 'Caméra arrêtée avec succès (projet était en capture)',
project_id: currentProject.id,
status: config.projectStatus.idle
});
} else {
console.log('[CAMERA] Aucun projet en cours d\'arrêt ou de capture trouvé.');
res.json({ message: 'Caméra arrêtée avec succès mais aucun projet à mettre à jour' });
}
}
console.log('[CAMERA] Caméra arrêtée.');
} catch (error) {
sendError('Erreur lors de la confirmation de l\'arrêt de la caméra', res, error, 500);
}
});
/**
* Active le mode maintenance
*/
static activateMaintenance = asyncHandler(async (req, res) => {
try {
await Camera.updateCamera(1, { maintenance: 1 });
console.log('[CAMERA] Mode maintenance activé.');
res.json({ message: 'Caméra en mode maintenance' });
} catch (error) {
sendError('Erreur lors de l\'activation du mode maintenance', res, error, 500);
}
});
/**
* Désactive le mode maintenance
*/
static deactivateMaintenance = asyncHandler(async (req, res) => {
try {
await Camera.updateCamera(1, { maintenance: 0 });
console.log('[CAMERA] Mode maintenance désactivé.');
res.json({ message: 'Caméra sortie du mode maintenance' });
} catch (error) {
sendError('Erreur lors de la désactivation du mode maintenance', res, error, 500);
}
});
}
module.exports = CameraController;

View File

@@ -0,0 +1,183 @@
// src/controllers/imageController.js
const fs = require('fs');
const sharp = require('sharp');
const Measurement = require('../models/Measurement');
const StorageService = require('../services/storageService');
const { sendError, asyncHandler } = require('../utils/errorHandler');
const path = require('path');
const config = require('../config');
/**
* Contrôleur pour les opérations liées aux images
*/
class ImageController {
/**
* Récupère une image par projet et ordre ID
*/
static getImageByProjectAndOrderId = asyncHandler(async (req, res) => {
const { projectId, orderId } = req.params;
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
}
try {
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
if (!measurement) {
return sendError('Image non trouvée', res, null, 404);
}
const imagePath = measurement.path;
// Vérifie si l'image existe
await fs.promises.access(imagePath, fs.constants.F_OK);
res.download(imagePath);
} catch (error) {
if (error.code === 'ENOENT') {
sendError('Image non trouvée sur le disque', res, error, 404);
} else {
sendError('Erreur lors de la récupération de l\'image', res, error, 500);
}
}
});
/**
* Récupère une image par son ID de mesure
*/
static getImageByMeasurementId = asyncHandler(async (req, res) => {
const measurementId = req.params.measurementId;
if (!measurementId || isNaN(measurementId)) {
return sendError('ID de mesure invalide', res, null, 400);
}
try {
const measurement = await Measurement.getMeasurementById(measurementId);
if (!measurement) {
return sendError('Image non trouvée', res, null, 404);
}
const imagePath = measurement.path;
// Vérifie si l'image existe
await fs.promises.access(imagePath, fs.constants.F_OK);
res.download(imagePath);
} catch (error) {
if (error.code === 'ENOENT') {
sendError('Image non trouvée sur le disque', res, error, 404);
} else {
sendError('Erreur lors de la récupération de l\'image', res, error, 500);
}
}
});
/**
* Récupère un aperçu redimensionné d'une image
*/
static getImagePreview = asyncHandler(async (req, res) => {
const { projectId, orderId } = req.params;
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
}
try {
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
if (!measurement) {
return sendError('Image non trouvée', res, null, 404);
}
const imagePath = measurement.path;
// Vérifie si l'image existe
await fs.promises.access(imagePath, fs.constants.F_OK);
// Redimensionne l'image
const metadata = await sharp(imagePath).metadata();
const width = Math.floor(metadata.width / 7);
const height = Math.floor(metadata.height / 7);
const resizedImage = await sharp(imagePath)
.resize(width, height)
.jpeg({ quality: 65 })
.toBuffer();
res.set('Content-Type', 'image/jpeg');
res.send(resizedImage);
} catch (error) {
if (error.code === 'ENOENT') {
sendError('Image non trouvée sur le disque', res, error, 404);
} else {
sendError('Erreur lors de la récupération de l\'aperçu de l\'image', res, error, 500);
}
}
});
/**
* Télécharge une nouvelle image avec des données de mesure
* Ne nécessite plus l'ID du projet, utilise automatiquement le projet actif
*/
static uploadImage = asyncHandler(async (req, res) => {
const { timestamp, temperature, humidity } = req.body;
const image = req.file;
if (!image || !timestamp || !temperature || !humidity) {
return sendError('Tous les champs sont requis', res, null, 400);
}
try {
// Récupération du projet actif (en cours de capture)
const Project = require('../models/Project');
const activeProject = await Project.findCurrentRenderingProject();
if (!activeProject) {
return sendError('Aucun projet actif en cours de capture', res, null, 400);
}
const projectId = activeProject.id;
// Obtention du prochain ordre ID
const nextOrderId = await Measurement.getNextOrderId(projectId);
// Enregistrement de l'image
const imagePath = await StorageService.measurement.uploadMeasurementImage(
image, projectId, nextOrderId
);
// Création de l'entrée dans la base de données
const measurement = await Measurement.createMeasurement(
projectId, timestamp, imagePath, temperature, humidity, nextOrderId
);
res.json({
message: 'Mesure téléchargée avec succès',
project_id: projectId,
path: imagePath,
id: measurement.id
});
} catch (error) {
sendError('Erreur lors du téléchargement de la mesure', res, error, 500);
}
});
/**
* Récupère une image d'exemple (pour les tests)
*/
static getSmileImage = asyncHandler(async (req, res) => {
const imagePath = path.join(config.paths.samples, 'smile.png');
try {
await fs.promises.access(imagePath, fs.constants.F_OK);
res.sendFile(imagePath);
} catch (error) {
sendError('Image d\'exemple non trouvée', res, error, 404);
}
});
}
module.exports = ImageController;

View File

@@ -0,0 +1,135 @@
// src/controllers/measurementController.js
const Measurement = require('../models/Measurement');
const { sendError, asyncHandler } = require('../utils/errorHandler');
const StorageService = require('../services/storageService');
const fs = require('fs').promises;
/**
* Contrôleur pour les opérations liées aux mesures
*/
class MeasurementController {
/**
* Récupère toutes les mesures
*/
static getAllMeasurements = asyncHandler(async (req, res) => {
const measurements = await Measurement.getAllMeasurements();
if (!measurements || measurements.length === 0) {
return sendError('Aucune mesure trouvée', res, null, 404);
}
res.json(measurements);
});
/**
* Récupère une mesure par son ID
*/
static getMeasurementById = asyncHandler(async (req, res) => {
const measurementId = req.params.id;
if (!measurementId || isNaN(measurementId)) {
return sendError('ID de mesure invalide', res, null, 400);
}
const measurement = await Measurement.getMeasurementById(measurementId);
if (!measurement) {
return sendError('Mesure non trouvée', res, null, 404);
}
res.json(measurement);
});
/**
* Récupère une mesure par son projet ID et son ordre ID
*/
static getMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
const { projectId, orderId } = req.params;
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
}
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
if (!measurement) {
return sendError('Mesure non trouvée', res, null, 404);
}
res.json(measurement);
});
/**
* Supprime une mesure par son ID
*/
static deleteMeasurement = asyncHandler(async (req, res) => {
const measurementId = req.params.id;
if (!measurementId || isNaN(measurementId)) {
return sendError('ID de mesure invalide', res, null, 400);
}
// Récupère les informations de la mesure avant suppression
const measurement = await Measurement.getMeasurementById(measurementId);
if (!measurement) {
return sendError('Mesure non trouvée', res, null, 404);
}
// Supprime le fichier image associé si existant
if (measurement.path) {
try {
await fs.access(measurement.path);
await fs.unlink(measurement.path);
} catch (error) {
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
}
}
// Supprime l'entrée de la base de données
await Measurement.deleteMeasurement(measurementId);
res.json({
message: 'Mesure supprimée avec succès',
id: measurementId
});
});
/**
* Supprime une mesure par projet ID et ordre ID
*/
static deleteMeasurementByProjectAndOrderId = asyncHandler(async (req, res) => {
const { projectId, orderId } = req.params;
if (!projectId || !orderId || isNaN(projectId) || isNaN(orderId)) {
return sendError('IDs de projet ou d\'ordre invalides', res, null, 400);
}
// Récupère les informations de la mesure avant suppression
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
if (!measurement) {
return sendError('Mesure non trouvée', res, null, 404);
}
// Supprime le fichier image associé si existant
if (measurement.path) {
try {
await fs.access(measurement.path);
await fs.unlink(measurement.path);
} catch (error) {
console.error(`[MEASUREMENT] Erreur lors de la suppression du fichier : ${measurement.path}`, error);
}
}
// Supprime l'entrée de la base de données
await Measurement.deleteMeasurement(measurement.id);
res.json({
message: 'Mesure supprimée avec succès',
id: measurement.id
});
});
}
module.exports = MeasurementController;

View File

@@ -0,0 +1,123 @@
// src/controllers/projectController.js
const Project = require('../models/Project');
const Video = require('../models/Video');
const Measurement = require('../models/Measurement');
const StorageService = require('../services/storageService');
const { sendError, asyncHandler } = require('../utils/errorHandler');
const config = require('../config');
/**
* Contrôleur pour les opérations liées aux projets
*/
class ProjectController {
/**
* Récupère tous les projets
*/
static getAllProjects = asyncHandler(async (req, res) => {
const projects = await Project.getAllProjects();
res.json(projects);
});
/**
* Récupère un projet par son ID
*/
static getProjectById = asyncHandler(async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return sendError('ID de projet invalide', res, null, 400);
}
const project = await Project.getProjectById(projectId);
if (!project) {
return sendError('Projet non trouvé', res, null, 404);
}
res.json(project);
});
/**
* Récupère les vidéos d'un projet
*/
static getProjectVideos = asyncHandler(async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return sendError('ID de projet invalide', res, null, 400);
}
const videos = await Video.getVideosByProjectId(projectId);
if (videos.length === 0) {
return sendError('Aucune vidéo trouvée pour ce projet', res, null, 404);
}
res.json(videos);
});
/**
* Récupère les mesures d'un projet
*/
static getProjectMeasurements = asyncHandler(async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return sendError('ID de projet invalide', res, null, 400);
}
const measurements = await Measurement.getMeasurementsByProjectId(projectId);
if (measurements.length === 0) {
return sendError('Aucune mesure trouvée pour ce projet', res, null, 404);
}
res.json(measurements);
});
/**
* Crée un nouveau projet
*/
static createProject = asyncHandler(async (req, res) => {
const { name, description } = req.body;
if (!name || !description) {
return sendError('Le nom et la description sont requis', res, null, 400);
}
const date = new Date();
const defaultStatus = config.projectStatus.brouillon;
const project = await Project.createProject(name, description, date, defaultStatus);
await StorageService.project.createProjectDirectory(project.id);
res.status(201).json({
message: 'Projet ajouté avec succès',
id: project.id
});
});
/**
* Supprime un projet existant
*/
static deleteProject = asyncHandler(async (req, res) => {
const projectId = req.params.id;
if (!projectId || isNaN(projectId)) {
return sendError('ID de projet invalide', res, null, 400);
}
// Supprime d'abord le répertoire du projet
await StorageService.project.deleteProjectDirectory(projectId);
// Puis supprime l'entrée dans la base de données
await Project.deleteProject(projectId);
res.status(200).json({
message: 'Projet supprimé avec succès',
id: projectId
});
});
}
module.exports = ProjectController;

View File

@@ -0,0 +1,256 @@
// src/controllers/videoController.js
const fs = require('fs');
const rangeParser = require('range-parser');
const Video = require('../models/Video');
const Measurement = require('../models/Measurement');
const StorageService = require('../services/storageService');
const VideoService = require('../services/videoService');
const { sendError, asyncHandler } = require('../utils/errorHandler');
const config = require('../config');
const path = require('path');
/**
* Contrôleur pour les opérations liées aux vidéos
*/
class VideoController {
/**
* Récupère toutes les vidéos
*/
static getAllVideos = asyncHandler(async (req, res) => {
const videos = await Video.getAllVideos();
res.json(videos);
});
/**
* Récupère une vidéo par son ID
*/
static getVideoById = asyncHandler(async (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return sendError('ID de vidéo invalide', res, null, 400);
}
const video = await Video.getVideoById(videoId);
if (!video) {
return sendError('Vidéo non trouvée', res, null, 404);
}
res.json(video);
});
/**
* Crée une nouvelle vidéo
*/
static createVideo = asyncHandler(async (req, res) => {
const { project_id, measurement_ids, name, resolution, duration } = req.body;
if (!project_id || !measurement_ids || !name || !resolution || !duration) {
return sendError('Tous les champs sont requis', res, null, 400);
}
console.log('[VIDEO] Création de vidéo avec les mesures:', measurement_ids);
try {
// Crée l'entrée vidéo dans la base de données
const video = await Video.createVideo(
project_id,
measurement_ids,
name,
resolution,
duration,
config.videoStatus.rendering
);
if (!video || !video.id) {
return sendError('Erreur lors de la création de la vidéo', res, null, 500);
}
console.log('[VIDEO] Vidéo créée avec succès:', video.id);
// Parse et récupère les chemins des images
const pathList = await this.getMeasurementPathList(measurement_ids, project_id);
if (!pathList || pathList.length === 0) {
return sendError('Aucun chemin trouvé pour les mesures', res, null, 404);
}
// Parse la résolution (ex: 1920x1080)
const [resWidth, resHeight] = resolution.split('x').map(Number);
if (isNaN(resWidth) || isNaN(resHeight)) {
return sendError('Format de résolution invalide. Utiliser LARGEURxHAUTEUR (ex: 1920x1080)', res, null, 400);
}
// Démarre le processus de rendu en arrière-plan
VideoService.createVideoFromImages(
project_id,
pathList,
duration,
video.id,
resWidth,
resHeight
)
.then(videoFile => {
console.log('[VIDEO] Rendu terminé:', videoFile);
return Video.updateVideoFilePath(video.id, videoFile);
})
.catch(error => {
console.error('[VIDEO] Échec du rendu:', error);
});
// Réponse immédiate
res.json({
message: 'Vidéo créée avec succès et le rendu a démarré',
id: video.id
});
} catch (err) {
console.error('[VIDEO] Erreur lors de la création de la vidéo:', err);
sendError('Erreur lors de la création de la vidéo', res, err, 500);
}
});
/**
* Supprime une vidéo existante
*/
static deleteVideo = asyncHandler(async (req, res) => {
const videoId = req.params.id;
if (!videoId || isNaN(videoId)) {
return sendError('ID de vidéo invalide', res, null, 400);
}
// Récupère les informations de la vidéo
const video = await Video.getVideoById(videoId);
if (!video) {
return sendError('Vidéo non trouvée', res, null, 404);
}
// Supprime le fichier vidéo s'il existe
if (video.video_file) {
try {
console.log('[VIDEO] Suppression du fichier vidéo:', video.video_file);
await StorageService.deleteFile(video.video_file);
} catch (err) {
console.error('[VIDEO] Erreur lors de la suppression du fichier vidéo:', err);
}
} else {
console.log('[VIDEO] Pas de fichier vidéo à supprimer');
}
// Supprime l'entrée dans la base de données
await Video.deleteVideo(videoId);
res.json({ message: 'Vidéo supprimée avec succès' });
});
/**
* Récupère le fichier vidéo avec support du streaming
*/
static getVideoFile = asyncHandler(async (req, res) => {
const videoId = req.params.video_id;
const video = await Video.getVideoById(videoId);
if (!video) {
return sendError('Vidéo non trouvée', res, null, 404);
}
if (video.status === config.videoStatus.rendering || video.status === config.videoStatus.rendering) {
return sendError('Vidéo pas encore produite', res, null, 400);
}
let videoPath = video.video_file;
// Vérifie si le fichier existe
try {
await fs.promises.access(videoPath, fs.constants.F_OK);
} catch (err) {
console.error('[VIDEO] Fichier vidéo non trouvé:', err);
return sendError('Fichier vidéo non trouvé', res, err, 404);
}
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = rangeParser(fileSize, range);
const start = parts[0].start;
const end = parts[0].end;
const chunksize = (end - start) + 1;
const file = fs.createReadStream(videoPath, { start, end });
const head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
};
res.writeHead(200, head);
fs.createReadStream(videoPath).pipe(res);
}
});
/**
* Récupère la progression d'une vidéo en cours de rendu
*/
static getVideoProgress = asyncHandler(async (req, res) => {
const videoId = req.params.video_id;
try {
const progress = await VideoService.getVideoProgress(videoId);
res.json({
progress: progress.progress,
elapsed: progress.elapsed,
eta: progress.eta,
status: VideoService.getStatusLabel(progress.status)
});
} catch (error) {
sendError('Erreur de récupération de la progression', res, error, 500);
}
});
/**
* Récupère les chemins d'images à partir d'une liste d'IDs de mesures
* Méthode utilitaire privée
*/
static async getMeasurementPathList(idListJson, projectId) {
let idList;
try {
idList = JSON.parse(idListJson);
if (!Array.isArray(idList)) {
throw new Error('Le format de la liste des IDs est invalide');
}
} catch (e) {
console.error('[VIDEO] Erreur de parsing de la liste d\'IDs:', e);
return [];
}
const pathList = [];
for (const orderId of idList) {
const measurement = await Measurement.getMeasurementByProjectAndOrderId(projectId, orderId);
if (measurement && measurement.path) {
pathList.push(measurement.path);
}
}
return pathList;
}
}
module.exports = VideoController;

View File

@@ -1,18 +1,14 @@
import db from '../../db.js'; const db = require('../../db.js');
import path from 'path'; const storage_manager = require('./storage_manager.js');
import storageManager from '../data/storageManager.js'; const fs = require('fs');
import fs from 'fs';
let localCounter = 0; let localCounter = 0;
async function checkAndRemoveInvalidEntries() { async function checkAndRemoveInvalidEntries() {
localCounter = 0; localCounter = 0;
console.log('[INFO] Vérification et suppression des entrées invalides...');
try { try {
const measurementsRes = await db.query('SELECT id, path FROM measurements'); const measurementsRes = await db.query('SELECT id, path FROM measurements');
//console.log('Fetched measurements:', measurementsRes.rows);
for (const row of measurementsRes.rows) { for (const row of measurementsRes.rows) {
//console.log('Checking file path:', row.path);
if (!fs.existsSync(row.path)) { if (!fs.existsSync(row.path)) {
await db.query('DELETE FROM measurements WHERE id = $1', [row.id]); await db.query('DELETE FROM measurements WHERE id = $1', [row.id]);
console.log(`Deleted invalid measurement entry with id: ${row.id}`); console.log(`Deleted invalid measurement entry with id: ${row.id}`);
@@ -20,22 +16,18 @@ async function checkAndRemoveInvalidEntries() {
} }
} }
// Scan all images in storage const allImages = await storage_manager.scanAllImages();
const allImages = await storageManager.scanAllImages();
//console.log('Scanned all images:', allImages);
for (const imagePath of allImages) { for (const imagePath of allImages) {
const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]); const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]);
if (entryRes.rows.length === 0) { if (entryRes.rows.length === 0) {
// Remove the file if the entry does not exist
fs.unlinkSync(imagePath); fs.unlinkSync(imagePath);
console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`); console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`);
localCounter++; // Increment counter if entry is deleted localCounter++;
} }
} }
if (localCounter > 0) { if (localCounter > 0) {
console.log(`[INFO] ${localCounter} entrées ont été modifiées`); console.log(`[INFO] ${localCounter} entrées ont été modifiées`);
} else { localCounter = 0; // Reset the counter after logging
console.log('[INFO] Aucune entrée n\'a été modifiée.');
} }
} catch (err) { } catch (err) {
console.error('Error checking and removing invalid entries:', err); console.error('Error checking and removing invalid entries:', err);
@@ -46,7 +38,7 @@ async function checkAndRemoveInvalidEntries() {
// Run the check periodically // Run the check periodically
console.log('[INFO] Activation du FileWatcher pour surveiller les fichiers invalides...') console.log('[INFO] Activation du FileWatcher pour surveiller les fichiers invalides...')
setInterval(checkAndRemoveInvalidEntries, 10000); // Every 10 seconds setInterval(checkAndRemoveInvalidEntries, 1000); // Every 10 seconds
// Initial run // Initial run
checkAndRemoveInvalidEntries(); checkAndRemoveInvalidEntries();

View File

@@ -1,87 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
const PROJECTS_DIR = path.join('.');
async function createFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(projectDir, { recursive: true });
} else {
throw error;
}
}
return projectDir;
}
async function deleteFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
await fs.rm(projectDir, { recursive: true, force: true });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
async function scanAllImages(dir = 'storage') {
const projectDir = path.join(PROJECTS_DIR, dir);
let results = [];
async function scanDirectory(directory) {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await scanDirectory(filePath);
} else if (file.endsWith('.jpg')) {
results.push(filePath);
}
}
}
await scanDirectory(projectDir);
return results;
}
async function saveFile(filePath, content) {
if (Buffer.isBuffer(content)) {
await fs.writeFile(filePath, content);
} else {
throw new Error('Content must be a buffer');
}
}
async function getFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
return await fs.readFile(filePath);
}
async function deleteFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(filePath); // Vérifie si le fichier existe
await fs.rm(filePath); // Supprime le fichier
return `File ${filePath} deleted successfully.`;
} catch (error) {
if (error.code === 'ENOENT') {
return `File ${filePath} does not exist.`;
} else {
throw error; // Relance l'erreur si ce n'est pas une erreur de fichier introuvable
}
}
}
module.exports = {
createFolder,
deleteFolder,
scanAllImages,
saveFile,
getFile,
deleteFile
};

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

View File

@@ -0,0 +1,44 @@
// src/database/connection.js
const { Client } = require('pg');
const config = require('../config');
// Création du client PostgreSQL avec la configuration centralisée
const client = new Client({
host: config.database.host,
port: config.database.port,
user: config.database.user,
password: config.database.password,
database: config.database.database
});
let isConnecting = false;
/**
* Initialise la connexion à la base de données
* Réessaie automatiquement si la connexion échoue
*/
function initDatabase() {
if (isConnecting) {
console.log('[DB] Tentative de connexion déjà en cours, ignorer...');
return;
}
console.log('[DB] Initialisation de la connexion à PostgreSQL...');
isConnecting = true;
client.connect(err => {
isConnecting = false;
if (err) {
console.error('[DB] Erreur de connexion à la base de données:', err);
setTimeout(initDatabase, config.database.reconnectInterval);
} else {
console.log('[DB] Connecté à la base de données PostgreSQL.');
}
});
}
// Initialise la connexion lors de l'importation de ce module
initDatabase();
module.exports = client;

View File

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

View File

@@ -1,97 +0,0 @@
import db from '../../db.js';
import path from 'path';
import storageManager from '../data/storageManager.js';
async function uploadMeasureImage(image, projectId, orderId) {
const projectDir = storageManager.createFolder('./storage/' + projectId.toString());
const imagesDir = storageManager.createFolder(path.join(projectDir, 'images'));
var imagePath = path.join(imagesDir, `${orderId}.jpg`);
storageManager.saveFile(imagePath, image.buffer);
console.log("[FILE] uploadMeasureImage - Image saved to: " + imagePath);
return imagePath;
}
async function getMeasureImage(projectId, orderId) {
const projectPath = `${projectId}`;
const imagePath = `${projectPath}/${orderId}.jpg`;
console.log("[FILE] getMeasureImage - Image path: " + imagePath);
return storageManager.getFile(imagePath);
}
async function getNextOrderId(projectId) {
const query = 'SELECT MAX(order_id) FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getNextOrderId - Max order_id: " + res.rows[0].max);
return res.rows[0].max + 1;
}
async function addMeasureToProject(projectId, orderId, timestamp, path, temperature, humidity) {
const query = 'INSERT INTO public.measurements (project_id, timestamp, path, temperature, humidity, order_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *';
const values = [projectId, orderId, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function getMeasurements(projectId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
return res.rows;
}
async function getMeasurement(projectId, orderId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const values = [projectId, orderId];
const res = await db.query(query, values);
return res.rows[0];
}
async function getMeasurementById(id) {
const query = 'SELECT * FROM public.measurements WHERE id = $1';
const values = [id];
const res = await db.query(query, values);
return res.rows[0];
}
async function updateMeasurement(projectId, orderId, timestamp, path, temperature, humidity) {
const query = 'UPDATE public.measurements SET timestamp = $3, path = $4, temperature = $5, humidity = $6 WHERE project_id = $1 AND order_id = $2 RETURNING *';
const values = [projectId, orderId, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function updateMeasurementById(id, timestamp, path, temperature, humidity) {
const query = 'UPDATE public.measurements SET timestamp = $2, path = $3, temperature = $4, humidity = $5 WHERE id = $1 RETURNING *';
const values = [id, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function deleteMeasurement(id) {
const query = 'DELETE FROM public.measurements WHERE id = $1';
const values = [id];
const res = await db.query(query, values);
return res.rows[0];
}
async function getPathFromIds(projectId, orderId) {
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const values = [projectId, orderId];
const res = await db.query(query, values);
return res.rows[0].path;
}
export {
uploadMeasureImage,
addMeasureToProject,
getNextOrderId,
getMeasurements,
getMeasurement,
updateMeasurement,
deleteMeasurement,
getMeasureImage,
getMeasurementById,
updateMeasurementById,
getPathFromIds
}

72
src/models/Camera.js Normal file
View File

@@ -0,0 +1,72 @@
// src/models/Camera.js
const db = require('../database/connection');
const { wrapDatabaseOperation } = require('../utils/errorHandler');
const config = require('../config');
/**
* Modèle pour la gestion de la caméra
*/
class Camera {
/**
* Récupère les paramètres de la caméra
* @returns {Promise<Object|null>} Paramètres de la caméra ou null si non trouvés
*/
static getCamera = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM camera WHERE id = 1;`;
const result = await db.query(query);
return result.rows[0] || null;
});
/**
* Met à jour les paramètres de la caméra
* @param {number} id - ID de l'entrée caméra (normalement 1)
* @param {Object} updates - Paramètres à mettre à jour
* @returns {Promise<Object|null>} Paramètres mis à jour ou null si non trouvés
*/
static updateCamera = wrapDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE camera SET ${fields} WHERE id = $1 RETURNING *;`;
const result = await db.query(query, values);
return result.rows[0] || null;
});
/**
* Supprime les paramètres de la caméra
* @param {number} id - ID de l'entrée caméra à supprimer
* @returns {Promise<void>}
*/
static deleteCamera = wrapDatabaseOperation(async (id) => {
const query = `DELETE FROM camera WHERE id = $1;`;
await db.query(query, [id]);
});
/**
* Initialise les paramètres de la caméra par défaut
* @returns {Promise<Object>} Paramètres de caméra créés
*/
static initializeCamera = wrapDatabaseOperation(async () => {
const { defaultSettings } = config.camera;
const query = `
INSERT INTO camera (id, interval, maintenance, idle)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING
RETURNING *;
`;
const result = await db.query(query, [
defaultSettings.id,
defaultSettings.interval,
defaultSettings.maintenance ? 1 : 0,
defaultSettings.idle ? 0 : 1
]);
// Si l'insertion a échoué à cause d'un conflit, récupérer l'enregistrement existant
if (result.rows.length === 0) {
return await this.getCamera();
}
return result.rows[0];
});
}
module.exports = Camera;

126
src/models/Measurement.js Normal file
View File

@@ -0,0 +1,126 @@
// src/models/Measurement.js
const db = require('../database/connection');
const { wrapDatabaseOperation } = require('../utils/errorHandler');
/**
* Modèle pour la gestion des mesures (photos avec données)
*/
class Measurement {
/**
* Récupère toutes les mesures
* @returns {Promise<Array>} Liste de toutes les mesures
*/
static getAllMeasurements = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM measurements;`;
return (await db.query(query)).rows;
});
/**
* Récupère une mesure par son ID
* @param {number} id - ID de la mesure
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
*/
static getMeasurementById = wrapDatabaseOperation(async (id) => {
const query = `SELECT * FROM measurements WHERE id = $1;`;
const result = await db.query(query, [id]);
return result.rows[0] || null;
});
/**
* Récupère une mesure par son projet ID et son ordre ID
* @param {number} projectId - ID du projet
* @param {number} orderId - ID d'ordre de la mesure
* @returns {Promise<Object|null>} Détails de la mesure ou null si non trouvée
*/
static getMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId) => {
const query = `SELECT * FROM measurements WHERE project_id = $1 AND order_id = $2;`;
const result = await db.query(query, [projectId, orderId]);
return result.rows[0] || null;
});
/**
* Récupère toutes les mesures d'un projet
* @param {number} projectId - ID du projet
* @returns {Promise<Array>} Liste des mesures du projet
*/
static getMeasurementsByProjectId = wrapDatabaseOperation(async (projectId) => {
const query = `SELECT * FROM measurements WHERE project_id = $1 ORDER BY order_id;`;
return (await db.query(query, [projectId])).rows;
});
/**
* Crée une nouvelle mesure
* @param {number} projectId - ID du projet
* @param {string} timestamp - Horodatage de la mesure
* @param {string} path - Chemin vers l'image
* @param {number} temperature - Température mesurée
* @param {number} humidity - Humidité mesurée
* @param {number} orderId - Ordre séquentiel de la mesure
* @returns {Promise<Object>} Mesure créée
*/
static createMeasurement = wrapDatabaseOperation(async (
projectId, timestamp, path, temperature, humidity, orderId
) => {
const query = `
INSERT INTO measurements (project_id, timestamp, path, temperature, humidity, order_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;
`;
const result = await db.query(
query, [projectId, timestamp, path, temperature, humidity, orderId]
);
return result.rows[0];
});
/**
* Met à jour une mesure existante
* @param {number} id - ID de la mesure
* @param {Object} updates - Champs à mettre à jour
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
*/
static updateMeasurement = wrapDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE measurements SET ${fields} WHERE id = $1 RETURNING *;`;
const result = await db.query(query, values);
return result.rows[0] || null;
});
/**
* Met à jour une mesure par projet ID et ordre ID
* @param {number} projectId - ID du projet
* @param {number} orderId - ID d'ordre de la mesure
* @param {Object} updates - Champs à mettre à jour
* @returns {Promise<Object|null>} Mesure mise à jour ou null si non trouvée
*/
static updateMeasurementByProjectAndOrderId = wrapDatabaseOperation(async (projectId, orderId, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 3}`).join(', ');
const values = [projectId, orderId, ...Object.values(updates)];
const query = `UPDATE measurements SET ${fields} WHERE project_id = $1 AND order_id = $2 RETURNING *;`;
const result = await db.query(query, values);
return result.rows[0] || null;
});
/**
* Supprime une mesure par son ID
* @param {number} id - ID de la mesure
* @returns {Promise<void>}
*/
static deleteMeasurement = wrapDatabaseOperation(async (id) => {
const query = `DELETE FROM measurements WHERE id = $1;`;
await db.query(query, [id]);
});
/**
* Obtient le prochain ordre ID disponible pour un projet
* @param {number} projectId - ID du projet
* @returns {Promise<number>} Prochain ordre ID
*/
static getNextOrderId = wrapDatabaseOperation(async (projectId) => {
const query = `SELECT COALESCE(MAX(order_id), 0) + 1 AS next_order_id FROM measurements WHERE project_id = $1;`;
const result = await db.query(query, [projectId]);
return result.rows[0].next_order_id;
});
}
module.exports = Measurement;

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

@@ -0,0 +1,109 @@
// src/models/Project.js
const db = require('../database/connection');
const { wrapDatabaseOperation } = require('../utils/errorHandler');
const config = require('../config');
/**
* Modèle pour la gestion des projets
*/
class Project {
/**
* Récupère tous les projets
* @returns {Promise<Array>} Liste de tous les projets
*/
static getAllProjects = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM projects;`;
return (await db.query(query)).rows;
});
/**
* Récupère un projet par son ID
* @param {number} id - ID du projet
* @returns {Promise<Object|null>} Détails du projet ou null si non trouvé
*/
static getProjectById = wrapDatabaseOperation(async (id) => {
const query = `SELECT * FROM projects WHERE id = $1;`;
const result = await db.query(query, [id]);
return result.rows[0] || null;
});
/**
* Crée un nouveau projet
* @param {string} name - Nom du projet
* @param {string} description - Description du projet
* @param {Date} startDate - Date de début du projet
* @param {number} status - Statut du projet
* @returns {Promise<Object>} Projet créé
*/
static createProject = wrapDatabaseOperation(async (name, description, startDate, status) => {
const query = `
INSERT INTO projects (name, description, start_date, status)
VALUES ($1, $2, $3, $4)
RETURNING *;
`;
const result = await db.query(query, [name, description, startDate, status]);
return result.rows[0];
});
/**
* Met à jour un projet existant
* @param {number} id - ID du projet
* @param {Object} updates - Champs à mettre à jour
* @returns {Promise<Object|null>} Projet mis à jour ou null si non trouvé
*/
static updateProject = wrapDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE projects SET ${fields} WHERE id = $1 RETURNING *;`;
const result = await db.query(query, values);
return result.rows[0] || null;
});
/**
* Met à jour le statut d'un projet
* @param {number} id - ID du projet
* @param {number} status - Nouveau statut du projet
* @returns {Promise<Object|null>} Projet mis à jour ou null si non trouvé
*/
static updateProjectStatus = wrapDatabaseOperation(async (id, status) => {
const query = `UPDATE projects SET status = $1 WHERE id = $2 RETURNING *;`;
const result = await db.query(query, [status, id]);
// Ajout du statut stopping pour les projets en cours d'arrêt
if (status === config.projectStatus.stopping) {
console.log(`[PROJECT] Projet ${id} mis à jour avec le statut 'stopping'.`);
}
return result.rows[0] || null;
});
/**
* Supprime un projet par son ID
* @param {number} id - ID du projet
* @returns {Promise<void>}
*/
static deleteProject = wrapDatabaseOperation(async (id) => {
const query = `DELETE FROM projects WHERE id = $1;`;
await db.query(query, [id]);
});
/**
* Trouve le projet en cours de rendu (status = capturing)
* @returns {Promise<Object|null>} Projet en cours de rendu ou null
*/
static findCurrentRenderingProject = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM projects WHERE status = ${config.projectStatus.capturing};`;
const result = await db.query(query);
return result.rows[0] || null;
});
/**
* Trouve le projet en cours d'arrêt (status = stopping)
* @returns {Promise<Object|null>} Projet en cours d'arrêt ou null
*/
static findStoppingProject = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM projects WHERE status = ${config.projectStatus.stopping};`;
const result = await db.query(query);
return result.rows[0] || null;
});
}
module.exports = Project;

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

@@ -0,0 +1,147 @@
// src/models/Video.js
const db = require('../database/connection');
const { wrapDatabaseOperation } = require('../utils/errorHandler');
const config = require('../config');
/**
* Modèle pour la gestion des vidéos
*/
class Video {
/**
* Récupère toutes les vidéos
* @returns {Promise<Array>} Liste de toutes les vidéos
*/
static getAllVideos = wrapDatabaseOperation(async () => {
const query = `SELECT * FROM videos;`;
return (await db.query(query)).rows;
});
/**
* Récupère une vidéo par son ID
* @param {number} id - ID de la vidéo
* @returns {Promise<Object|null>} Détails de la vidéo ou null si non trouvée
*/
static getVideoById = wrapDatabaseOperation(async (id) => {
const query = `SELECT * FROM videos WHERE id = $1;`;
const result = await db.query(query, [id]);
return result.rows[0] || null;
});
/**
* Récupère toutes les vidéos d'un projet
* @param {number} projectId - ID du projet
* @returns {Promise<Array>} Liste des vidéos du projet
*/
static getVideosByProjectId = wrapDatabaseOperation(async (projectId) => {
const query = `SELECT * FROM videos WHERE project_id = $1;`;
return (await db.query(query, [projectId])).rows;
});
/**
* Crée une nouvelle vidéo
* @param {number} projectId - ID du projet
* @param {string} measurementIds - IDs des mesures (format JSON)
* @param {string} name - Nom de la vidéo
* @param {string} resolution - Résolution de la vidéo
* @param {number} duration - Durée de la vidéo en secondes
* @param {number} status - Statut de la vidéo (défaut: 0 = en attente)
* @returns {Promise<Object>} Vidéo créée
*/
static createVideo = wrapDatabaseOperation(async (
projectId, measurementIds, name, resolution, duration, status = config.videoStatus.rendering
) => {
const query = `
INSERT INTO videos (project_id, measurement_ids, name, resolution, duration, status)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;
`;
const result = await db.query(
query, [projectId, measurementIds, name, resolution, duration, status]
);
return result.rows[0];
});
/**
* Met à jour une vidéo existante
* @param {number} id - ID de la vidéo
* @param {Object} updates - Champs à mettre à jour
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
*/
static updateVideo = wrapDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE videos SET ${fields} WHERE id = $1 RETURNING *;`;
const result = await db.query(query, values);
return result.rows[0] || null;
});
/**
* Met à jour le chemin du fichier vidéo
* @param {number} id - ID de la vidéo
* @param {string} videoFile - Chemin du fichier vidéo
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
*/
static updateVideoFilePath = wrapDatabaseOperation(async (id, videoFile) => {
const query = `UPDATE videos SET video_file = $1 WHERE id = $2 RETURNING *;`;
const result = await db.query(query, [videoFile, id]);
return result.rows[0] || null;
});
/**
* Met à jour le statut d'une vidéo
* @param {number} id - ID de la vidéo
* @param {number} status - Nouveau statut de la vidéo
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
*/
static async updateVideoStatus(id, status) {
const query = `UPDATE videos SET status = $1 WHERE id = $2 RETURNING *;`;
const result = await db.query(query, [status, id]);
return result.rows[0] || null;
}
/**
* Supprime une vidéo par son ID
* @param {number} id - ID de la vidéo
* @returns {Promise<void>}
*/
static deleteVideo = wrapDatabaseOperation(async (id) => {
const query = `DELETE FROM videos WHERE id = $1;`;
await db.query(query, [id]);
});
/**
* Récupère toutes les vidéos non terminées (statut 0, 2 ou 3)
* @returns {Promise<Array>} Liste des vidéos non terminées
*/
static getUnfinishedVideos = wrapDatabaseOperation(async () => {
const query = `
SELECT * FROM videos
WHERE status IN (
${config.videoStatus.rendering},
${config.videoStatus.error},
0
);
`;
return (await db.query(query)).rows;
});
/**
* Met à jour la progression d'une vidéo en cours de rendu
* @param {number} id - ID de la vidéo
* @param {number} progress - Pourcentage de progression
* @param {number} eta - Temps estimé restant en secondes
* @returns {Promise<Object|null>} Vidéo mise à jour ou null si non trouvée
*/
static updateVideoProgress = wrapDatabaseOperation(async (id, progress, eta) => {
const query = `
UPDATE videos
SET progress = $1, eta = $2, updated_at = NOW()
WHERE id = $3
RETURNING *;
`;
const result = await db.query(query, [progress, eta, id]);
return result.rows[0] || null;
});
}
module.exports = Video;

144
src/models/database.js Normal file
View File

@@ -0,0 +1,144 @@
// src/models/database.js
const db = require('../database/connection');
const { wrapDatabaseOperation } = require('../utils/errorHandler');
const config = require('../config');
/**
* Module gérant l'initialisation et la maintenance de la structure de la base de données
*/
class DatabaseManager {
/**
* Crée les tables de la base de données si elles n'existent pas déjà
*/
static async createDatabase() {
const queries = [
`CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
start_date DATE,
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3]))
);`,
`ALTER TABLE projects OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS measurements (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
timestamp TIMESTAMP NOT NULL,
path VARCHAR(255),
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
order_id INTEGER NOT NULL,
CONSTRAINT unique_project_photo_order UNIQUE (project_id, order_id)
);`,
`ALTER TABLE measurements OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS videos (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
measurement_ids TEXT NOT NULL,
video_file VARCHAR(255),
resolution VARCHAR(255),
duration INTEGER,
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3])),
name VARCHAR(255),
progress DOUBLE PRECISION,
started_at TIMESTAMP,
updated_at TIMESTAMP,
eta DOUBLE PRECISION
);`,
`ALTER TABLE videos OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS camera (
id SERIAL PRIMARY KEY,
interval INTEGER NOT NULL,
maintenance INTEGER NOT NULL,
active INTEGER DEFAULT 0 NOT NULL
);`,
`ALTER TABLE camera OWNER TO timelapse;`
];
try {
for (const query of queries) {
await db.query(query);
}
console.log('[DB] Tables créées ou vérifiées avec succès.');
} catch (err) {
console.error('[DB] Erreur lors de la création des tables:', err);
throw err;
}
}
/**
* Vérifie l'existence des tables requises dans la base de données
*/
static async checkDatabaseExistence() {
const query = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('projects', 'measurements', 'videos', 'camera');
`;
try {
const result = await db.query(query);
const existingTables = result.rows.map(row => row.table_name);
const requiredTables = ['projects', 'measurements', 'videos', 'camera'];
const missingTables = requiredTables.filter(table => !existingTables.includes(table));
if (missingTables.length > 0) {
console.error('[DB] Tables manquantes ou incorrectement construites:', missingTables);
throw new Error(`Les tables suivantes sont manquantes ou incorrectement construites: ${missingTables.join(', ')}`);
} else {
console.log('[DB] Toutes les tables requises existent et sont correctement construites.');
}
} catch (err) {
console.error('[DB] Erreur lors de la vérification des tables:', err);
throw err;
}
}
/**
* Supprime toutes les tables de la base de données
*/
static async deleteDatabase() {
const queries = [
`DROP TABLE IF EXISTS videos;`,
`DROP TABLE IF EXISTS measurements;`,
`DROP TABLE IF EXISTS projects;`,
`DROP TABLE IF EXISTS camera;`
];
try {
for (const query of queries) {
await db.query(query);
}
console.log('[DB] Tables supprimées avec succès.');
} catch (err) {
console.error('[DB] Erreur lors de la suppression des tables:', err);
throw err;
}
}
/**
* Initialise la base de données en vérifiant et en créant les tables si nécessaire
*/
static async initialize() {
try {
await this.checkDatabaseExistence();
console.log('[DB] Structure de base de données validée');
} catch (err) {
console.error('[DB] Vérification de la base de données échouée:', err);
try {
await this.deleteDatabase();
await this.createDatabase();
console.log('[DB] Base de données initialisée avec succès.');
} catch (err) {
console.error('[DB] Erreur lors de l\'initialisation de la base de données:', err);
throw err;
}
} finally {
console.log('[DB] Processus d\'initialisation de la base de données terminé.');
}
}
}
module.exports = DatabaseManager;

View File

@@ -1,82 +0,0 @@
import storageManager from '../data/storageManager.js';
import db from '../../db.js';
function createProjectDirectory(projectId) {
const projectPath = `${projectId}`;
storageManager.createFolder(projectPath);
storageManager.createFolder(`${projectPath}/images`);
storageManager.createFolder(`${projectPath}/videos`);
console.log("[FILE] createProjectDirectory : " + projectPath);
}
function deleteProjectDirectory(projectId) {
const projectPath = `${projectId}`;
storageManager.deleteFolder(projectPath);
console.log("[FILE] deleteProjectDirectory : " + projectPath);
}
async function getAllProjects() {
const query = 'SELECT * FROM public.projects';
const res = await db.query(query);
console.log("[DB] getAllProjects : ", res.rows);
return res.rows;
}
async function getProjectById(projectId) {
const query = 'SELECT * FROM public.projects WHERE id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getProjectById : ", res.rows[0]);
return res.rows[0];
}
async function createProject(name, description, start_date, status) {
const query = 'INSERT INTO public.projects (name, description, start_date, status) VALUES ($1, $2, $3, $4) RETURNING *';
const values = [name, description, start_date, status];
const res = await db.query(query, values);
console.log("[DB] createProject : ", res.rows[0]);
return res.rows[0];
}
async function editProjectById(projectID, name, description, startDate, status) {
const query = 'UPDATE public.projects SET name = $1, description = $2, start_date = $3, status = $4 WHERE id = $5 RETURNING *';
const values = [name, description, startDate, status, projectID];
const res = await db.query(query, values);
console.log("[DB] editProjectById : ", res.rows[0]);
return res.rows[0];
}
async function deleteProjectById(projectId) {
const query = 'DELETE FROM public.projects WHERE id = $1';
const values = [projectId];
console.log("[DB] deleteProjectById : ", values);
await db.query(query, values);
}
async function getVideosByProjectId(projectId) {
const query = 'SELECT * FROM public.videos WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getVideosByProjectId : ", res.rows);
return res.rows;
}
async function getMeasurementsByProjectId(projectId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getMeasurementsByProjectId : ", res.rows);
return res.rows;
}
export {
createProjectDirectory,
deleteProjectDirectory,
getAllProjects,
getProjectById,
createProject,
editProjectById,
deleteProjectById,
getVideosByProjectId,
getMeasurementsByProjectId
};

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

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

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

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

34
src/routes/index.js Normal file
View File

@@ -0,0 +1,34 @@
// src/routes/index.js
const express = require('express');
const router = express.Router();
const cors = require('cors');
const config = require('../config');
// Importe toutes les routes modulaires
const projectRoutes = require('./projectRoutes');
const measurementRoutes = require('./measurementRoutes');
const videoRoutes = require('./videoRoutes');
const imageRoutes = require('./imageRoutes');
const cameraRoutes = require('./cameraRoutes');
// Configuration CORS
router.use(cors({
origin: config.server.cors.origins,
methods: config.server.cors.methods,
allowedHeaders: config.server.cors.allowedHeaders,
credentials: config.server.cors.credentials
}));
// Enregistre toutes les routes
router.use(projectRoutes);
router.use(measurementRoutes);
router.use(videoRoutes);
router.use(imageRoutes);
router.use(cameraRoutes);
// Route de test/santé de l'API
router.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
module.exports = router;

View File

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

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

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

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

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

View File

@@ -0,0 +1,209 @@
// src/services/storageService.js
const fs = require('fs').promises;
const path = require('path');
const { Buffer } = require('buffer');
const config = require('../config');
/**
* Service de gestion du stockage des fichiers
*/
class StorageService {
/**
* Crée un dossier s'il n'existe pas déjà
* @param {string} dirPath - Chemin du dossier à créer
* @returns {Promise<string>} Chemin du dossier créé
*/
static async createDirectory(dirPath) {
try {
await fs.access(dirPath);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(dirPath, { recursive: true });
} else {
throw error;
}
}
return dirPath;
}
/**
* Supprime un dossier et son contenu
* @param {string} dirPath - Chemin du dossier à supprimer
*/
static async deleteDirectory(dirPath) {
try {
await fs.access(dirPath);
await fs.rm(dirPath, { recursive: true, force: true });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
/**
* Cherche toutes les images dans un dossier
* @param {string} dirPath - Dossier à scanner
* @returns {Promise<Array<string>>} Liste des chemins d'images trouvées
*/
static async scanImages(dirPath = 'storage') {
const basePath = path.join(config.paths.storage, dirPath);
let results = [];
try {
await fs.access(basePath);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(basePath, { recursive: true });
} else {
throw error;
}
}
async function scanDirectory(directory) {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await scanDirectory(filePath);
} else if (file.endsWith('.jpg')) {
results.push(filePath);
}
}
}
await scanDirectory(basePath);
return results;
}
/**
* Enregistre un contenu dans un fichier
* @param {string} filePath - Chemin du fichier
* @param {Buffer} content - Contenu à enregistrer
*/
static async saveFile(filePath, content) {
const dirPath = path.dirname(filePath);
await this.createDirectory(dirPath);
if (Buffer.isBuffer(content)) {
await fs.writeFile(filePath, content);
} else {
throw new Error('Le contenu doit être un buffer');
}
}
/**
* Récupère le contenu d'un fichier
* @param {string} filePath - Chemin du fichier
* @returns {Promise<Buffer>} Contenu du fichier
*/
static async getFile(filePath) {
return await fs.readFile(filePath);
}
/**
* Supprime un fichier
* @param {string} filePath - Chemin du fichier à supprimer
* @returns {Promise<string>} Message de confirmation
*/
static async deleteFile(filePath) {
try {
await fs.access(filePath);
await fs.rm(filePath);
return `Fichier ${filePath} supprimé avec succès.`;
} catch (error) {
if (error.code === 'ENOENT') {
return `Fichier ${filePath} inexistant.`;
} else {
throw error;
}
}
}
/**
* Gestionnaire pour les opérations de projet
*/
static project = {
/**
* Crée le répertoire d'un projet
* @param {number} projectId - ID du projet
*/
createProjectDirectory: async function(projectId) {
const projectPath = path.join(config.paths.storage, `${projectId}`);
await StorageService.createDirectory(projectPath);
await StorageService.createDirectory(path.join(projectPath, 'images'));
await StorageService.createDirectory(path.join(projectPath, 'videos'));
console.log(`[STORAGE] Répertoire créé : ${projectPath}`);
},
/**
* Supprime le répertoire d'un projet et son contenu
* @param {number} projectId - ID du projet
*/
deleteProjectDirectory: async function(projectId) {
const projectPath = path.join(config.paths.storage, `${projectId}`);
await StorageService.deleteDirectory(projectPath);
console.log(`[STORAGE] Répertoire supprimé : ${projectPath}`);
}
};
/**
* Gestionnaire pour les opérations de mesures (images)
*/
static measurement = {
/**
* Récupère l'image d'une mesure
* @param {number} projectId - ID du projet
* @param {number} orderId - ID d'ordre de la mesure
* @returns {Promise<Buffer>} Contenu de l'image
*/
getMeasurementImage: async function(projectId, orderId) {
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
console.log(`[STORAGE] Récupération de l'image : ${imagePath}`);
return await StorageService.getFile(imagePath);
},
/**
* Enregistre l'image d'une mesure
* @param {Object} image - Objet image avec buffer
* @param {number} projectId - ID du projet
* @param {number} orderId - ID d'ordre de la mesure
* @returns {Promise<string>} Chemin de l'image enregistrée
*/
uploadMeasurementImage: async function(image, projectId, orderId) {
const imagePath = path.join(config.paths.storage, `${projectId}`, 'images', `${orderId}.jpg`);
console.log(`[STORAGE] Enregistrement de l'image : ${imagePath}`);
await StorageService.saveFile(imagePath, image.buffer);
return imagePath;
}
};
/**
* Gestionnaire pour les opérations de vidéos
*/
static video = {
/**
* Récupère une vidéo
* @param {number} projectId - ID du projet
* @param {number} videoId - ID de la vidéo
* @returns {Promise<Buffer>} Contenu de la vidéo
*/
getVideo: async function(projectId, videoId) {
const videoPath = path.join(config.paths.storage, `${projectId}`, 'videos', `${videoId}.mp4`);
console.log(`[STORAGE] Récupération de la vidéo : ${videoPath}`);
return await StorageService.getFile(videoPath);
},
/**
* Supprime une vidéo
* @param {string} videoPath - Chemin de la vidéo à supprimer
* @returns {Promise<string>} Message de confirmation
*/
deleteVideo: async function(videoPath) {
console.log(`[STORAGE] Suppression de la vidéo : ${videoPath}`);
return await StorageService.deleteFile(videoPath);
}
};
}
module.exports = StorageService;

View File

@@ -0,0 +1,218 @@
// src/services/videoService.js
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const config = require('../config');
const Video = require('../models/Video');
/**
* Service de gestion des opérations vidéo
*/
class VideoService {
/**
* Crée une vidéo à partir d'une liste d'images
* @param {number} projectId - ID du projet
* @param {Array<string>} pathList - Liste des chemins d'images
* @param {number} duration - Durée souhaitée en secondes
* @param {number} videoId - ID de la vidéo
* @param {number} resWidth - Largeur de la résolution
* @param {number} resHeight - Hauteur de la résolution
* @returns {Promise<string>} Chemin du fichier vidéo créé
*/
static async createVideoFromImages(projectId, pathList, duration, videoId, resWidth, resHeight) {
const tempFile = path.join('temp.txt');
let ffmpegProcess;
let cleanupDone = false;
try {
// Configuration des chemins
const workdir = path.join(config.paths.storage, projectId.toString());
if (!fs.existsSync(workdir)) {
fs.mkdirSync(workdir, { recursive: true });
}
// Vérifie que des images ont été fournies
if (!Array.isArray(pathList) || pathList.length === 0) {
throw new Error('Liste d\'images vide ou invalide');
}
// Tri des images par ordre numérique
const sortedImages = pathList.sort((a, b) => {
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
return numA - numB;
});
// Création du fichier temporaire pour FFmpeg
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
// Calcul des paramètres vidéo
const totalFrames = sortedImages.length;
const frameRate = Math.ceil(totalFrames / parseInt(duration));
const timestamp = Date.now();
const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0];
const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0];
const outputVideo = path.join(
workdir,
`${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`
);
// Mise à jour initiale du statut vidéo
await Video.updateVideo(videoId, {
status: config.videoStatus.rendering,
progress: 0,
started_at: new Date(),
updated_at: new Date(),
eta: null
});
// Configuration du scaling vidéo
const scale = resWidth && resHeight ? `scale=${resWidth}:${resHeight}` : 'scale=854:480';
// Configuration des arguments FFmpeg
const ffmpegArgs = [
'-y', // Écrase les fichiers existants sans demander
'-r', frameRate.toString(), // Framerate
'-f', 'concat', // Format de concaténation
'-safe', '0', // Autorise les chemins non sécurisés
'-i', tempFile, // Fichier d'entrée
'-vsync', 'vfr', // Synchronisation vidéo
'-pix_fmt', 'yuv420p', // Format de pixels
'-vf', scale, // Filter vidéo pour le scaling
'-b:v', '1500k', // Bitrate vidéo
outputVideo // Fichier de sortie
];
// Lancement du processus FFmpeg
ffmpegProcess = spawn('ffmpeg', ffmpegArgs, {
stdio: ['ignore', 'ignore', 'pipe']
});
let lastUpdate = 0;
const startTime = Date.now();
// Capture de la progression du rendu
ffmpegProcess.stderr.on('data', (data) => {
const output = data.toString();
const frameMatch = output.match(/frame=\s*(\d+)/);
if (frameMatch) {
const currentFrame = parseInt(frameMatch[1], 10);
const progress = Math.min((currentFrame / totalFrames) * 100, 99.99);
const now = Date.now();
// Calcul du temps restant estimé
const elapsedSeconds = (now - startTime) / 1000;
const eta = elapsedSeconds / (currentFrame / totalFrames) - elapsedSeconds;
// Mise à jour périodique (max toutes les 500ms)
if (now - lastUpdate > 500) {
Video.updateVideo(videoId, {
progress: progress,
eta: Math.round(eta),
updated_at: new Date()
});
console.log(`[VIDEO] Progression: ${progress.toFixed(2)}%, ETA: ${eta.toFixed(0)}s`);
lastUpdate = now;
}
}
});
// Attente de la fin du rendu vidéo
await new Promise((resolve, reject) => {
ffmpegProcess.on('close', async (code) => {
if (code === 0) {
try {
// Mise à jour finale de la vidéo
await Video.updateVideo(videoId, {
status: config.videoStatus.completed,
progress: 100,
eta: 0,
video_file: outputVideo,
updated_at: new Date()
});
resolve();
} catch (e) {
reject(e);
}
} else {
reject(new Error(`FFmpeg s'est terminé avec le code ${code}`));
}
});
ffmpegProcess.on('error', reject);
});
return outputVideo;
} catch (error) {
// Gestion des erreurs
console.error('[VIDEO] Erreur lors de la création de la vidéo:', error);
try {
// Mise à jour du statut vidéo en cas d'erreur
await Video.updateVideo(videoId, {
status: config.videoStatus.error,
progress: 0,
eta: null,
updated_at: new Date()
});
} catch (dbError) {
console.error('[VIDEO] Erreur lors de la mise à jour de la base de données:', dbError);
}
throw error;
} finally {
// Nettoyage des ressources
if (!cleanupDone) {
if (tempFile && fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
if (ffmpegProcess) {
ffmpegProcess.kill();
}
cleanupDone = true;
}
}
}
/**
* Récupère les informations de progression d'une vidéo
* @param {number} videoId - ID de la vidéo
* @returns {Promise<Object>} Informations de progression
*/
static async getVideoProgress(videoId) {
const video = await Video.getVideoById(videoId);
if (!video) {
throw new Error('Vidéo non trouvée');
}
return {
progress: video.progress,
elapsed: video.started_at ? Math.floor((new Date() - new Date(video.started_at)) / 1000) : 0,
eta: video.eta,
status: video.status
};
}
/**
* Convertit un code de statut en libellé
* @param {number} status - Code de statut
* @returns {string} Libellé du statut
*/
static getStatusLabel(status) {
const statusMap = {
[config.videoStatus.rendering]: 'En cours',
[config.videoStatus.completed]: 'Terminé',
[config.videoStatus.error]: 'Échec',
0: 'En attente'
};
return statusMap[status] || 'Inconnu';
}
}
module.exports = VideoService;

58
src/utils/errorHandler.js Normal file
View File

@@ -0,0 +1,58 @@
// src/utils/errorHandler.js
/**
* Envoie une réponse d'erreur standardisée
* @param {string} message - Message d'erreur à afficher
* @param {Object} res - Objet Response d'Express
* @param {Error|null} error - Objet d'erreur original (facultatif)
* @param {number} statusCode - Code HTTP d'erreur (défaut: 500)
*/
function sendError(message, res, error = null, statusCode = 500) {
console.error(`[ERROR] ${message}`, error);
res.status(statusCode).json({
error: {
message,
statusCode,
details: error ? (error.message || String(error)) : null,
timestamp: new Date().toISOString()
}
});
}
/**
* Wrapper pour les contrôleurs qui gère automatiquement les erreurs
* @param {Function} controller - Fonction de contrôleur à exécuter
* @returns {Function} - Middleware Express qui gère les erreurs
*/
function asyncHandler(controller) {
return async (req, res, next) => {
try {
await controller(req, res, next);
} catch (error) {
sendError('Une erreur est survenue lors du traitement de la requête', res, error);
}
};
}
/**
* Crée un wrapper pour les opérations de base de données qui gère les erreurs
* @param {Function} operation - Fonction à exécuter
* @returns {Function} - Fonction qui execute l'opération et gère les erreurs
*/
function wrapDatabaseOperation(operation) {
return async (...args) => {
try {
return await operation(...args);
} catch (err) {
console.error(`[DB ERROR] Erreur lors de l'opération ${operation.name || 'database'}:`, err);
throw err;
}
};
}
module.exports = {
sendError,
asyncHandler,
wrapDatabaseOperation
};

View File

@@ -1,111 +1,13 @@
const fs = require('fs'); /**
const path = require('path'); * Ce fichier est conservé pour la rétrocompatibilité mais redirige vers le nouveau service vidéo.
const { execSync } = require('child_process'); * Il sera progressivement supprimé lorsque toutes les références auront été mises à jour.
*/
const serverError = require('../../utils/serverError'); const VideoService = require('../services/videoService');
const db = require('../../db');
const storageManager = require('../data/storageManager');
const measureManager = require('../measure/measureManager');
const PROJECTS_DIR = path.join('.'); // Fonction de pont pour maintenir la compatibilité avec l'ancien code
async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) {
async function createVideoWithList(projectId, pathList) { return await VideoService.createVideoFromImages(projectId, pathList, duration, videoId, res_width, res_height);
//pathList étant la liste des chemins déjà triés
const tempFile = path.join('temp.txt');
try {
// Trouver tous les fichiers image pour le projet donné
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images');
console.log('dir:', dir);
const images = pathList;
console.log('images:', images);
// Trier les images numériquement
const sortedImages = images.sort((a, b) => {
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
return numA - numB;
});
// En déduire l'id de la première et dernière image utilisée
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
console.log('firstImageId:', firstImageId);
console.log('lastImageId:', lastImageId);
// Créer un fichier temporaire pour la liste des images
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
const frameRate = 10;
// le fichier final prend cette forme : {projectId}_{firstImageId}_{lastImageId}-{timestamp}.mp4
const timestamp = new Date().getTime();
const outputVideo = path.join(workdir, `${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`);
// Commande ffmpeg pour créer la vidéo
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
console.log('Running ffmpeg command:', ffmpegCommand);
execSync(ffmpegCommand);
console.log('Video created successfully:', outputVideo);
return outputVideo;
} catch (error) {
console.error('Error creating video:', error);
serverError(error);
} finally {
// Supprimer le fichier temporaire
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
console.log('Temporary file deleted:', tempFile);
}
}
} }
async function createVideo(projectId) { module.exports = { createVideoWithList };
const tempFile = path.join('temp.txt');
try {
// Trouver tous les fichiers image pour le projet donné
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images');
console.log('dir:', dir);
const images = storageManager.scanAllImages(dir);
console.log('images:', images);
// Trier les images numériquement
const sortedImages = images.sort((a, b) => {
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
return numA - numB;
});
// En déduire l'id de la première et dernière image utilisée
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
console.log('firstImageId:', firstImageId);
console.log('lastImageId:', lastImageId);
// Créer un fichier temporaire pour la liste des images
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
const frameRate = 10;
const outputVideo = path.join(workdir, 'video.mp4');
// Commande ffmpeg pour créer la vidéo
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
console.log('Running ffmpeg command:', ffmpegCommand);
execSync(ffmpegCommand);
console.log('Video created successfully:', outputVideo);
} catch (error) {
console.error('Error creating video:', error);
serverError(error);
} finally {
// Supprimer le fichier temporaire
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
console.log('Temporary file deleted:', tempFile);
}
}
}
module.exports = { createVideo, createVideoWithList };

View File

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

View File

@@ -1,51 +1,14 @@
const storageManager = require('../src/data/storageManager'); import path from 'path';
const videoManager = require('../src/video/videoManager'); import { fileURLToPath } from 'url';
const measureManager = require('../src/measure/measureManager');
const path = require('path');
// console.log('Testing database functions...'); const __dirname = path.dirname(fileURLToPath(import.meta.url));
try {
storageManager.createFolder('test_folder');
console.log('1 - Folder created');
storageManager.deleteFolder('test_folder');
console.log('2 - Folder deleted');
} catch (error) {
console.error('Error testing database functions:', error);
}
function getSmileImage() { function getSmileImage() {
return path.join(__dirname, '../sample/smile.png'); return path.join(__dirname, '../sample/smile.png');
} }
//test de lancement d'une création de vidéo sur le projet 1 function getCatVideo() {
// videoManager.createVideo(1).then(res => { return path.join(__dirname, '../sample/cat.mp4');
// console.log('3 - Video created:', res); }
// }).catch(err => {
// console.error('Error creating video:', err);
// });
// async function run() {
// var Path = await measureManager.getPathFromIds(1, 1);
// console.log(Path);
// }
// run().catch(err => { export { getSmileImage, getCatVideo };
// console.error('Error:', err);
// });
var pathList = [
'storage/1/images/1.jpg',
'storage/1/images/10.jpg',
'storage/1/images/20.jpg',
'storage/1/images/30.jpg',
];
videoManager.createVideoWithList(1, pathList).then(res => {
console.log('3 - Video created:', res);
return storageManager.deleteFile(res);
}).then(res => {
console.log('4 - Video deleted:', res);
}).catch(err => {
console.error('Error:', err);
});
exports.getSmileImage = getSmileImage;

View File

@@ -1,6 +1,12 @@
function sendError(comment, res, err) { function sendError(comment, res = { status: () => ({ json: () => {} }) }, err = null, code = 500) {
console.error(comment, err); console.error(comment, err);
res.status(500).send('Server error'); res.status(code).json({
error: {
message: comment,
code: code,
error: err
}
});
} }
module.exports = { module.exports = {