@fkom13/mcp-sftp-orchestrator 6.0.0
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/.env.example +22 -0
- package/README.md +102 -0
- package/apis.js +88 -0
- package/config.js +34 -0
- package/history.js +44 -0
- package/package.json +40 -0
- package/publish1github.md +216 -0
- package/queue.js +377 -0
- package/server.js +773 -0
- package/servers.js +58 -0
- package/sftp.js +248 -0
- package/ssh.js +492 -0
- package/sshPool.js +272 -0
- package/test_features.js +112 -0
package/queue.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import config from './config.js';
|
|
5
|
+
|
|
6
|
+
const QUEUE_FILE = path.join(config.dataDir, 'queue.json');
|
|
7
|
+
const QUEUE_BACKUP = path.join(config.dataDir, 'queue.backup.json');
|
|
8
|
+
const SAVE_INTERVAL = 5000; // Sauvegarder toutes les 5 secondes
|
|
9
|
+
const MAX_QUEUE_SIZE = 1000; // Limite de tâches en mémoire
|
|
10
|
+
|
|
11
|
+
const jobQueue = {};
|
|
12
|
+
const logHistory = [];
|
|
13
|
+
const MAX_LOGS = 500;
|
|
14
|
+
|
|
15
|
+
let saveTimer = null;
|
|
16
|
+
let isDirty = false;
|
|
17
|
+
|
|
18
|
+
// Charger la queue au démarrage
|
|
19
|
+
async function loadQueue() {
|
|
20
|
+
try {
|
|
21
|
+
const data = await fs.readFile(QUEUE_FILE, 'utf-8');
|
|
22
|
+
const savedQueue = JSON.parse(data);
|
|
23
|
+
|
|
24
|
+
// Restaurer les tâches non terminées
|
|
25
|
+
for (const [id, job] of Object.entries(savedQueue)) {
|
|
26
|
+
// Convertir les dates string en objets Date
|
|
27
|
+
if (job.createdAt) job.createdAt = new Date(job.createdAt);
|
|
28
|
+
if (job.updatedAt) job.updatedAt = new Date(job.updatedAt);
|
|
29
|
+
if (job.reminderAt) job.reminderAt = new Date(job.reminderAt);
|
|
30
|
+
|
|
31
|
+
// Marquer les tâches running comme crashed
|
|
32
|
+
if (job.status === 'running') {
|
|
33
|
+
job.status = 'crashed';
|
|
34
|
+
job.crashedAt = new Date();
|
|
35
|
+
job.canRetry = true;
|
|
36
|
+
log('warn', `Tâche ${id} marquée comme crashed (reprise après redémarrage)`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
jobQueue[id] = job;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log('info', `${Object.keys(jobQueue).length} tâches restaurées depuis la sauvegarde`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code !== 'ENOENT') {
|
|
45
|
+
log('error', `Erreur lors du chargement de la queue: ${err.message}`);
|
|
46
|
+
|
|
47
|
+
// Essayer de charger la sauvegarde de secours
|
|
48
|
+
try {
|
|
49
|
+
const backupData = await fs.readFile(QUEUE_BACKUP, 'utf-8');
|
|
50
|
+
const backupQueue = JSON.parse(backupData);
|
|
51
|
+
Object.assign(jobQueue, backupQueue);
|
|
52
|
+
log('info', 'Queue restaurée depuis la sauvegarde de secours');
|
|
53
|
+
} catch (backupErr) {
|
|
54
|
+
log('warn', 'Aucune sauvegarde de queue trouvée, démarrage avec une queue vide');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let isSaving = false;
|
|
61
|
+
|
|
62
|
+
// Sauvegarder la queue
|
|
63
|
+
async function saveQueue() {
|
|
64
|
+
if (!isDirty || isSaving) return;
|
|
65
|
+
|
|
66
|
+
isSaving = true;
|
|
67
|
+
try {
|
|
68
|
+
// Créer une sauvegarde de l'ancienne queue
|
|
69
|
+
try {
|
|
70
|
+
await fs.copyFile(QUEUE_FILE, QUEUE_BACKUP);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Ignorer si le fichier n'existe pas encore
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Filtrer les tâches terminées anciennes (garder 24h)
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const filteredQueue = {};
|
|
78
|
+
|
|
79
|
+
for (const [id, job] of Object.entries(jobQueue)) {
|
|
80
|
+
const age = now - new Date(job.createdAt).getTime();
|
|
81
|
+
const isRecent = age < 86400000; // 24 heures
|
|
82
|
+
const isActive = ['pending', 'running', 'crashed'].includes(job.status);
|
|
83
|
+
|
|
84
|
+
if (isActive || isRecent) {
|
|
85
|
+
filteredQueue[id] = job;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sauvegarder la queue filtrée
|
|
90
|
+
await fs.writeFile(
|
|
91
|
+
QUEUE_FILE,
|
|
92
|
+
JSON.stringify(filteredQueue, null, 2)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
isDirty = false;
|
|
96
|
+
log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
|
|
99
|
+
} finally {
|
|
100
|
+
isSaving = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Démarrer la sauvegarde automatique
|
|
105
|
+
function startAutoSave() {
|
|
106
|
+
if (saveTimer) clearInterval(saveTimer);
|
|
107
|
+
|
|
108
|
+
saveTimer = setInterval(() => {
|
|
109
|
+
saveQueue();
|
|
110
|
+
}, SAVE_INTERVAL);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Arrêter la sauvegarde automatique
|
|
114
|
+
function stopAutoSave() {
|
|
115
|
+
if (saveTimer) {
|
|
116
|
+
clearInterval(saveTimer);
|
|
117
|
+
saveTimer = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function log(level, message) {
|
|
122
|
+
const logEntry = { level, message, timestamp: new Date().toISOString() };
|
|
123
|
+
logHistory.push(logEntry);
|
|
124
|
+
if (logHistory.length > MAX_LOGS) {
|
|
125
|
+
logHistory.shift();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Afficher les logs importants
|
|
129
|
+
if (['error', 'warn', 'info'].includes(level)) {
|
|
130
|
+
const prefix = {
|
|
131
|
+
error: '[❌ ERROR]',
|
|
132
|
+
warn: '[⚠️ WARN]',
|
|
133
|
+
info: '[ℹ️ INFO]',
|
|
134
|
+
debug: '[🔧 DEBUG]'
|
|
135
|
+
}[level] || `[${level.toUpperCase()}]`;
|
|
136
|
+
|
|
137
|
+
console.log(`${prefix} ${new Date().toISOString().split('T')[1].split('.')[0]} - ${message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function addJob(details) {
|
|
142
|
+
// Vérifier la taille de la queue
|
|
143
|
+
if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
|
|
144
|
+
// Nettoyer les vieilles tâches terminées
|
|
145
|
+
cleanOldJobs();
|
|
146
|
+
|
|
147
|
+
if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
|
|
148
|
+
throw new Error(`Queue pleine (${MAX_QUEUE_SIZE} tâches max)`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const id = uuidv4().split('-')[0];
|
|
153
|
+
const job = {
|
|
154
|
+
id,
|
|
155
|
+
type: details.type || 'unknown',
|
|
156
|
+
...details,
|
|
157
|
+
createdAt: new Date(),
|
|
158
|
+
retryCount: 0,
|
|
159
|
+
maxRetries: details.maxRetries || 3
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (details.rappel && details.rappel > 0) {
|
|
163
|
+
job.reminderAt = new Date(Date.now() + details.rappel * 1000);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
jobQueue[id] = job;
|
|
167
|
+
isDirty = true;
|
|
168
|
+
log('info', `Nouvelle tâche ${id} (${job.type}) ajoutée.`);
|
|
169
|
+
return jobQueue[id];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function updateJobStatus(id, status, data = {}) {
|
|
173
|
+
if (jobQueue[id]) {
|
|
174
|
+
const oldStatus = jobQueue[id].status;
|
|
175
|
+
jobQueue[id].status = status;
|
|
176
|
+
jobQueue[id].updatedAt = new Date();
|
|
177
|
+
|
|
178
|
+
// Copier toutes les données supplémentaires
|
|
179
|
+
Object.assign(jobQueue[id], data);
|
|
180
|
+
|
|
181
|
+
if (data.error) {
|
|
182
|
+
jobQueue[id].error = data.error;
|
|
183
|
+
jobQueue[id].failedAt = new Date();
|
|
184
|
+
log('error', `Tâche ${id} échouée: ${data.error}`);
|
|
185
|
+
} else if (status === 'completed') {
|
|
186
|
+
jobQueue[id].completedAt = new Date();
|
|
187
|
+
const duration = jobQueue[id].completedAt - jobQueue[id].createdAt;
|
|
188
|
+
jobQueue[id].duration = duration;
|
|
189
|
+
log('info', `Tâche ${id} terminée en ${(duration/1000).toFixed(2)}s`);
|
|
190
|
+
} else {
|
|
191
|
+
log('info', `Tâche ${id}: ${oldStatus} -> ${status}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isDirty = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getJob(id) {
|
|
199
|
+
return jobQueue[id];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getQueue() {
|
|
203
|
+
return jobQueue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getLogs(filter = {}) {
|
|
207
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
208
|
+
return logHistory;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return logHistory.filter(log => {
|
|
212
|
+
if (filter.level && log.level !== filter.level) return false;
|
|
213
|
+
if (filter.since && new Date(log.timestamp) < new Date(filter.since)) return false;
|
|
214
|
+
if (filter.search && !log.message.toLowerCase().includes(filter.search.toLowerCase())) return false;
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Nettoyer les vieilles tâches terminées
|
|
220
|
+
function cleanOldJobs() {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const toDelete = [];
|
|
223
|
+
|
|
224
|
+
for (const [id, job] of Object.entries(jobQueue)) {
|
|
225
|
+
const age = now - new Date(job.createdAt).getTime();
|
|
226
|
+
const isOld = age > 86400000; // Plus de 24h
|
|
227
|
+
const isCompleted = ['completed', 'failed'].includes(job.status);
|
|
228
|
+
|
|
229
|
+
if (isOld && isCompleted) {
|
|
230
|
+
toDelete.push(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const id of toDelete) {
|
|
235
|
+
delete jobQueue[id];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (toDelete.length > 0) {
|
|
239
|
+
log('info', `${toDelete.length} vieilles tâches supprimées de la queue`);
|
|
240
|
+
isDirty = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Réessayer une tâche échouée ou crashed
|
|
245
|
+
async function retryJob(id) {
|
|
246
|
+
const job = jobQueue[id];
|
|
247
|
+
if (!job) {
|
|
248
|
+
throw new Error(`Tâche ${id} introuvable`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!['failed', 'crashed'].includes(job.status)) {
|
|
252
|
+
throw new Error(`La tâche ${id} ne peut pas être réessayée (statut: ${job.status})`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (job.retryCount >= job.maxRetries) {
|
|
256
|
+
throw new Error(`La tâche ${id} a atteint le nombre max de tentatives (${job.maxRetries})`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Créer une nouvelle tâche basée sur l'ancienne
|
|
260
|
+
const newJob = {
|
|
261
|
+
...job,
|
|
262
|
+
id: uuidv4().split('-')[0],
|
|
263
|
+
status: 'pending',
|
|
264
|
+
retryCount: (job.retryCount || 0) + 1,
|
|
265
|
+
retriedFrom: id,
|
|
266
|
+
createdAt: new Date(),
|
|
267
|
+
updatedAt: new Date(),
|
|
268
|
+
error: null,
|
|
269
|
+
output: null
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
delete newJob.failedAt;
|
|
273
|
+
delete newJob.crashedAt;
|
|
274
|
+
delete newJob.completedAt;
|
|
275
|
+
|
|
276
|
+
jobQueue[newJob.id] = newJob;
|
|
277
|
+
isDirty = true;
|
|
278
|
+
|
|
279
|
+
log('info', `Tâche ${id} réessayée -> nouvelle tâche ${newJob.id} (tentative ${newJob.retryCount}/${newJob.maxRetries})`);
|
|
280
|
+
|
|
281
|
+
return newJob;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Obtenir les tâches crashées qui peuvent être reprises
|
|
285
|
+
function getCrashedJobs() {
|
|
286
|
+
return Object.values(jobQueue).filter(job =>
|
|
287
|
+
job.status === 'crashed' &&
|
|
288
|
+
job.canRetry &&
|
|
289
|
+
job.retryCount < job.maxRetries
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Statistiques de la queue
|
|
294
|
+
function getStats() {
|
|
295
|
+
const stats = {
|
|
296
|
+
total: Object.keys(jobQueue).length,
|
|
297
|
+
byStatus: {},
|
|
298
|
+
byType: {},
|
|
299
|
+
avgDuration: 0,
|
|
300
|
+
successRate: 0
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let totalDuration = 0;
|
|
304
|
+
let completedCount = 0;
|
|
305
|
+
|
|
306
|
+
for (const job of Object.values(jobQueue)) {
|
|
307
|
+
// Par statut
|
|
308
|
+
stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
|
|
309
|
+
|
|
310
|
+
// Par type
|
|
311
|
+
stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
|
|
312
|
+
|
|
313
|
+
// Durée moyenne
|
|
314
|
+
if (job.duration) {
|
|
315
|
+
totalDuration += job.duration;
|
|
316
|
+
completedCount++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (completedCount > 0) {
|
|
321
|
+
stats.avgDuration = Math.round(totalDuration / completedCount);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const totalFinished = (stats.byStatus.completed || 0) + (stats.byStatus.failed || 0);
|
|
325
|
+
if (totalFinished > 0) {
|
|
326
|
+
stats.successRate = Math.round((stats.byStatus.completed || 0) / totalFinished * 100);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return stats;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Initialiser le module
|
|
333
|
+
async function init() {
|
|
334
|
+
await loadQueue();
|
|
335
|
+
startAutoSave();
|
|
336
|
+
|
|
337
|
+
// Nettoyer périodiquement
|
|
338
|
+
setInterval(cleanOldJobs, 3600000); // Toutes les heures
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Arrêter proprement
|
|
342
|
+
async function shutdown() {
|
|
343
|
+
log('info', 'Arrêt du gestionnaire de queue...');
|
|
344
|
+
stopAutoSave();
|
|
345
|
+
await saveQueue();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Gérer l'arrêt du processus
|
|
349
|
+
process.on('SIGINT', async () => {
|
|
350
|
+
await shutdown();
|
|
351
|
+
process.exit(0);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
process.on('SIGTERM', async () => {
|
|
355
|
+
await shutdown();
|
|
356
|
+
process.exit(0);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Démarrer automatiquement
|
|
360
|
+
init().catch(err => {
|
|
361
|
+
console.error('Erreur lors de l\'initialisation de la queue:', err);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export default {
|
|
365
|
+
addJob,
|
|
366
|
+
updateJobStatus,
|
|
367
|
+
getJob,
|
|
368
|
+
getQueue,
|
|
369
|
+
getLogs,
|
|
370
|
+
log,
|
|
371
|
+
retryJob,
|
|
372
|
+
getCrashedJobs,
|
|
373
|
+
getStats,
|
|
374
|
+
cleanOldJobs,
|
|
375
|
+
saveQueue,
|
|
376
|
+
shutdown
|
|
377
|
+
};
|