feat(SessionManager): Gestion avancée des médias (Rename/Delete)
- Implémentation renameMedia avec retry (fix Windows file locking) - Implémentation deleteMedia via MQTT - Sécurisation des chemins d'accès (Directory Traversal prevention)
This commit is contained in:
@@ -21,7 +21,9 @@ const {
|
|||||||
mqttSessionListTopic,
|
mqttSessionListTopic,
|
||||||
mqttSessionListResponseTopic,
|
mqttSessionListResponseTopic,
|
||||||
mqttSessionCreateTopic,
|
mqttSessionCreateTopic,
|
||||||
mqttSessionDeleteTopic
|
mqttSessionDeleteTopic,
|
||||||
|
mqttSessionDeleteMediaTopic,
|
||||||
|
mqttSessionRenameMediaTopic
|
||||||
},
|
},
|
||||||
httpPort
|
httpPort
|
||||||
}
|
}
|
||||||
@@ -116,6 +118,8 @@ client.on('connect', () => {
|
|||||||
client.subscribe(mqttSessionListTopic);
|
client.subscribe(mqttSessionListTopic);
|
||||||
client.subscribe(mqttSessionCreateTopic);
|
client.subscribe(mqttSessionCreateTopic);
|
||||||
client.subscribe(mqttSessionDeleteTopic);
|
client.subscribe(mqttSessionDeleteTopic);
|
||||||
|
client.subscribe(mqttSessionDeleteMediaTopic);
|
||||||
|
client.subscribe(mqttSessionRenameMediaTopic);
|
||||||
|
|
||||||
console.log(`[INFO] Abonné aux topics session`);
|
console.log(`[INFO] Abonné aux topics session`);
|
||||||
});
|
});
|
||||||
@@ -163,6 +167,26 @@ client.on('message', (topic, message) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ERREUR] Impossible de parser la demande de suppression', e);
|
console.error('[ERREUR] Impossible de parser la demande de suppression', e);
|
||||||
}
|
}
|
||||||
|
} else if (topic === mqttSessionDeleteMediaTopic) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(message.toString());
|
||||||
|
console.log(`[INFO] Demande de suppression de média: ${payload.MediaPath} pour session: ${payload.SessionId}`);
|
||||||
|
if (payload.SessionId && payload.MediaPath) {
|
||||||
|
deleteMedia(payload.SessionId, payload.MediaPath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERREUR] Impossible de parser la demande de suppression de média', e);
|
||||||
|
}
|
||||||
|
} else if (topic === mqttSessionRenameMediaTopic) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(message.toString());
|
||||||
|
console.log(`[INFO] Demande de renommage de média: ${payload.OldPath} -> ${payload.NewName}`);
|
||||||
|
if (payload.SessionId && payload.OldPath && payload.NewName) {
|
||||||
|
renameMedia(payload.SessionId, payload.OldPath, payload.NewName);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERREUR] Impossible de parser la demande de renommage', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,6 +309,91 @@ function deleteSession(sessionId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteMedia(sessionId, relativePath) {
|
||||||
|
// Sécuriser le chemin
|
||||||
|
// relativePath devrait être du type "/assets/..."
|
||||||
|
if (!relativePath) return;
|
||||||
|
|
||||||
|
// Nettoyer le chemin (retirer le slash initial s'il existe)
|
||||||
|
if (relativePath.startsWith('/')) relativePath = relativePath.substring(1);
|
||||||
|
|
||||||
|
const fullPath = path.join(quizzDir, sessionId, relativePath);
|
||||||
|
|
||||||
|
// Vérifier que le chemin reste dans le dossier de la session (protection directory traversal)
|
||||||
|
if (!fullPath.startsWith(path.join(quizzDir, sessionId))) {
|
||||||
|
console.error(`[ERREUR] Tentative de suppression hors session: ${fullPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
console.error(`[ERREUR] Le fichier ${fullPath} n'existe pas`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
console.log(`[INFO] Média supprimé: ${fullPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[ERREUR] Impossible de supprimer le fichier ${fullPath}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
client.on('error', (error) => {
|
client.on('error', (error) => {
|
||||||
console.error('[ERREUR] Erreur de connexion au broker MQTT:', error.message);
|
console.error('[ERREUR] Erreur de connexion au broker MQTT:', error.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper pour les renames tenaces sur Windows
|
||||||
|
function renameWithRetry(oldPath, newPath, retries = 5, delay = 100) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(oldPath, newPath);
|
||||||
|
console.log(`[INFO] Média renommé : ${oldPath} -> ${newPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
if (retries > 0) {
|
||||||
|
console.log(`[WARN] Échec renommage, nouvel essai dans ${delay}ms... (${retries} restants)`);
|
||||||
|
setTimeout(() => {
|
||||||
|
renameWithRetry(oldPath, newPath, retries - 1, delay * 2);
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
console.error(`[ERREUR] Impossible de renommer le fichier après plusieurs essais`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameMedia(sessionId, oldRelativePath, newName) {
|
||||||
|
if (!oldRelativePath || !newName) return;
|
||||||
|
|
||||||
|
// Clean paths
|
||||||
|
if (oldRelativePath.startsWith('/')) oldRelativePath = oldRelativePath.substring(1);
|
||||||
|
|
||||||
|
const oldFullPath = path.join(quizzDir, sessionId, oldRelativePath);
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!oldFullPath.startsWith(path.join(quizzDir, sessionId))) {
|
||||||
|
console.error(`[ERREUR] Tentative de renommage hors session`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(oldFullPath)) {
|
||||||
|
console.error(`[ERREUR] Fichier à renommer introuvable: ${oldFullPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = path.dirname(oldFullPath);
|
||||||
|
const ext = path.extname(oldFullPath);
|
||||||
|
// newName comes as "Q-005", we add the extension
|
||||||
|
const newFilename = newName + ext;
|
||||||
|
const newFullPath = path.join(dir, newFilename);
|
||||||
|
|
||||||
|
// Prevent overwriting existing files (check & delete)
|
||||||
|
if (fs.existsSync(newFullPath)) {
|
||||||
|
console.log(`[INFO] Le fichier de destination existe déjà, on supprime : ${newFullPath}`);
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(newFullPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[ERREUR] Impossible de supprimer le fichier existant ${newFullPath}`, e);
|
||||||
|
// On continue quand même pour tenter le rename (le rename écrasera peut-être ou échouera)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renameWithRetry(oldFullPath, newFullPath);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user