data-primals-engine 1.1.8-rc.1 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/core.js +10 -5
- package/src/modules/data.js +63 -83
- package/test/data.backup.integration.test.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.1.8
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "data-primals-engine is the package responsible from handling large amount of data in a practical and performant way. It can handle large amount of data in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"./*": "./src/*.js"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
+
"express": "^5.1.0",
|
|
46
47
|
"react": ">=18.0.0",
|
|
47
|
-
"react-query": ">=3.0.0"
|
|
48
|
-
"express": "^5.1.0"
|
|
48
|
+
"react-query": ">=3.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@langchain/core": "^0.3.66",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"express-rate-limit": "^8.0.1",
|
|
70
70
|
"express-session": "^1.18.2",
|
|
71
71
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
72
|
+
"isolated-vm": "^4.7.2",
|
|
72
73
|
"juice": "^11.0.1",
|
|
73
74
|
"mathjs": "^14.6.0",
|
|
74
75
|
"mongodb": "^6.18.0",
|
|
@@ -88,7 +89,6 @@
|
|
|
88
89
|
"tar": "^7.4.3",
|
|
89
90
|
"uniqid": "^5.4.0",
|
|
90
91
|
"vitest": "^3.2.4",
|
|
91
|
-
"isolated-vm": "^4.7.2",
|
|
92
92
|
"yaml": "^2.8.0"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
package/src/core.js
CHANGED
|
@@ -340,12 +340,13 @@ export const event_off = (name, callback) => {
|
|
|
340
340
|
};
|
|
341
341
|
|
|
342
342
|
|
|
343
|
-
export function slugify(str) {
|
|
343
|
+
export function slugify(str,replacer='-', replaceUnicode=false) {
|
|
344
344
|
str = str.replace(/^\s+|\s+$/g, ''); // trim leading/trailing white space
|
|
345
345
|
str = str.toLowerCase(); // convert string to lowercase
|
|
346
|
-
|
|
347
|
-
.replace(
|
|
348
|
-
|
|
346
|
+
if( replaceUnicode)
|
|
347
|
+
str = str.replace(/[^a-z0-9 -]/g, ''); // remove any non-alphanumeric characters
|
|
348
|
+
str = str.replace(/\s+/g, replacer) // replace spaces with hyphens
|
|
349
|
+
.replace(/-+/g, replacer); // remove consecutive hyphens
|
|
349
350
|
return str;
|
|
350
351
|
}
|
|
351
352
|
|
|
@@ -388,4 +389,8 @@ export function object_equals( x, y ) {
|
|
|
388
389
|
// allows x[ p ] to be set to undefined
|
|
389
390
|
|
|
390
391
|
return true;
|
|
391
|
-
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const isValidPath = (path) =>{
|
|
395
|
+
return /^(?:[a-z]:)?[\/\\]{0,2}(?:[.\/\\ ](?![.\/\\\n])|[^<>:"|?*.\/\\ \n])+$/gmi.test(path);
|
|
396
|
+
}
|
package/src/modules/data.js
CHANGED
|
@@ -3,7 +3,7 @@ import {BSON, ObjectId} from "mongodb";
|
|
|
3
3
|
import * as util from 'node:util';
|
|
4
4
|
import {promisify} from 'node:util';
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
|
-
import {exec} from 'node:child_process';
|
|
6
|
+
import {exec,execFile} from 'node:child_process';
|
|
7
7
|
import sanitizeHtml from 'sanitize-html';
|
|
8
8
|
import * as tar from "tar";
|
|
9
9
|
import process from "node:process";
|
|
@@ -105,6 +105,7 @@ const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
|
105
105
|
|
|
106
106
|
const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
|
|
107
107
|
const execAsync = promisify(exec);
|
|
108
|
+
const execFileAsync = promisify(execFile);
|
|
108
109
|
|
|
109
110
|
let importJobs = {};
|
|
110
111
|
const IMPORT_CHUNK_SIZE = 100; // Nombre d'enregistrements à traiter par lot
|
|
@@ -5217,6 +5218,7 @@ const handleFields = async (model, data, user, isRecursiveCall = false) => {
|
|
|
5217
5218
|
}
|
|
5218
5219
|
};
|
|
5219
5220
|
|
|
5221
|
+
let restoreRequests = {};
|
|
5220
5222
|
export const validateRestoreRequest = (username, token) => {
|
|
5221
5223
|
const request = restoreRequests[username];
|
|
5222
5224
|
if (!request) {
|
|
@@ -5247,20 +5249,21 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
5247
5249
|
throw new Error("No encryption key found for this user. Cannot restore.");
|
|
5248
5250
|
}
|
|
5249
5251
|
|
|
5252
|
+
const userId = getObjectHash({user: user.username});
|
|
5250
5253
|
// --- La logique pour trouver le fichier de backup (local ou S3) reste la même ---
|
|
5251
5254
|
// (le code existant pour trouver/télécharger le backupFilePath va ici)
|
|
5252
5255
|
// ...
|
|
5253
5256
|
let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
|
|
5254
5257
|
// Exemple simplifié :
|
|
5255
5258
|
const backupDir = getBackupDir();
|
|
5256
|
-
const backupFilenameRegex = new RegExp(`^backup_${
|
|
5259
|
+
const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
|
|
5257
5260
|
const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
|
|
5258
5261
|
if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
|
|
5259
5262
|
const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
|
|
5260
5263
|
backupFilePath = path.join(backupDir, latestBackupFile);
|
|
5261
5264
|
// --- Fin de la logique de recherche de fichier ---
|
|
5262
5265
|
|
|
5263
|
-
const tmpRestoreDir = path.join(backupDir, `tmp_restore_${
|
|
5266
|
+
const tmpRestoreDir = path.join(backupDir, `tmp_restore_${userId}_${Date.now()}`);
|
|
5264
5267
|
|
|
5265
5268
|
try {
|
|
5266
5269
|
await runCryptoWorkerTask('decrypt', { filePath: backupFilePath, password: encryptedKey });
|
|
@@ -5297,17 +5300,25 @@ export const loadFromDump = async (user, options = {}) => {
|
|
|
5297
5300
|
}
|
|
5298
5301
|
|
|
5299
5302
|
let command;
|
|
5303
|
+
const args = [
|
|
5304
|
+
'--uri', dbUrl,
|
|
5305
|
+
'--db', dbName
|
|
5306
|
+
];
|
|
5307
|
+
|
|
5300
5308
|
if (modelsOnly) {
|
|
5301
|
-
|
|
5302
|
-
command = `mongorestore --uri="${dbUrl}" --db=${dbName} --nsInclude="${dbName}.models" "${restoreSourceDir}"`;
|
|
5309
|
+
args.push('--nsInclude', `${dbName}.models`);
|
|
5303
5310
|
} else {
|
|
5304
|
-
//
|
|
5305
|
-
|
|
5311
|
+
// mongorestore accepte plusieurs fois l'option --nsInclude
|
|
5312
|
+
args.push('--nsInclude', `${dbName}.datas`);
|
|
5313
|
+
args.push('--nsInclude', `${dbName}.models`);
|
|
5306
5314
|
}
|
|
5315
|
+
// Le répertoire source est le dernier argument
|
|
5316
|
+
args.push(restoreSourceDir);
|
|
5307
5317
|
|
|
5308
|
-
logger.info(`[${action}] Executing restore command: ${command}`);
|
|
5309
|
-
await execAsync(command);
|
|
5310
5318
|
|
|
5319
|
+
logger.info(`[${action}] Executing restore command: ${command}`);
|
|
5320
|
+
await execFileAsync('mongorestore', args);
|
|
5321
|
+
|
|
5311
5322
|
// --- Tâches Post-Restauration ---
|
|
5312
5323
|
await scheduleAlerts();
|
|
5313
5324
|
await scheduleWorkflowTriggers();
|
|
@@ -5346,121 +5357,90 @@ const readKeyFromFile = (user) => {
|
|
|
5346
5357
|
return null;
|
|
5347
5358
|
};
|
|
5348
5359
|
|
|
5360
|
+
// C:/Dev/data-primals-engine/src/modules/data.js
|
|
5361
|
+
|
|
5349
5362
|
export const dumpUserData = async (user) => {
|
|
5350
|
-
|
|
5351
|
-
// Pour cet exemple, on simule la config S3. Remplace par la vraie récupération.
|
|
5352
|
-
const s3Config = user.configS3; // Supposons que l'objet 'user' passé contient déjà 'configS3'
|
|
5363
|
+
const s3Config = user.configS3;
|
|
5353
5364
|
const backupDir = getBackupDir();
|
|
5365
|
+
const userId = getObjectHash({ user: user.username });
|
|
5366
|
+
const backupFilename = `backup_${userId}`;
|
|
5367
|
+
const timestamp = Date.now();
|
|
5368
|
+
|
|
5369
|
+
// Déclarer les chemins ici pour qu'ils soient accessibles dans tout le scope de la fonction
|
|
5370
|
+
const localTempDumpDir = path.join(backupDir, `${backupFilename}_${timestamp}_temp`);
|
|
5371
|
+
const finalArchiveName = `${backupFilename}_${timestamp}.tar.gz`;
|
|
5372
|
+
const localArchivePath = path.join(backupDir, finalArchiveName);
|
|
5354
5373
|
|
|
5355
5374
|
let encryptedKey = readKeyFromFile(user);
|
|
5356
5375
|
if (!encryptedKey) {
|
|
5357
5376
|
encryptedKey = generateAndStoreKey(user);
|
|
5358
|
-
logger.info('Encryption key générée.');
|
|
5359
|
-
} else {
|
|
5360
|
-
logger.info('Encryption key chargée.');
|
|
5361
5377
|
}
|
|
5362
5378
|
|
|
5363
5379
|
try {
|
|
5364
|
-
// Déterminer la fréquence de la sauvegarde
|
|
5365
5380
|
const backupFrequency = await engine.userProvider.getBackupFrequency(user);
|
|
5366
|
-
|
|
5367
5381
|
logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
|
|
5368
5382
|
|
|
5369
|
-
// Définir le nom du fichier de sauvegarde et les chemins
|
|
5370
|
-
const backupFilename = `backup_${user.username}`; //nom corrigé
|
|
5371
|
-
const backupFileBasePath = path.join(backupDir, backupFilename); //chemin corrigé
|
|
5372
|
-
const backupFilePath = `${backupFileBasePath}_${Date.now()}`;
|
|
5373
|
-
const backupFilenameBase = `backup_${user.username}`;
|
|
5374
|
-
const timestamp = Date.now();
|
|
5375
|
-
const localTempDumpDir = path.join(backupDir, `${backupFilenameBase}_${timestamp}_temp`); // Répertoire temporaire local
|
|
5376
|
-
const finalArchiveName = `${backupFilenameBase}_${timestamp}.tar.gz`; // Nom de l'archive finale
|
|
5377
|
-
const localArchivePath = path.join(backupDir, finalArchiveName); // Chemin
|
|
5378
|
-
|
|
5379
|
-
logger.info(`Chemin du fichier de sauvegarde : ${backupFilePath}.`);
|
|
5380
|
-
|
|
5381
|
-
// Ajoute les filtres sur les collections et l'utilisateur
|
|
5382
5383
|
const collections = await MongoDatabase.listCollections().toArray();
|
|
5383
|
-
let query;
|
|
5384
|
-
let col;
|
|
5385
5384
|
for (const collection of collections) {
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
await execAsync(command);
|
|
5385
|
+
const collsToBackup = [await getUserCollectionName(user), 'models'];
|
|
5386
|
+
if (collsToBackup.includes(collection.name)) {
|
|
5387
|
+
const query = { _user: user.username };
|
|
5388
|
+
const args = [
|
|
5389
|
+
'--uri', dbUrl,
|
|
5390
|
+
'--db', dbName,
|
|
5391
|
+
'--out', localTempDumpDir,
|
|
5392
|
+
'--collection', collection.name,
|
|
5393
|
+
'--query', JSON.stringify(query)
|
|
5394
|
+
];
|
|
5395
|
+
logger.info(`Exécution de la commande : mongodump ${args.join(' ')}`);
|
|
5396
|
+
await execFileAsync('mongodump', args);
|
|
5399
5397
|
}
|
|
5400
|
-
|
|
5401
5398
|
}
|
|
5402
5399
|
|
|
5403
|
-
|
|
5404
|
-
// Zipper le contenu du répertoire de dump temporaire
|
|
5405
|
-
// Le répertoire zippé sera `localTempDumpDir/${dbName}` si mongodump a créé ce sous-dossier
|
|
5406
|
-
const dumpSourceDir = path.join(localTempDumpDir, dbName); // Le répertoire que mongodump a créé
|
|
5400
|
+
const dumpSourceDir = path.join(localTempDumpDir, dbName);
|
|
5407
5401
|
if (fs.existsSync(dumpSourceDir)) {
|
|
5408
5402
|
await tar.create({ gzip: true, file: localArchivePath, C: localTempDumpDir }, [dbName]);
|
|
5409
5403
|
logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
|
|
5410
5404
|
} else {
|
|
5411
|
-
|
|
5405
|
+
logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a été créée.`);
|
|
5406
|
+
// On s'arrête ici car il n'y a rien à traiter
|
|
5407
|
+
return Promise.resolve();
|
|
5412
5408
|
}
|
|
5413
5409
|
|
|
5414
|
-
|
|
5415
|
-
await encryptFile(localArchivePath, encryptedKey); // Ta fonction existante
|
|
5410
|
+
await encryptFile(localArchivePath, encryptedKey);
|
|
5416
5411
|
|
|
5417
5412
|
if (s3Config && s3Config.bucketName) {
|
|
5418
|
-
logger.info(`Téléversement de la sauvegarde vers S3 pour l'utilisateur ${user.username}. Bucket: ${s3Config.bucketName}`);
|
|
5419
5413
|
await uploadToS3(s3Config, localArchivePath, finalArchiveName);
|
|
5420
|
-
|
|
5421
|
-
// Optionnel: supprimer l'archive locale après l'upload S3 réussi
|
|
5422
|
-
fs.unlinkSync(localArchivePath);
|
|
5423
|
-
logger.info(`Archive locale ${localArchivePath} supprimée après l'upload S3.`);
|
|
5414
|
+
fs.unlinkSync(localArchivePath); // Supprime l'archive locale après l'upload
|
|
5424
5415
|
} else {
|
|
5425
|
-
logger.info(`Aucune configuration S3 trouvée
|
|
5426
|
-
// Si pas de S3, le fichier chiffré localement (si encryptFile a été appelé) reste.
|
|
5427
|
-
// Si tu ne chiffres pas localement et pas de S3, c'est l'archive .tar.gz qui reste.
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
|
-
// Supprimer le répertoire de dump temporaire
|
|
5431
|
-
if (fs.existsSync(localTempDumpDir)) {
|
|
5432
|
-
fs.rmSync(localTempDumpDir, { recursive: true, force: true });
|
|
5433
|
-
logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
|
|
5416
|
+
logger.info(`Aucune configuration S3 trouvée. La sauvegarde reste locale : ${localArchivePath}.`);
|
|
5434
5417
|
}
|
|
5435
5418
|
|
|
5436
5419
|
logger.info(`Sauvegarde réussie pour l'utilisateur ${user.username}.`);
|
|
5437
|
-
|
|
5438
|
-
// si les sauvegardes sont sur S3 (lister depuis S3, supprimer depuis S3).
|
|
5439
|
-
await manageBackupRotation(user, backupFrequency, s3Config); // Passer s3Config
|
|
5420
|
+
await manageBackupRotation(user, backupFrequency, s3Config);
|
|
5440
5421
|
|
|
5441
|
-
return Promise.resolve();
|
|
5442
5422
|
} catch (error) {
|
|
5443
5423
|
logger.error(`Erreur lors de la sauvegarde pour l'utilisateur ${user.username}:`, error);
|
|
5444
|
-
// Nettoyage
|
|
5445
|
-
const localTempDumpDir = path.join(backupDir, `backup_${user.username}_${Date.now()}_temp`); // Recalculer pour être sûr
|
|
5446
|
-
if (fs.existsSync(localTempDumpDir)) {
|
|
5447
|
-
fs.rmSync(localTempDumpDir, { recursive: true, force: true });
|
|
5448
|
-
}
|
|
5449
|
-
const localArchivePath = path.join(backupDir, `backup_${user.username}_${Date.now()}.tar.gz`); // Recalculer
|
|
5424
|
+
// Nettoyage de l'archive si elle a été créée avant l'erreur
|
|
5450
5425
|
if (fs.existsSync(localArchivePath)) {
|
|
5451
5426
|
fs.unlinkSync(localArchivePath);
|
|
5452
5427
|
}
|
|
5453
|
-
throw error; // Relancer l'erreur
|
|
5428
|
+
throw error; // Relancer l'erreur pour que l'appelant soit informé
|
|
5429
|
+
} finally {
|
|
5430
|
+
// --- NETTOYAGE GARANTI ---
|
|
5431
|
+
// Ce bloc s'exécute toujours, que la sauvegarde réussisse ou échoue.
|
|
5432
|
+
if (fs.existsSync(localTempDumpDir)) {
|
|
5433
|
+
fs.rmSync(localTempDumpDir, { recursive: true, force: true });
|
|
5434
|
+
logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
|
|
5435
|
+
}
|
|
5454
5436
|
}
|
|
5455
|
-
}
|
|
5456
|
-
|
|
5437
|
+
};
|
|
5457
5438
|
async function manageBackupRotation(user, backupFrequency, s3Config = null) { // Accepter s3Config
|
|
5458
|
-
const userId = user.username;
|
|
5459
|
-
|
|
5439
|
+
const userId = getObjectHash({user:user.username});
|
|
5460
5440
|
let filesToManage = [];
|
|
5461
5441
|
|
|
5462
5442
|
if (s3Config && s3Config.bucketName) {
|
|
5463
|
-
logger.info(`Gestion de la rotation des sauvegardes S3 pour ${
|
|
5443
|
+
logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userid}.`);
|
|
5464
5444
|
const s3Backups = await listS3Backups(s3Config);
|
|
5465
5445
|
// Filtrer pour ne garder que les backups de cet utilisateur et trier
|
|
5466
5446
|
filesToManage = s3Backups
|
|
@@ -86,7 +86,7 @@ afterAll(async () => {
|
|
|
86
86
|
fs.readdirSync(backupDir).forEach(file => {
|
|
87
87
|
fs.unlinkSync(path.join(backupDir, file));
|
|
88
88
|
});
|
|
89
|
-
|
|
89
|
+
fs.rmdirSync(backupDir); // Remove the directory itself
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -132,5 +132,7 @@ describe('Data Backup and Restore Integration', () => {
|
|
|
132
132
|
expect(docAfterRestore.testField).toBe('Initial Value');
|
|
133
133
|
expect(docAfterRestore.optionalField).toBe(123);
|
|
134
134
|
|
|
135
|
+
console.log("Tests passed for backup and restore");
|
|
136
|
+
|
|
135
137
|
}, 15000); // Timeout augmenté pour les opérations de fichiers
|
|
136
138
|
});
|