mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-26 20:42:08 +00:00
141 lines
4.7 KiB
JavaScript
141 lines
4.7 KiB
JavaScript
/**
|
|
* Safe file read/write operations with Redis distributed locking and atomic writes.
|
|
* Extracted from server.js — provides safeWriteFile, safeWriteJsonFile, safeReadJsonFile.
|
|
*/
|
|
|
|
const fsp = require('fs').promises;
|
|
const path = require('path');
|
|
const writeFileAtomic = require('write-file-atomic');
|
|
const { acquireRedisLock } = require('./redisLock');
|
|
|
|
// ===== FONCTIONS SECURISEES POUR LES ECRITURES DE FICHIERS =====
|
|
|
|
/**
|
|
* Écriture sécurisée avec verrou de fichier et écriture atomique
|
|
* @param {string} filePath - Chemin du fichier à écrire
|
|
* @param {string|Buffer} data - Données à écrire
|
|
* @param {object} options - Options pour l'écriture
|
|
* @returns {Promise<boolean>} - Succès de l'opération
|
|
*/
|
|
async function safeWriteFile(filePath, data, options = {}) {
|
|
const defaultOptions = {
|
|
encoding: 'utf8',
|
|
mode: 0o644,
|
|
lockTtl: 10, // TTL du verrou Redis en secondes
|
|
retries: 3,
|
|
retryDelay: 100
|
|
};
|
|
|
|
const finalOptions = { ...defaultOptions, ...options };
|
|
let lock = null;
|
|
|
|
try {
|
|
// Créer le répertoire parent si nécessaire
|
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
|
|
// Tentative d'acquisition du verrou Redis (avec fallback si échec)
|
|
// Clé de lock basée sur le chemin absolu normalisé
|
|
const lockResource = path.resolve(filePath).replace(/\\/g, '/');
|
|
lock = await acquireRedisLock(lockResource, {
|
|
ttl: finalOptions.lockTtl,
|
|
retries: finalOptions.retries * 10, // plus de tentatives car Redis est rapide
|
|
retryDelay: finalOptions.retryDelay
|
|
});
|
|
|
|
if (!lock) {
|
|
// Redis indisponible ou timeout — on continue l'écriture sans verrou
|
|
console.warn(`[SafeWriteFile] AVERTISSEMENT: Impossible de verrouiller ${path.basename(filePath)} via Redis, écriture forcée sans verrou.`);
|
|
}
|
|
|
|
// Écrire le fichier de manière atomique (même si le lock a échoué)
|
|
// flush: false désactive fsync pour améliorer les performances sur software RAID
|
|
await writeFileAtomic(filePath, data, {
|
|
encoding: finalOptions.encoding,
|
|
mode: finalOptions.mode,
|
|
fsync: false // Désactiver fsync pour perf sur software RAID OVH
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[SafeWriteFile] Échec pour ${filePath}:`, error.message, error.code || '');
|
|
return false;
|
|
} finally {
|
|
// Libérer le verrou Redis si acquis
|
|
if (lock) {
|
|
try {
|
|
await lock.release();
|
|
} catch (releaseError) {
|
|
console.error(`[SafeWriteFile] Erreur release Redis lock pour ${filePath}:`, releaseError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Écriture sécurisée de données JSON avec verrou de fichier et écriture atomique
|
|
* @param {string} filePath - Chemin du fichier à écrire
|
|
* @param {object} data - Objet JSON à écrire
|
|
* @param {object} options - Options pour l'écriture
|
|
* @returns {Promise<boolean>} - Succès de l'opération
|
|
*/
|
|
async function safeWriteJsonFile(filePath, data, options = {}) {
|
|
const jsonOptions = {
|
|
...options,
|
|
encoding: 'utf8'
|
|
};
|
|
|
|
try {
|
|
const jsonString = JSON.stringify(data);
|
|
return await safeWriteFile(filePath, jsonString, jsonOptions);
|
|
} catch (error) {
|
|
console.error(`Erreur lors de la sérialisation JSON pour ${filePath}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lecture sécurisée avec verrou de fichier
|
|
* @param {string} filePath - Chemin du fichier à lire
|
|
* @param {object} options - Options pour la lecture
|
|
* @returns {Promise<object|null>} - Données lues ou null en cas d'erreur
|
|
*/
|
|
async function safeReadJsonFile(filePath, options = {}) {
|
|
const defaultOptions = {
|
|
encoding: 'utf8',
|
|
retries: 2,
|
|
retryDelay: 50
|
|
};
|
|
|
|
const finalOptions = { ...defaultOptions, ...options };
|
|
|
|
// Petite boucle de retry en cas d'accès concurrentiel très rare
|
|
for (let attempt = 0; attempt <= finalOptions.retries; attempt++) {
|
|
try {
|
|
const fileContent = await fsp.readFile(filePath, finalOptions.encoding);
|
|
try {
|
|
return JSON.parse(fileContent);
|
|
} catch (parseError) {
|
|
// Si JSON temporairement invalide, réessayer brièvement
|
|
if (attempt < finalOptions.retries) {
|
|
await new Promise(r => setTimeout(r, finalOptions.retryDelay));
|
|
continue;
|
|
}
|
|
console.error(`Erreur de parse JSON pour ${filePath}:`, parseError);
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return null; // Fichier absent
|
|
}
|
|
if (attempt < finalOptions.retries) {
|
|
await new Promise(r => setTimeout(r, finalOptions.retryDelay));
|
|
continue;
|
|
}
|
|
console.error(`Erreur lors de la lecture de ${filePath}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
module.exports = { safeWriteFile, safeWriteJsonFile, safeReadJsonFile };
|