data-primals-engine 1.2.1 → 1.2.2

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.
@@ -3,7 +3,6 @@ import AWS from "aws-sdk";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import {decryptValue, encryptValue} from "../data.js";
6
- import {MongoClient} from "../engine.js";
7
6
  import {loadFromDump, validateRestoreRequest} from "./data.js";
8
7
  import {Logger} from "../gameObject.js";
9
8
  import {middlewareAuthenticator, userInitiator} from "./user.js";
@@ -12,6 +11,7 @@ import crypto from "node:crypto";
12
11
  import i18n from "../../src/i18n.js";
13
12
  import {sendEmail} from "../email.js";
14
13
  import {throttleMiddleware} from "../middlewares/throttle.js";
14
+ import {getCollectionForUser} from "./mongodb.js";
15
15
 
16
16
  const restoreRequests = {};
17
17
 
@@ -54,14 +54,77 @@ const getDefaultS3Config = () => {
54
54
  bucketName: process.env.AWS_BUCKET || awsDefaultConfig.bucketName
55
55
  };
56
56
  }
57
- const getS3Client = (s3Config) => {
58
57
 
59
- const decryptedSecretAccessKey = decryptValue(s3Config.secretAccessKey);
58
+ /**
59
+ * Récupère la configuration S3 depuis les variables d'environnement de l'utilisateur en base de données.
60
+ * @param {object} user - L'objet utilisateur.
61
+ * @returns {Promise<object>} - Un objet contenant la configuration S3 trouvée pour l'utilisateur.
62
+ */
63
+ async function _getUserS3ConfigFromDb(user) {
64
+ if (!user || !user.username) {
65
+ return {}; // Pas d'utilisateur, pas de configuration spécifique.
66
+ }
67
+
68
+ try {
69
+ const collection = await getCollectionForUser(user);
70
+ const userEnvVars = await collection.find({
71
+ _model: 'env',
72
+ _user: user.username,
73
+ // On ne cherche que les clés pertinentes pour optimiser la requête
74
+ key: { $in: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'AWS_BUCKET_NAME'] }
75
+ }).toArray();
76
+
77
+ // Transforme le tableau de documents [{key, value}, ...] en un objet de configuration.
78
+ return userEnvVars.reduce((config, envVar) => {
79
+ if (envVar.key === 'AWS_ACCESS_KEY_ID') config.accessKeyId = envVar.value;
80
+ if (envVar.key === 'AWS_SECRET_ACCESS_KEY') config.secretAccessKey = envVar.value; // La clé est déjà chiffrée en BDD
81
+ if (envVar.key === 'AWS_REGION') config.region = envVar.value;
82
+ if (envVar.key === 'AWS_BUCKET_NAME') config.bucketName = envVar.value;
83
+ return config;
84
+ }, {});
60
85
 
61
- const defaultConfig = getDefaultS3Config();
86
+ } catch (error) {
87
+ logger.error(`Failed to fetch user S3 config from DB for ${user.username}:`, error);
88
+ return {}; // Retourne un objet vide en cas d'erreur pour utiliser le fallback.
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Récupère la configuration S3 effective en priorisant celle de l'utilisateur
94
+ * puis en se rabattant sur la configuration globale de l'environnement.
95
+ * C'est la fonction à utiliser pour toute opération S3.
96
+ * @param {object} user - L'objet utilisateur.
97
+ * @returns {Promise<object>} - L'objet de configuration S3 final.
98
+ */
99
+ export async function getUserS3Config(user) {
100
+ // 1. Récupérer la configuration globale par défaut
101
+ const defaultConfig = {
102
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
103
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
104
+ region: process.env.AWS_REGION || awsDefaultConfig.region,
105
+ bucketName: process.env.AWS_BUCKET_NAME || awsDefaultConfig.bucketName
106
+ };
107
+
108
+ // 2. Récupérer la configuration spécifique de l'utilisateur
109
+ const userConfig = await _getUserS3ConfigFromDb(user);
110
+
111
+ if( userConfig.bucketName && userConfig.accessKeyId && userConfig.secretAccessKey) {
112
+ // 3. Fusionner les configurations, en donnant la priorité à celle de l'utilisateur
113
+ return {
114
+ accessKeyId: userConfig.accessKeyId || defaultConfig.accessKeyId,
115
+ secretAccessKey: userConfig.secretAccessKey || defaultConfig.secretAccessKey,
116
+ region: userConfig.region || defaultConfig.region,
117
+ bucketName: userConfig.bucketName || defaultConfig.bucketName
118
+ };
119
+ }
120
+ console.log({defaultConfig})
121
+ return defaultConfig;
122
+ }
123
+ const getS3Client = (s3Config) => {
124
+
125
+ const decryptedSecretAccessKey = s3Config.secretAccessKey ? decryptValue(s3Config.secretAccessKey): undefined;
62
126
 
63
127
  return new AWS.S3({
64
- ...defaultConfig,
65
128
  accessKeyId: s3Config.accessKeyId || defaultConfig.accessKeyId,
66
129
  secretAccessKey: decryptedSecretAccessKey ? decryptedSecretAccessKey : defaultConfig.secretAccessKey,
67
130
  region: s3Config.region || defaultConfig.region,
@@ -74,6 +137,10 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
74
137
  const fileContent = fs.readFileSync(filePath);
75
138
  const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
76
139
 
140
+ if( !s3.accessKeyId || !s3.secretAccessKey || !s3.bucketName) {
141
+ throw new Error('Missing S3 configuration');
142
+ }
143
+
77
144
  const params = {
78
145
  Bucket: s3Config.bucketName,
79
146
  Key: bucketPath,
@@ -90,6 +157,7 @@ export const uploadToS3 = async (s3Config, filePath, remoteFilename) => {
90
157
  }
91
158
  };
92
159
 
160
+
93
161
  export const listS3Backups = async (s3Config) => {
94
162
  const s3 = getS3Client(s3Config);
95
163
  const bucketPathPrefix = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/` : '';
@@ -133,6 +201,48 @@ export const downloadFromS3 = async (s3Config, s3FileKey, downloadPath) => {
133
201
  }
134
202
  };
135
203
 
204
+ export const deleteFromS3 = async (s3Config, remoteFilename) => {
205
+ const s3 = getS3Client(s3Config);
206
+ const bucketPath = s3Config.pathPrefix ? `${s3Config.pathPrefix.replace(/\/$/, "")}/${remoteFilename}` : remoteFilename;
207
+
208
+ const params = {
209
+ Bucket: s3Config.bucketName,
210
+ Key: bucketPath
211
+ };
212
+
213
+ try {
214
+ await s3.deleteObject(params).promise();
215
+ console.log(`Fichier supprimé avec succès de S3 : ${bucketPath}`);
216
+ } catch (err) {
217
+ console.error("Erreur lors de la suppression sur S3 :", err);
218
+ throw err;
219
+ }
220
+ };
221
+
222
+ /**
223
+ * Crée un flux de lecture pour un objet depuis un bucket S3.
224
+ * @param {object} s3Config - La configuration S3 (bucketName, accessKeyId, secretAccessKey, region).
225
+ * @param {string} s3Key - La clé (nom du fichier) de l'objet sur S3.
226
+ * @returns {import('stream').Readable} - Le flux de lecture de l'objet S3.
227
+ */
228
+ export const getS3Stream = (s3Config, s3Key) => {
229
+ if (!s3Config || !s3Config.bucketName || !s3Key) {
230
+ throw new Error("La configuration S3 et la clé de l'objet sont requises pour créer un flux.");
231
+ }
232
+
233
+ // On utilise la fonction existante qui gère le déchiffrement !
234
+ const s3 = getS3Client(s3Config);
235
+
236
+ const params = {
237
+ Bucket: s3Config.bucketName,
238
+ Key: s3Key
239
+ };
240
+
241
+ // getObject().createReadStream() retourne directement un flux lisible.
242
+ // Les erreurs (ex: objet non trouvé) seront émises sur l'événement 'error' de ce flux.
243
+ return s3.getObject(params).createReadStream();
244
+ };
245
+
136
246
  const throttle = throttleMiddleware(maxBytesPerSecondThrottleData);
137
247
 
138
248
  let engine, logger;
@@ -3,35 +3,30 @@ 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,execFile} 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";
10
10
  import {randomColor} from "randomcolor";
11
11
  import cronstrue from 'cronstrue/i18n.js';
12
- import { setTimeoutMiddleware } from '../middlewares/timeout.js';
13
- import { mkdir } from 'node:fs/promises';
12
+ import {setTimeoutMiddleware} from '../middlewares/timeout.js';
13
+ import {mkdir} from 'node:fs/promises';
14
+ import {anonymizeText, getDefaultForType, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../data.js";
14
15
  import {
15
- anonymizeText,
16
- encryptValue,
17
- getDefaultForType,
18
- getFieldValueHash,
19
- getUserId, isDemoUser,
20
- isLocalUser
21
- } from "../data.js";
22
- import {
23
- allowedFields, availableLangs,
16
+ allowedFields,
24
17
  dbName,
25
- install, maxAlertsPerUser,
18
+ install,
19
+ maxAlertsPerUser,
26
20
  maxBytesPerSecondThrottleData,
27
21
  maxExportCount,
28
22
  maxFileSize,
29
- maxFilterDepth, maxMagnetsDataPerModel, maxMagnetsModels,
23
+ maxFilterDepth,
24
+ maxMagnetsDataPerModel,
25
+ maxMagnetsModels,
30
26
  maxModelNameLength,
31
27
  maxModelsPerUser,
32
28
  maxPasswordLength,
33
29
  maxPostData,
34
- maxPrivateFileSize,
35
30
  maxRelationsPerData,
36
31
  maxRequestData,
37
32
  maxRichTextLength,
@@ -39,7 +34,8 @@ import {
39
34
  maxTotalDataPerUser,
40
35
  megabytes,
41
36
  optionsSanitizer,
42
- searchRequestTimeout, storageSafetyMargin
37
+ searchRequestTimeout,
38
+ storageSafetyMargin
43
39
  } from "../constants.js";
44
40
  import {
45
41
  getCollection,
@@ -51,16 +47,8 @@ import {
51
47
  } from "./mongodb.js";
52
48
  import {dbUrl, MongoClient, MongoDatabase} from "../engine.js";
53
49
  import path from "node:path";
54
- import {
55
- event_trigger,
56
- getFileExtension,
57
- getObjectHash,
58
- getRandom,
59
- isGUID,
60
- isPlainObject,
61
- randomDate,
62
- uuidv4
63
- } from "../core.js";
50
+ import {getFileExtension, getObjectHash, getRandom, isGUID, isPlainObject, randomDate, uuidv4} from "../core.js";
51
+ import {Event} from "../events.js";
64
52
  import fs from "node:fs";
65
53
  import schedule from "node-schedule";
66
54
  import {middleware} from "../middlewares/middleware-mongodb.js";
@@ -75,12 +63,14 @@ import NodeCache from "node-cache";
75
63
  import AWS from 'aws-sdk';
76
64
  import {openaiJobModel} from "../openai.jobs.js";
77
65
  import checkDiskSpace from "check-disk-space";
78
- import { fileURLToPath } from 'url';
79
- import { Worker } from 'worker_threads';
66
+ import {fileURLToPath} from 'url';
67
+ import {Worker} from 'worker_threads';
80
68
  import {addFile, encryptFile, removeFile} from "./file.js";
81
- import {listS3Backups, uploadToS3} from "./bucket.js";
69
+ import {downloadFromS3, getS3Stream, getUserS3Config, listS3Backups, uploadToS3} from "./bucket.js";
82
70
  import {
83
- calculateTotalUserStorageUsage, generateLimiter, hasPermission,
71
+ calculateTotalUserStorageUsage,
72
+ generateLimiter,
73
+ hasPermission,
84
74
  middlewareAuthenticator,
85
75
  userInitiator
86
76
  } from "./user.js";
@@ -132,6 +122,7 @@ export function sendSseToUser(username, data) {
132
122
  const res = sseConnections.get(username);
133
123
  if (res) {
134
124
  res.write(`data: ${JSON.stringify(data)}\n\n`);
125
+ Event.Trigger("sendSseToUser", "system", "calls");
135
126
  return true;
136
127
  }
137
128
  logger.warn(`[SSE] Attempted to send event to disconnected user: ${username}`);
@@ -155,12 +146,13 @@ export const jobDumpUserData = async () => {
155
146
  return;
156
147
  try {
157
148
  dumpUserData(user).catch(e => {
158
-
149
+ Event.Trigger("OnUserDataDumped", "event", "system", engine);
159
150
  })
160
151
  } catch (ignored) {
161
152
 
162
153
  }
163
154
  });
155
+
164
156
  }catch (e) {
165
157
  console.error(e);
166
158
  }
@@ -1210,7 +1202,11 @@ export const editModel = async (user, id, data) => {
1210
1202
  logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
1211
1203
  });
1212
1204
 
1213
- return ({ success: true, data: await modelsCollection.findOne({_id : oid}) });
1205
+ const newModel = await modelsCollection.findOne({_id : oid});
1206
+ const res = ({ success: true, data: newModel });
1207
+ const plugin = Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
1208
+ Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
1209
+ return plugin || res
1214
1210
  } catch (e) {
1215
1211
  logger.error(e);
1216
1212
  return ({ success: false, error: e.message, statusCode: 500 });
@@ -2111,7 +2107,7 @@ export async function onInit(defaultEngine) {
2111
2107
  await cancelAlerts(req.me);
2112
2108
 
2113
2109
  await getPromise();
2114
- event_trigger('jobAddUserData', req.me.username);
2110
+ Event.Trigger('jobAddUserData', req.me.username);
2115
2111
  }else{
2116
2112
  await getPromise();
2117
2113
  }
@@ -2850,12 +2846,24 @@ export async function onInit(defaultEngine) {
2850
2846
  const resourceInfo = await getResource(guid, user); // Passez l'objet utilisateur
2851
2847
  res.setHeader('Content-Type', resourceInfo.mimeType);
2852
2848
 
2853
- const fileStream = fs.createReadStream(resourceInfo.filepath);
2854
- fileStream.on('error', (streamError) => {
2855
- console.error(`Stream error for resource ${guid}:`, streamError); // ou logger.error
2856
- res.status(404).json({ error: 'Resource file not found on server.' });
2857
- });
2858
- fileStream.pipe(res);
2849
+ if (resourceInfo.storage === 's3') {
2850
+ const s3Config = await getUserS3Config(user);
2851
+ const s3Stream = getS3Stream(s3Config, resourceInfo.s3Key);
2852
+
2853
+ s3Stream.on('error', (s3Error) => {
2854
+ logger.error(`S3 stream error for resource ${guid}:`, s3Error);
2855
+ res.status(404).json({ error: 'Resource file not found in storage.' });
2856
+ });
2857
+
2858
+ s3Stream.pipe(res);
2859
+ } else { // Stockage 'local'
2860
+ const fileStream = fs.createReadStream(resourceInfo.filepath);
2861
+ fileStream.on('error', (streamError) => {
2862
+ console.error(`Stream error for resource ${guid}:`, streamError); // ou logger.error
2863
+ res.status(404).json({error: 'Resource file not found on server.'});
2864
+ });
2865
+ fileStream.pipe(res);
2866
+ }
2859
2867
 
2860
2868
  } catch (error) {
2861
2869
  console.error(`Error serving resource ${req.params.guid}:`, error); // ou logger.error
@@ -2892,7 +2900,7 @@ export const getModel = async (modelName, user) => {
2892
2900
  return model;
2893
2901
  }
2894
2902
  export const getModels = async () => {
2895
- return await getCollection('models').find({'$or': [{_user: { $exists: false}}]}).toArray();
2903
+ return await getCollection('models')?.find({'$or': [{_user: { $exists: false}}]}).toArray() || [];
2896
2904
  }
2897
2905
 
2898
2906
 
@@ -3119,9 +3127,10 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
3119
3127
  // Attendre que toutes les opérations post-insertion (workflows, planification) soient tentées
3120
3128
  await Promise.allSettled(postInsertionPromises);
3121
3129
  }
3122
-
3123
- // --- Retourner succès car l'insertion principale a réussi ---
3124
- return { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
3130
+ const res = { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
3131
+ const plugin = Event.Trigger("OnDataAdded", "event", "system", engine, insertedIds);
3132
+ Event.Trigger("OnDataAdded", "event", "user", plugin?.insertedIds || insertedIds);
3133
+ return plugin || res;
3125
3134
 
3126
3135
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
3127
3136
  logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
@@ -3633,27 +3642,48 @@ const checkHash = async (me, model, hash, excludeId = null) => {
3633
3642
  return count > 0;
3634
3643
  };
3635
3644
 
3645
+
3636
3646
  export const getResource = async (guid, user) => {
3637
3647
  if (!guid) throw new Error("Le GUID du fichier est requis.");
3638
3648
  if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
3639
3649
 
3640
3650
  const collection = getCollection("files");
3641
-
3642
- // Trouver le fichier et vérifier l'autorisation (propriétaire, admin ou utilisateur principal)
3643
3651
  const file = await collection.findOne({ guid });
3644
- if (!file) throw new Error("Fichier non trouvé."+guid);
3645
3652
 
3653
+ if (!file) {
3654
+ throw new Error("Fichier non trouvé.");
3655
+ }
3656
+
3657
+ // La vérification des permissions reste la même...
3646
3658
  if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_READ_FILE", `API_READ_FILE_privateFile_${guid}`], user)) {
3647
- if (file._user !== (user._user || user.username)) { // Vérifier si l'utilisateur est le propriétaire
3659
+ if (file.user !== (user._user || user.username)) {
3648
3660
  throw new Error("Vous n'êtes pas autorisé à accéder à ce fichier.");
3649
3661
  }
3650
3662
  }
3651
- // Construire le chemin vers le fichier
3652
- const filepath = path.join(process.cwd(), 'uploads', 'private', guid)+'.'+getFileExtension(file.filename);
3653
- if (!fs.existsSync(filepath)) throw new Error("Fichier non trouvé sur le serveur.");
3654
3663
 
3655
-
3656
- return { success: true, filepath, filename: file.filename, mimeType: file.mimeType }; // Retourner des informations utiles
3664
+ // On retourne des informations différentes selon le type de stockage
3665
+ if (file.storage === 's3') {
3666
+ return {
3667
+ success: true,
3668
+ storage: 's3',
3669
+ s3Key: file.filename, // 'filename' contient la clé S3
3670
+ mimeType: file.mimeType
3671
+ // Idlement, on aurait aussi le nom de fichier original ici
3672
+ };
3673
+ } else { // Par défaut, on considère le stockage local
3674
+ // On utilise le chemin stocké en base de données
3675
+ const filepath = file.path;
3676
+ if (!filepath || !fs.existsSync(filepath)) {
3677
+ throw new Error("Fichier non trouvé sur le serveur.");
3678
+ }
3679
+ return {
3680
+ success: true,
3681
+ storage: 'local',
3682
+ filepath: filepath,
3683
+ filename: file.filename,
3684
+ mimeType: file.mimeType
3685
+ };
3686
+ }
3657
3687
  };
3658
3688
 
3659
3689
  export const patchData = async (modelName, filter, data, files, user, triggerWorkflow = true, waitForWorkflow = false) => {
@@ -4088,7 +4118,10 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
4088
4118
  logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
4089
4119
  }
4090
4120
 
4091
- return ({ success: true, deletedCount });
4121
+ const res = { success: true, deletedCount }
4122
+ const plugin = Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
4123
+ Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
4124
+ return plugin || res;
4092
4125
 
4093
4126
  } catch (error) {
4094
4127
  logger.error(`[deleteData] Error during deletion process for user ${user?.username}:`, error);
@@ -4598,7 +4631,10 @@ export const searchData = async (query, user) => {
4598
4631
  let data = await prom.toArray();
4599
4632
  data = await handleFields(modelElement, data, user);
4600
4633
 
4601
- return {data, count: count[0]?.count || 0};
4634
+ const res = {data, count: count[0]?.count || 0};
4635
+ const plugin = Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
4636
+ Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
4637
+ return plugin || res;
4602
4638
  }
4603
4639
 
4604
4640
  export const importData = async(options, files, user) => {
@@ -4909,8 +4945,7 @@ export const importData = async(options, files, user) => {
4909
4945
  importJobs[importJobId].errors.push(error.message || "An unhandled error occurred in background process.");
4910
4946
  }
4911
4947
  });
4912
-
4913
- return ({ success: true, message: "Import initiated. Check progress via SSE.", job: importJob });
4948
+ return ({success: true, message: "Import initiated. Check progress via SSE.", job: importJob});
4914
4949
  }
4915
4950
 
4916
4951
  export const exportData= async (options, user) =>{
@@ -5025,7 +5060,10 @@ export const exportData= async (options, user) =>{
5025
5060
  exportResults._exportErrors = errors;
5026
5061
  }
5027
5062
 
5028
- return { success: true, data: exportResults, models: modelsToExport };
5063
+ const res = { success: true, data: exportResults, models: modelsToExport };
5064
+ const plugin = Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
5065
+ Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
5066
+ return plugin || res;
5029
5067
  }
5030
5068
 
5031
5069
  function handleCalculationExpression(calcExpression, fi, modelElement, calculationName) {
@@ -5270,43 +5308,68 @@ export const loadFromDump = async (user, options = {}) => {
5270
5308
  const action = modelsOnly ? 'restore-models' : 'full-restore';
5271
5309
  logger.info(`[${action}] Starting for user: ${user.username}`);
5272
5310
 
5273
- let encryptedKey = readKeyFromFile(user);
5311
+ const encryptedKey = readKeyFromFile(user);
5274
5312
  if (!encryptedKey) {
5275
5313
  throw new Error("No encryption key found for this user. Cannot restore.");
5276
5314
  }
5277
5315
 
5278
- const userId = getObjectHash({user: user.username});
5279
- // --- La logique pour trouver le fichier de backup (local ou S3) reste la même ---
5280
- // (le code existant pour trouver/télécharger le backupFilePath va ici)
5281
- // ...
5282
- let backupFilePath; // Assurez-vous que cette variable est bien définie avec le chemin du fichier .tar.gz
5283
- // Exemple simplifié :
5316
+ const userId = getObjectHash({ user: user.username });
5284
5317
  const backupDir = getBackupDir();
5285
- const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
5286
- const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
5287
- if (backupFiles.length === 0) throw new Error(`Aucun fichier de sauvegarde local trouvé pour l'utilisateur ${user.username}.`);
5288
- const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
5289
- backupFilePath = path.join(backupDir, latestBackupFile);
5290
- // --- Fin de la logique de recherche de fichier ---
5318
+ let backupFilePath = ''; // Will hold the path to the archive to be restored
5319
+ let isTempFile = false; // Flag to know if we need to delete the file later
5291
5320
 
5292
5321
  const tmpRestoreDir = path.join(backupDir, `tmp_restore_${userId}_${Date.now()}`);
5293
5322
 
5294
5323
  try {
5324
+ const s3Config = await getUserS3Config(user);
5325
+ // --- NEW LOGIC: Check for S3 config first ---
5326
+ if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
5327
+ logger.info(`[${action}] S3 config found for user. Searching for backups in bucket: ${s3Config.bucketName}`);
5328
+
5329
+ const s3Backups = await listS3Backups(s3Config);
5330
+ const userBackups = s3Backups
5331
+ .filter(f => f.filename.startsWith(`backup_${userId}_`))
5332
+ .sort((a, b) => b.timestamp - a.timestamp);
5333
+
5334
+ if (userBackups.length === 0) {
5335
+ throw new Error(`No S3 backups found for user ${user.username} in bucket ${s3Config.bucketName}.`);
5336
+ }
5337
+
5338
+ const latestBackup = userBackups[0];
5339
+ logger.info(`[${action}] Found latest S3 backup: ${latestBackup.key}. Downloading...`);
5340
+
5341
+ // Download the file to a temporary location
5342
+ backupFilePath = path.join(backupDir, latestBackup.filename);
5343
+ isTempFile = true;
5344
+ await downloadFromS3(s3Config, latestBackup.key, backupFilePath);
5345
+ logger.info(`[${action}] S3 backup downloaded to ${backupFilePath}.`);
5346
+
5347
+ } else {
5348
+ // --- FALLBACK LOGIC: Look for local backups ---
5349
+ logger.info(`[${action}] No S3 config. Searching for local backups.`);
5350
+ const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
5351
+ const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
5352
+ if (backupFiles.length === 0) {
5353
+ throw new Error(`No local backup files found for user ${user.username}.`);
5354
+ }
5355
+ const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
5356
+ backupFilePath = path.join(backupDir, latestBackupFile);
5357
+ }
5358
+
5359
+ // --- The rest of the logic remains the same, operating on backupFilePath ---
5360
+
5295
5361
  await runCryptoWorkerTask('decrypt', { filePath: backupFilePath, password: encryptedKey });
5362
+
5296
5363
  if (!fs.existsSync(tmpRestoreDir)) {
5297
5364
  fs.mkdirSync(tmpRestoreDir, { recursive: true });
5298
5365
  }
5299
5366
  await tar.extract({ file: backupFilePath, gzip: true, C: tmpRestoreDir, sync: true });
5300
5367
 
5301
- // --- NETTOYAGE SÉCURISÉ AVANT RESTAURATION ---
5368
+ // ... (Cleaning logic: deleteMany, removeFile, cancelAlerts) ...
5302
5369
  const datasCollection = getCollection("datas");
5303
-
5304
5370
  if (modelsOnly) {
5305
- // Supprime uniquement les modèles de l'utilisateur
5306
5371
  await modelsCollection.deleteMany({ _user: user.username });
5307
- logger.info(`[${action}] Deleted existing models for user ${user.username}.`);
5308
5372
  } else {
5309
- // Restauration complète : supprime les données, modèles, fichiers et alertes de l'utilisateur
5310
5373
  await datasCollection.deleteMany({ _user: user.username });
5311
5374
  await modelsCollection.deleteMany({ _user: user.username });
5312
5375
 
@@ -5344,23 +5407,31 @@ export const loadFromDump = async (user, options = {}) => {
5344
5407
 
5345
5408
  logger.info(`[${action}] Executing restore command: ${command}`);
5346
5409
  await execFileAsync('mongorestore', args);
5347
-
5348
- // --- Tâches Post-Restauration ---
5410
+
5411
+ // ... (Post-restore tasks) ...
5349
5412
  await scheduleAlerts();
5350
5413
  await scheduleWorkflowTriggers();
5351
- modelsCache.flushAll(); // Vider le cache des modèles
5414
+ modelsCache.flushAll();
5352
5415
 
5353
5416
  logger.info(`[${action}] Restore successful for user ${user.username}.`);
5417
+ Event.Trigger("OnDataRestored", "event", "system");
5354
5418
 
5355
5419
  } finally {
5356
- // --- Nettoyage final ---
5420
+ // --- GUARANTEED CLEANUP ---
5357
5421
  if (fs.existsSync(tmpRestoreDir)) {
5358
5422
  await fs.promises.rm(tmpRestoreDir, { recursive: true, force: true });
5359
5423
  }
5360
- // Il est préférable de rechiffrer le fichier de backup après l'avoir utilisé
5361
- if (fs.existsSync(backupFilePath)) {
5424
+
5425
+ // Re-encrypt the original file if it's not a temporary one
5426
+ if (fs.existsSync(backupFilePath) && !isTempFile) {
5362
5427
  await runCryptoWorkerTask('encrypt', { filePath: backupFilePath, password: encryptedKey });
5363
5428
  }
5429
+
5430
+ // If we downloaded a temp file from S3, delete it
5431
+ if (fs.existsSync(backupFilePath) && isTempFile) {
5432
+ fs.unlinkSync(backupFilePath);
5433
+ logger.info(`[${action}] Deleted temporary downloaded backup file: ${backupFilePath}`);
5434
+ }
5364
5435
  }
5365
5436
  };
5366
5437
 
@@ -5384,7 +5455,7 @@ const readKeyFromFile = (user) => {
5384
5455
  };
5385
5456
 
5386
5457
  export const dumpUserData = async (user) => {
5387
- const s3Config = user.configS3;
5458
+ const s3Config = await getUserS3Config(user);
5388
5459
  const backupDir = getBackupDir();
5389
5460
  const userId = getObjectHash({ user: user.username });
5390
5461
  const backupFilename = `backup_${userId}`;
@@ -5420,28 +5491,28 @@ export const dumpUserData = async (user) => {
5420
5491
  await execFileAsync('mongodump', args);
5421
5492
  }
5422
5493
  }
5423
-
5424
5494
  const dumpSourceDir = path.join(localTempDumpDir, dbName);
5425
5495
  if (fs.existsSync(dumpSourceDir)) {
5426
5496
  await tar.create({ gzip: true, file: localArchivePath, C: localTempDumpDir }, [dbName]);
5427
5497
  logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
5428
5498
  } else {
5429
- logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a été créée.`);
5430
- // On s'arrête ici car il n'y a rien à traiter
5499
+ logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a créée.`);
5431
5500
  return Promise.resolve();
5432
5501
  }
5433
5502
 
5434
- await encryptFile(localArchivePath, encryptedKey);
5503
+ await runCryptoWorkerTask('encrypt', { filePath: localArchivePath, password: encryptedKey });
5435
5504
 
5436
- if (s3Config && s3Config.bucketName) {
5505
+ try {
5506
+ // Attempt the S3 upload
5437
5507
  await uploadToS3(s3Config, localArchivePath, finalArchiveName);
5438
- fs.unlinkSync(localArchivePath); // Supprime l'archive locale après l'upload
5439
- } else {
5440
- logger.info(`Aucune configuration S3 trouvée. La sauvegarde reste locale : ${localArchivePath}.`);
5508
+ // ONLY if the upload succeeds, delete the local file.
5509
+ fs.unlinkSync(localArchivePath);
5510
+ logger.info(`Local archive ${finalArchiveName} deleted after successful S3 upload.`);
5511
+ } catch (e) {
5441
5512
  }
5442
5513
 
5443
5514
  logger.info(`Sauvegarde réussie pour l'utilisateur ${user.username}.`);
5444
- await manageBackupRotation(user, backupFrequency, s3Config);
5515
+ await manageBackupRotation(user, await engine.userProvider.getBackupFrequency(user), s3Config);
5445
5516
 
5446
5517
  } catch (error) {
5447
5518
  logger.error(`Erreur lors de la sauvegarde pour l'utilisateur ${user.username}:`, error);
@@ -5463,8 +5534,8 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
5463
5534
  const userId = getObjectHash({user:user.username});
5464
5535
  let filesToManage = [];
5465
5536
 
5466
- if (s3Config && s3Config.bucketName) {
5467
- logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userid}.`);
5537
+ if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
5538
+ logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userId}.`);
5468
5539
  const s3Backups = await listS3Backups(s3Config);
5469
5540
  // Filtrer pour ne garder que les backups de cet utilisateur et trier
5470
5541
  filesToManage = s3Backups
@@ -5841,6 +5912,8 @@ export async function installPack(packId, user, lang) {
5841
5912
  }
5842
5913
  }
5843
5914
 
5915
+ Event.Trigger("OnPackInstalled", "event", "system", pack);
5916
+
5844
5917
  const modifiedCount = summary.datas.inserted + summary.datas.updated;
5845
5918
  logger.info(`--- Installation of pack '${pack.name}' finished. ---`);
5846
5919
  return { success: errors.length === 0, summary, errors, modifiedCount };