data-primals-engine 1.4.0 → 1.4.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.
- package/README.md +856 -797
- package/client/src/AssistantChat.jsx +48 -5
- package/client/src/AssistantChat.scss +0 -1
- package/client/src/ChartConfigModal.jsx +34 -9
- package/client/src/Dashboard.jsx +396 -349
- package/client/src/DashboardChart.jsx +91 -12
- package/client/src/DataTable.jsx +817 -807
- package/client/src/Field.jsx +1784 -1782
- package/client/src/PackGallery.jsx +391 -290
- package/client/src/PackGallery.scss +231 -210
- package/client/src/contexts/UIContext.jsx +5 -2
- package/client/src/translations.js +188 -0
- package/package.json +19 -8
- package/src/config.js +2 -2
- package/src/core.js +21 -3
- package/src/engine.js +2 -1
- package/src/events.js +4 -3
- package/src/gameObject.js +1 -1
- package/src/middlewares/middleware-mongodb.js +3 -1
- package/src/modules/assistant/assistant.js +131 -42
- package/src/modules/auth-google/index.js +51 -0
- package/src/modules/auth-microsoft/index.js +82 -0
- package/src/modules/auth-saml/index.js +90 -0
- package/src/modules/bucket.js +3 -2
- package/src/modules/data/data.backup.js +374 -0
- package/src/modules/data/data.history.js +11 -8
- package/src/modules/data/data.js +190 -4543
- package/src/modules/data/data.operations.js +2790 -0
- package/src/modules/data/data.relations.js +687 -0
- package/src/modules/data/data.routes.js +1785 -1646
- package/src/modules/data/data.scheduling.js +274 -0
- package/src/modules/data/data.validation.js +245 -0
- package/src/modules/data/index.js +29 -1
- package/src/modules/user.js +2 -1
- package/src/modules/workflow.js +3 -2
- package/src/providers.js +110 -1
- package/src/services/stripe.js +3 -2
- package/src/sso.js +194 -0
- package/test/data.integration.test.js +3 -0
- package/test/user.test.js +1 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {dbUrl, MongoClient, MongoDatabase} from "../../engine.js";
|
|
2
|
+
import {getUserId, isDemoUser} from "../../data.js";
|
|
3
|
+
import {Config} from "../../config.js";
|
|
4
|
+
import {Event} from "../../events.js";
|
|
5
|
+
import {getObjectHash} from "../../core.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {downloadFromS3, getUserS3Config, listS3Backups, uploadToS3} from "../bucket.js";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import {modelsCache, runCryptoWorkerTask} from "./data.core.js";
|
|
10
|
+
import * as tar from "tar";
|
|
11
|
+
import {getCollection, getUserCollectionName, modelsCollection} from "../mongodb.js";
|
|
12
|
+
import {removeFile} from "../file.js";
|
|
13
|
+
import {cancelAlerts, scheduleAlerts} from "./data.scheduling.js";
|
|
14
|
+
import {dbName} from "../../constants.js";
|
|
15
|
+
import {scheduleWorkflowTriggers} from "../workflow.js";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import process from "node:process";
|
|
18
|
+
import {promisify} from "node:util";
|
|
19
|
+
import {execFile} from "node:child_process";
|
|
20
|
+
import AWS from "aws-sdk";
|
|
21
|
+
import {Logger} from "../../gameObject.js";
|
|
22
|
+
|
|
23
|
+
const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
let engine, logger;
|
|
27
|
+
export function onInit(defaultEngine) {
|
|
28
|
+
engine = defaultEngine;
|
|
29
|
+
logger = engine.getComponent(Logger);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const jobDumpUserData = async () => {
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
|
|
36
|
+
const primalsDb = MongoClient.db("primals");
|
|
37
|
+
|
|
38
|
+
let usersCollection = primalsDb.collection("users");
|
|
39
|
+
const users = await usersCollection.find().toArray();
|
|
40
|
+
|
|
41
|
+
users.forEach((user) => {
|
|
42
|
+
if (isDemoUser(user) && Config.Get("useDemoAccounts"))
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
dumpUserData(user).catch(e => {
|
|
46
|
+
Event.Trigger("OnUserDataDumped", "event", "system", engine);
|
|
47
|
+
})
|
|
48
|
+
} catch (ignored) {
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let restoreRequests = {};
|
|
58
|
+
export const validateRestoreRequest = (username, token) => {
|
|
59
|
+
const request = restoreRequests[username];
|
|
60
|
+
if (!request) {
|
|
61
|
+
return {error: 'Invalid username.'};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (request.token !== token) {
|
|
65
|
+
return {error: 'Invalid token.'};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (request.expiresAt < new Date()) {
|
|
69
|
+
delete restoreRequests[username];
|
|
70
|
+
return {error: 'Token has expired.'};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
delete restoreRequests[username]; // Remove the request after validation
|
|
74
|
+
return {success: true};
|
|
75
|
+
};
|
|
76
|
+
export const loadFromDump = async (user, options = {}) => {
|
|
77
|
+
const {modelsOnly = false} = options;
|
|
78
|
+
const action = modelsOnly ? 'restore-models' : 'full-restore';
|
|
79
|
+
logger.info(`[${action}] Starting for user: ${user.username}`);
|
|
80
|
+
|
|
81
|
+
const encryptedKey = readKeyFromFile(user);
|
|
82
|
+
if (!encryptedKey) {
|
|
83
|
+
throw new Error("No encryption key found for this user. Cannot restore.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const userId = getObjectHash({user: user.username});
|
|
87
|
+
const backupDir = getBackupDir();
|
|
88
|
+
let backupFilePath = ''; // Will hold the path to the archive to be restored
|
|
89
|
+
let isTempFile = false; // Flag to know if we need to delete the file later
|
|
90
|
+
|
|
91
|
+
const tmpRestoreDir = path.join(backupDir, `tmp_restore_${userId}_${Date.now()}`);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const s3Config = await getUserS3Config(user);
|
|
95
|
+
// --- NEW LOGIC: Check for S3 config first ---
|
|
96
|
+
if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
|
|
97
|
+
logger.info(`[${action}] S3 config found for user. Searching for backups in bucket: ${s3Config.bucketName}`);
|
|
98
|
+
|
|
99
|
+
const s3Backups = await listS3Backups(s3Config);
|
|
100
|
+
const userBackups = s3Backups
|
|
101
|
+
.filter(f => f.filename.startsWith(`backup_${userId}_`))
|
|
102
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
103
|
+
|
|
104
|
+
if (userBackups.length === 0) {
|
|
105
|
+
throw new Error(`No S3 backups found for user ${user.username} in bucket ${s3Config.bucketName}.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const latestBackup = userBackups[0];
|
|
109
|
+
logger.info(`[${action}] Found latest S3 backup: ${latestBackup.key}. Downloading...`);
|
|
110
|
+
|
|
111
|
+
// Download the file to a temporary location
|
|
112
|
+
backupFilePath = path.join(backupDir, latestBackup.filename);
|
|
113
|
+
isTempFile = true;
|
|
114
|
+
await downloadFromS3(s3Config, latestBackup.key, backupFilePath);
|
|
115
|
+
logger.info(`[${action}] S3 backup downloaded to ${backupFilePath}.`);
|
|
116
|
+
|
|
117
|
+
} else {
|
|
118
|
+
// --- FALLBACK LOGIC: Look for local backups ---
|
|
119
|
+
logger.info(`[${action}] No S3 config. Searching for local backups.`);
|
|
120
|
+
const backupFilenameRegex = new RegExp(`^backup_${userId}_(\\d+)\\.tar\\.gz$`);
|
|
121
|
+
const backupFiles = fs.readdirSync(backupDir).filter(filename => backupFilenameRegex.test(filename));
|
|
122
|
+
if (backupFiles.length === 0) {
|
|
123
|
+
throw new Error(`No local backup files found for user ${user.username}.`);
|
|
124
|
+
}
|
|
125
|
+
const latestBackupFile = backupFiles.sort((a, b) => parseInt(b.match(backupFilenameRegex)[1], 10) - parseInt(a.match(backupFilenameRegex)[1], 10))[0];
|
|
126
|
+
backupFilePath = path.join(backupDir, latestBackupFile);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- The rest of the logic remains the same, operating on backupFilePath ---
|
|
130
|
+
|
|
131
|
+
await runCryptoWorkerTask('decrypt', {filePath: backupFilePath, password: encryptedKey});
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(tmpRestoreDir)) {
|
|
134
|
+
fs.mkdirSync(tmpRestoreDir, {recursive: true});
|
|
135
|
+
}
|
|
136
|
+
await tar.extract({file: backupFilePath, gzip: true, C: tmpRestoreDir, sync: true});
|
|
137
|
+
|
|
138
|
+
// ... (Cleaning logic: deleteMany, removeFile, cancelAlerts) ...
|
|
139
|
+
const datasCollection = getCollection("datas");
|
|
140
|
+
if (modelsOnly) {
|
|
141
|
+
await modelsCollection.deleteMany({_user: user.username});
|
|
142
|
+
} else {
|
|
143
|
+
await datasCollection.deleteMany({_user: user.username});
|
|
144
|
+
await modelsCollection.deleteMany({_user: user.username});
|
|
145
|
+
|
|
146
|
+
const filesCollection = getCollection("files");
|
|
147
|
+
const userFiles = await filesCollection.find({user: user.username, _model: "privateFile"}).toArray();
|
|
148
|
+
for (const file of userFiles) {
|
|
149
|
+
await removeFile(file.guid, user).catch(e => logger.error(e.message));
|
|
150
|
+
}
|
|
151
|
+
await cancelAlerts(user);
|
|
152
|
+
logger.info(`[${action}] Cleaned existing data, models, files, and alerts for user ${user.username}.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- EXÉCUTION DE MONGORESTORE ---
|
|
156
|
+
const restoreSourceDir = path.join(tmpRestoreDir, dbName);
|
|
157
|
+
if (!fs.existsSync(restoreSourceDir)) {
|
|
158
|
+
throw new Error(`Restore source directory (${restoreSourceDir}) not found.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let command;
|
|
162
|
+
const args = [
|
|
163
|
+
'--uri', dbUrl,
|
|
164
|
+
'--db', dbName
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
if (modelsOnly) {
|
|
168
|
+
args.push('--nsInclude', `${dbName}.models`);
|
|
169
|
+
} else {
|
|
170
|
+
// mongorestore accepte plusieurs fois l'option --nsInclude
|
|
171
|
+
args.push('--nsInclude', `${dbName}.datas`);
|
|
172
|
+
args.push('--nsInclude', `${dbName}.models`);
|
|
173
|
+
}
|
|
174
|
+
// Le répertoire source est le dernier argument
|
|
175
|
+
args.push(restoreSourceDir);
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
logger.info(`[${action}] Executing restore command: ${command}`);
|
|
179
|
+
await execFileAsync('mongorestore', args);
|
|
180
|
+
|
|
181
|
+
// ... (Post-restore tasks) ...
|
|
182
|
+
await scheduleAlerts();
|
|
183
|
+
await scheduleWorkflowTriggers();
|
|
184
|
+
modelsCache.flushAll();
|
|
185
|
+
|
|
186
|
+
logger.info(`[${action}] Restore successful for user ${user.username}.`);
|
|
187
|
+
await Event.Trigger("OnDataRestored", "event", "system");
|
|
188
|
+
|
|
189
|
+
} finally {
|
|
190
|
+
// --- GUARANTEED CLEANUP ---
|
|
191
|
+
if (fs.existsSync(tmpRestoreDir)) {
|
|
192
|
+
await fs.promises.rm(tmpRestoreDir, {recursive: true, force: true});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Re-encrypt the original file if it's not a temporary one
|
|
196
|
+
if (fs.existsSync(backupFilePath) && !isTempFile) {
|
|
197
|
+
await runCryptoWorkerTask('encrypt', {filePath: backupFilePath, password: encryptedKey});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If we downloaded a temp file from S3, delete it
|
|
201
|
+
if (fs.existsSync(backupFilePath) && isTempFile) {
|
|
202
|
+
fs.unlinkSync(backupFilePath);
|
|
203
|
+
logger.info(`[${action}] Deleted temporary downloaded backup file: ${backupFilePath}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// Fonction pour générer une clé aléatoire et la stocker dans un fichier
|
|
208
|
+
const generateAndStoreKey = (user) => {
|
|
209
|
+
const backupDir = getBackupDir();
|
|
210
|
+
const keyFile = path.join(backupDir, getObjectHash({id: getUserId(user)}) + '_encryption.key');
|
|
211
|
+
const key = crypto.randomBytes(16).toString('hex');
|
|
212
|
+
fs.writeFileSync(keyFile, key, {mode: 0o600}); // Permissions strictes
|
|
213
|
+
return key;
|
|
214
|
+
};
|
|
215
|
+
// Fonction pour lire la clé depuis le fichier
|
|
216
|
+
const readKeyFromFile = (user) => {
|
|
217
|
+
const backupDir = getBackupDir();
|
|
218
|
+
const keyFile = path.join(backupDir, getObjectHash({id: getUserId(user)}) + '_encryption.key');
|
|
219
|
+
if (fs.existsSync(keyFile)) {
|
|
220
|
+
return fs.readFileSync(keyFile, 'utf8');
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
export const dumpUserData = async (user) => {
|
|
225
|
+
const s3Config = await getUserS3Config(user);
|
|
226
|
+
const backupDir = getBackupDir();
|
|
227
|
+
const userId = getObjectHash({user: user.username});
|
|
228
|
+
const backupFilename = `backup_${userId}`;
|
|
229
|
+
const timestamp = Date.now();
|
|
230
|
+
|
|
231
|
+
// Déclarer les chemins ici pour qu'ils soient accessibles dans tout le scope de la fonction
|
|
232
|
+
const localTempDumpDir = path.join(backupDir, `${backupFilename}_${timestamp}_temp`);
|
|
233
|
+
const finalArchiveName = `${backupFilename}_${timestamp}.tar.gz`;
|
|
234
|
+
const localArchivePath = path.join(backupDir, finalArchiveName);
|
|
235
|
+
|
|
236
|
+
let encryptedKey = readKeyFromFile(user);
|
|
237
|
+
if (!encryptedKey) {
|
|
238
|
+
encryptedKey = generateAndStoreKey(user);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const backupFrequency = await engine.userProvider.getBackupFrequency(user);
|
|
243
|
+
logger.info(`Fréquence de sauvegarde : ${backupFrequency}.`);
|
|
244
|
+
|
|
245
|
+
const collections = await MongoDatabase.listCollections().toArray();
|
|
246
|
+
for (const collection of collections) {
|
|
247
|
+
const collsToBackup = [await getUserCollectionName(user), 'models'];
|
|
248
|
+
if (collsToBackup.includes(collection.name)) {
|
|
249
|
+
const query = {_user: user.username};
|
|
250
|
+
const args = [
|
|
251
|
+
'--uri', dbUrl,
|
|
252
|
+
'--db', dbName,
|
|
253
|
+
'--out', localTempDumpDir,
|
|
254
|
+
'--collection', collection.name,
|
|
255
|
+
'--query', JSON.stringify(query)
|
|
256
|
+
];
|
|
257
|
+
logger.info(`Exécution de la commande : mongodump ${args.join(' ')}`);
|
|
258
|
+
await execFileAsync('mongodump', args);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const dumpSourceDir = path.join(localTempDumpDir, dbName);
|
|
262
|
+
if (fs.existsSync(dumpSourceDir)) {
|
|
263
|
+
await tar.create({gzip: true, file: localArchivePath, C: localTempDumpDir}, [dbName]);
|
|
264
|
+
logger.info(`Archive de sauvegarde locale créée : ${localArchivePath}`);
|
|
265
|
+
} else {
|
|
266
|
+
logger.warn(`Le répertoire de dump ${dumpSourceDir} était vide. Aucune archive n'a créée.`);
|
|
267
|
+
return Promise.resolve();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await runCryptoWorkerTask('encrypt', {filePath: localArchivePath, password: encryptedKey});
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Attempt the S3 upload
|
|
274
|
+
await uploadToS3(s3Config, localArchivePath, finalArchiveName);
|
|
275
|
+
// ONLY if the upload succeeds, delete the local file.
|
|
276
|
+
fs.unlinkSync(localArchivePath);
|
|
277
|
+
logger.info(`Local archive ${finalArchiveName} deleted after successful S3 upload.`);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logger.info(`Sauvegarde réussie pour l'utilisateur ${user.username}.`);
|
|
282
|
+
await manageBackupRotation(user, await engine.userProvider.getBackupFrequency(user), s3Config);
|
|
283
|
+
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logger.error(`Erreur lors de la sauvegarde pour l'utilisateur ${user.username}:`, error);
|
|
286
|
+
// Nettoyage de l'archive si elle a été créée avant l'erreur
|
|
287
|
+
if (fs.existsSync(localArchivePath)) {
|
|
288
|
+
fs.unlinkSync(localArchivePath);
|
|
289
|
+
}
|
|
290
|
+
throw error; // Relancer l'erreur pour que l'appelant soit informé
|
|
291
|
+
} finally {
|
|
292
|
+
// --- NETTOYAGE GARANTI ---
|
|
293
|
+
// Ce bloc s'exécute toujours, que la sauvegarde réussisse ou échoue.
|
|
294
|
+
if (fs.existsSync(localTempDumpDir)) {
|
|
295
|
+
fs.rmSync(localTempDumpDir, {recursive: true, force: true});
|
|
296
|
+
logger.info(`Répertoire de dump temporaire ${localTempDumpDir} supprimé.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
async function manageBackupRotation(user, backupFrequency, s3Config = null) { // Accepter s3Config
|
|
302
|
+
const userId = getObjectHash({user: user.username});
|
|
303
|
+
let filesToManage = [];
|
|
304
|
+
|
|
305
|
+
if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) {
|
|
306
|
+
logger.info(`Gestion de la rotation des sauvegardes S3 pour ${userId}.`);
|
|
307
|
+
const s3Backups = await listS3Backups(s3Config);
|
|
308
|
+
// Filtrer pour ne garder que les backups de cet utilisateur et trier
|
|
309
|
+
filesToManage = s3Backups
|
|
310
|
+
.filter(f => f.filename.startsWith(`backup_${userId}_`) && f.filename.endsWith('.tar.gz'))
|
|
311
|
+
.map(f => ({
|
|
312
|
+
name: f.filename,
|
|
313
|
+
key: f.key,
|
|
314
|
+
timestamp: f.timestamp
|
|
315
|
+
})) // listS3Backups devrait fournir le timestamp
|
|
316
|
+
.sort((a, b) => b.timestamp - a.timestamp); // Tri décroissant (plus récent en premier)
|
|
317
|
+
|
|
318
|
+
} else {
|
|
319
|
+
logger.info(`Gestion de la rotation des sauvegardes locales pour ${userId}.`);
|
|
320
|
+
const backupDir = getBackupDir();
|
|
321
|
+
const localFiles = fs.readdirSync(backupDir);
|
|
322
|
+
filesToManage = localFiles
|
|
323
|
+
.filter(f => !fs.lstatSync(path.join(backupDir, f)).isDirectory() && f.startsWith(`backup_${userId}_`) && f.endsWith('.tar.gz'))
|
|
324
|
+
.map(f => {
|
|
325
|
+
const match = f.match(/_(\d+)\.tar\.gz$/);
|
|
326
|
+
return {name: f, key: path.join(backupDir, f), timestamp: match ? parseInt(match[1], 10) : 0};
|
|
327
|
+
})
|
|
328
|
+
.sort((a, b) => b.timestamp - a.timestamp); // Tri décroissant
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let maxFilesToKeep;
|
|
332
|
+
// ... (ta logique existante pour maxFilesToKeep basée sur backupFrequency)
|
|
333
|
+
switch (backupFrequency) {
|
|
334
|
+
case 'daily': // Premium
|
|
335
|
+
maxFilesToKeep = 7; // Garder 7 jours
|
|
336
|
+
break;
|
|
337
|
+
case 'weekly': // Standard
|
|
338
|
+
maxFilesToKeep = 4; // Garder 4 semaines
|
|
339
|
+
break;
|
|
340
|
+
case 'monthly': // Free
|
|
341
|
+
default:
|
|
342
|
+
maxFilesToKeep = 2; // Garder 2 mois
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
logger.info(`Rotation pour ${userId}: fréquence ${backupFrequency}, garde ${maxFilesToKeep} sauvegardes.`);
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if (filesToManage.length > maxFilesToKeep) {
|
|
349
|
+
const filesToDelete = filesToManage.slice(maxFilesToKeep);
|
|
350
|
+
logger.info(`Suppression de ${filesToDelete.length} anciennes sauvegardes pour ${userId}.`);
|
|
351
|
+
|
|
352
|
+
const deletionPromises = filesToDelete.map(async (fileInfo) => {
|
|
353
|
+
try {
|
|
354
|
+
if (s3Config && s3Config.bucketName) {
|
|
355
|
+
const s3 = new AWS.S3({ /* ... config ... */
|
|
356
|
+
accessKeyId: s3Config.accessKeyId,
|
|
357
|
+
secretAccessKey: s3Config.secretAccessKey,
|
|
358
|
+
region: s3Config.region
|
|
359
|
+
});
|
|
360
|
+
await s3.deleteObject({Bucket: s3Config.bucketName, Key: fileInfo.key}).promise();
|
|
361
|
+
logger.info(`Ancienne sauvegarde S3 supprimée : ${fileInfo.key}`);
|
|
362
|
+
} else {
|
|
363
|
+
await fs.promises.unlink(fileInfo.key); // key est le chemin complet pour les fichiers locaux
|
|
364
|
+
logger.info(`Ancienne sauvegarde locale supprimée : ${fileInfo.name}`);
|
|
365
|
+
}
|
|
366
|
+
} catch (err) {
|
|
367
|
+
logger.error(`Erreur lors de la suppression de l'ancienne sauvegarde ${fileInfo.name || fileInfo.key}:`, err);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
await Promise.allSettled(deletionPromises);
|
|
371
|
+
} else {
|
|
372
|
+
logger.info(`Aucune ancienne sauvegarde à supprimer pour ${userId} (total: ${filesToManage.length}, garde: ${maxFilesToKeep}).`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {isPlainObject} from "../../core.js";
|
|
1
|
+
import {isPlainObject, safeAssignObject} from "../../core.js";
|
|
2
2
|
import {getCollection, getCollectionForUser, isObjectId, ObjectId} from "../mongodb.js";
|
|
3
|
-
import {
|
|
3
|
+
import {handleDemoInitialization} from "./data.js";
|
|
4
4
|
import { Event} from "../../events.js"
|
|
5
5
|
import {Logger} from "../../gameObject.js";
|
|
6
6
|
import {hasPermission, middlewareAuthenticator, userInitiator} from "../user.js";
|
|
7
7
|
import {isLocalUser} from "../../data.js";
|
|
8
|
+
import {getModel} from "./data.operations.js";
|
|
8
9
|
|
|
9
|
-
let logger;
|
|
10
10
|
/**
|
|
11
11
|
* Compare deux valeurs de manière récursive. Gère les ObjectId, les objets, les tableaux et les primitives.
|
|
12
12
|
* @param {*} a - Première valeur.
|
|
@@ -55,10 +55,10 @@ function calculateDiff(beforeDoc, afterDoc, historizedFields) {
|
|
|
55
55
|
const afterValue = afterDoc[fieldName];
|
|
56
56
|
|
|
57
57
|
if (!isEqual(beforeValue, afterValue)) {
|
|
58
|
-
changes
|
|
58
|
+
safeAssignObject(changes, fieldName, {
|
|
59
59
|
from: beforeValue,
|
|
60
60
|
to: afterValue
|
|
61
|
-
};
|
|
61
|
+
});
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
// Retourne null si aucun changement n'a été détecté, ce qui est plus facile à vérifier qu'un objet vide.
|
|
@@ -264,13 +264,16 @@ export async function handleGetRevisionRequest(req, res) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
|
|
267
|
+
|
|
268
|
+
let engine, logger;
|
|
267
269
|
/**
|
|
268
270
|
* Initialise les écouteurs d'événements pour le module d'historique.
|
|
269
|
-
* @param {object}
|
|
271
|
+
* @param {object} defaultEngine - L'instance du moteur.
|
|
270
272
|
*/
|
|
271
|
-
export function onInit(
|
|
272
|
-
logger = engine.getComponent(Logger);
|
|
273
|
+
export function onInit(defaultEngine) {
|
|
273
274
|
|
|
275
|
+
engine = defaultEngine;
|
|
276
|
+
logger = engine.getComponent(Logger);
|
|
274
277
|
engine.get('/api/data/history/:modelName/:recordId', [middlewareAuthenticator, userInitiator], handleGetHistoryRequest);
|
|
275
278
|
engine.get('/api/data/history/:modelName/:recordId/:version', [middlewareAuthenticator, userInitiator], handleGetRevisionRequest);
|
|
276
279
|
engine.post('/api/data/history/:modelName/:recordId/revert/:version', [middlewareAuthenticator, userInitiator], handleRevertToRevisionRequest);
|