data-primals-engine 1.2.1 → 1.2.3
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/README.md +69 -9
- package/client/src/DataEditor.jsx +1 -202
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +4 -2
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +2 -2
- package/src/constants.js +2 -2
- package/src/data.js +4 -4
- package/src/defaultModels.js +1 -14
- package/src/email.js +8 -6
- package/src/engine.js +12 -8
- package/src/events.js +26 -4
- package/src/filter.js +221 -0
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +174 -101
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +5 -2
- package/test/data.integration.test.js +10 -4
- package/test/events.test.js +202 -0
- package/test/file.test.js +193 -0
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
package/src/modules/data.js
CHANGED
|
@@ -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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
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,
|
|
18
|
+
install,
|
|
19
|
+
maxAlertsPerUser,
|
|
26
20
|
maxBytesPerSecondThrottleData,
|
|
27
21
|
maxExportCount,
|
|
28
22
|
maxFileSize,
|
|
29
|
-
maxFilterDepth,
|
|
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,
|
|
37
|
+
searchRequestTimeout,
|
|
38
|
+
storageSafetyMargin
|
|
43
39
|
} from "../constants.js";
|
|
44
40
|
import {
|
|
45
41
|
getCollection,
|
|
@@ -51,22 +47,14 @@ 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
|
-
|
|
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, sleep, 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";
|
|
67
55
|
import i18n from "../i18n.js";
|
|
68
56
|
import {
|
|
69
|
-
executeSafeJavascript,
|
|
57
|
+
executeSafeJavascript, processWorkflowRun,
|
|
70
58
|
runScheduledJobWithDbLock,
|
|
71
59
|
scheduleWorkflowTriggers,
|
|
72
60
|
triggerWorkflows
|
|
@@ -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 {
|
|
79
|
-
import {
|
|
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,
|
|
71
|
+
calculateTotalUserStorageUsage,
|
|
72
|
+
generateLimiter,
|
|
73
|
+
hasPermission,
|
|
84
74
|
middlewareAuthenticator,
|
|
85
75
|
userInitiator
|
|
86
76
|
} from "./user.js";
|
|
@@ -104,7 +94,6 @@ const sseConnections = new Map();
|
|
|
104
94
|
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
105
95
|
|
|
106
96
|
const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
|
|
107
|
-
const execAsync = promisify(exec);
|
|
108
97
|
const execFileAsync = promisify(execFile);
|
|
109
98
|
|
|
110
99
|
let importJobs = {};
|
|
@@ -132,6 +121,7 @@ export function sendSseToUser(username, data) {
|
|
|
132
121
|
const res = sseConnections.get(username);
|
|
133
122
|
if (res) {
|
|
134
123
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
124
|
+
Event.Trigger("sendSseToUser", "system", "calls");
|
|
135
125
|
return true;
|
|
136
126
|
}
|
|
137
127
|
logger.warn(`[SSE] Attempted to send event to disconnected user: ${username}`);
|
|
@@ -155,12 +145,13 @@ export const jobDumpUserData = async () => {
|
|
|
155
145
|
return;
|
|
156
146
|
try {
|
|
157
147
|
dumpUserData(user).catch(e => {
|
|
158
|
-
|
|
148
|
+
Event.Trigger("OnUserDataDumped", "event", "system", engine);
|
|
159
149
|
})
|
|
160
150
|
} catch (ignored) {
|
|
161
151
|
|
|
162
152
|
}
|
|
163
153
|
});
|
|
154
|
+
|
|
164
155
|
}catch (e) {
|
|
165
156
|
console.error(e);
|
|
166
157
|
}
|
|
@@ -1210,7 +1201,11 @@ export const editModel = async (user, id, data) => {
|
|
|
1210
1201
|
logger.error(`Erreur asynchrone lors du déclenchement des workflows pour ${model._model} ID ${model._id}:`, workflowError);
|
|
1211
1202
|
});
|
|
1212
1203
|
|
|
1213
|
-
|
|
1204
|
+
const newModel = await modelsCollection.findOne({_id : oid});
|
|
1205
|
+
const res = ({ success: true, data: newModel });
|
|
1206
|
+
const plugin = Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
|
|
1207
|
+
Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
|
|
1208
|
+
return plugin || res
|
|
1214
1209
|
} catch (e) {
|
|
1215
1210
|
logger.error(e);
|
|
1216
1211
|
return ({ success: false, error: e.message, statusCode: 500 });
|
|
@@ -1390,6 +1385,7 @@ export async function onInit(defaultEngine) {
|
|
|
1390
1385
|
schedule.scheduleJob("0 2 * * *", jobDumpUserData);
|
|
1391
1386
|
//await jobDumpUserData();
|
|
1392
1387
|
|
|
1388
|
+
|
|
1393
1389
|
schedule.scheduleJob("0 0 * * *", async () => {
|
|
1394
1390
|
const dt = new Date();
|
|
1395
1391
|
dt.setTime(dt.getTime()-1000*3600*24*14);
|
|
@@ -1971,7 +1967,7 @@ export async function onInit(defaultEngine) {
|
|
|
1971
1967
|
const filter = req.fields.filter;
|
|
1972
1968
|
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
1973
1969
|
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
1974
|
-
const r = await editData(req.fields.model, filter || hash, data, req.files, req.me)
|
|
1970
|
+
const r = await editData(req.fields.model, filter || { "$eq": ["$_id", { "$toObjectId": hash}]}, data, req.files, req.me)
|
|
1975
1971
|
if (r.error)
|
|
1976
1972
|
res.status(400).json(r);
|
|
1977
1973
|
else
|
|
@@ -2111,7 +2107,7 @@ export async function onInit(defaultEngine) {
|
|
|
2111
2107
|
await cancelAlerts(req.me);
|
|
2112
2108
|
|
|
2113
2109
|
await getPromise();
|
|
2114
|
-
|
|
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
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
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
|
|
@@ -2881,18 +2889,18 @@ export const deleteModels = async (filter) => {
|
|
|
2881
2889
|
}
|
|
2882
2890
|
|
|
2883
2891
|
export const getModel = async (modelName, user) => {
|
|
2884
|
-
const modelInCache = modelsCache.get(user
|
|
2892
|
+
const modelInCache = modelsCache.get((user?.username||'')+"@@"+modelName);
|
|
2885
2893
|
if(modelInCache)
|
|
2886
2894
|
return modelInCache;
|
|
2887
|
-
const model = await getCollection('models').findOne({name: modelName, $and: [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}]});
|
|
2895
|
+
const model = await getCollection('models').findOne({name: modelName, $and: user ? [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}] : [{_user: { $exists: false}}]});
|
|
2888
2896
|
if (!model) {
|
|
2889
2897
|
throw new Error(i18n.t('api.model.notFound', {model: modelName}));
|
|
2890
2898
|
}
|
|
2891
|
-
modelsCache.set(user
|
|
2899
|
+
modelsCache.set((user?.username||'')+"@@"+modelName, model);
|
|
2892
2900
|
return model;
|
|
2893
2901
|
}
|
|
2894
2902
|
export const getModels = async () => {
|
|
2895
|
-
return await getCollection('models')
|
|
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
|
-
|
|
3124
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -4102,7 +4135,7 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
|
|
|
4102
4135
|
export const searchData = async (query, user) => {
|
|
4103
4136
|
const { page, limit, sort, model, ids, timeout, pack } = query; // Les filtres de la requête (attention aux injections MongoDB !)
|
|
4104
4137
|
|
|
4105
|
-
if( user.username !== 'demo' && isLocalUser(user) && (
|
|
4138
|
+
if( user && user.username !== 'demo' && isLocalUser(user) && (
|
|
4106
4139
|
!await hasPermission(["API_ADMIN", "API_SEARCH_DATA", "API_SEARCH_DATA_"+model], user) ||
|
|
4107
4140
|
await hasPermission(["API_SEARCH_DATA_NOT_"+model], user))){
|
|
4108
4141
|
throw new Error(i18n.t('api.permission.searchData'));
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5286
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
5410
|
+
|
|
5411
|
+
// ... (Post-restore tasks) ...
|
|
5349
5412
|
await scheduleAlerts();
|
|
5350
5413
|
await scheduleWorkflowTriggers();
|
|
5351
|
-
modelsCache.flushAll();
|
|
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
|
-
// ---
|
|
5420
|
+
// --- GUARANTEED CLEANUP ---
|
|
5357
5421
|
if (fs.existsSync(tmpRestoreDir)) {
|
|
5358
5422
|
await fs.promises.rm(tmpRestoreDir, { recursive: true, force: true });
|
|
5359
5423
|
}
|
|
5360
|
-
|
|
5361
|
-
if
|
|
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
|
|
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
|
|
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
|
|
5503
|
+
await runCryptoWorkerTask('encrypt', { filePath: localArchivePath, password: encryptedKey });
|
|
5435
5504
|
|
|
5436
|
-
|
|
5505
|
+
try {
|
|
5506
|
+
// Attempt the S3 upload
|
|
5437
5507
|
await uploadToS3(s3Config, localArchivePath, finalArchiveName);
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
logger.info(`
|
|
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,
|
|
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 ${
|
|
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 };
|
|
@@ -5915,4 +5988,4 @@ export async function handleDemoInitialization(req, res) {
|
|
|
5915
5988
|
logger.error(`[Demo Init] Critical error during initialization for user '${user.username}':`, error);
|
|
5916
5989
|
res.status(500).json({ success: false, error: 'An internal server error occurred during initialization.' });
|
|
5917
5990
|
}
|
|
5918
|
-
}
|
|
5991
|
+
}
|