@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/servers.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import config from './config.js';
|
|
4
|
+
const SERVERS_FILE_PATH = path.join(config.dataDir, 'servers.json');
|
|
5
|
+
|
|
6
|
+
async function readServers() {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(SERVERS_FILE_PATH);
|
|
9
|
+
const data = await fs.readFile(SERVERS_FILE_PATH, 'utf-8');
|
|
10
|
+
return JSON.parse(data);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
// Si le fichier n'existe pas, on retourne un objet vide
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function writeServers(servers) {
|
|
18
|
+
await fs.writeFile(SERVERS_FILE_PATH, JSON.stringify(servers, null, 2));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function addServer(alias, config) {
|
|
22
|
+
const servers = await readServers();
|
|
23
|
+
if (servers[alias]) {
|
|
24
|
+
// L'alias existe, on le met à jour
|
|
25
|
+
servers[alias] = { ...servers[alias], ...config };
|
|
26
|
+
await writeServers(servers);
|
|
27
|
+
return { success: true, message: `Serveur '${alias}' mis à jour avec succès.` };
|
|
28
|
+
}
|
|
29
|
+
// L'alias n'existe pas, on le crée
|
|
30
|
+
servers[alias] = config;
|
|
31
|
+
await writeServers(servers);
|
|
32
|
+
return { success: true, message: `Serveur '${alias}' ajouté avec succès.` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function removeServer(alias) {
|
|
36
|
+
const servers = await readServers();
|
|
37
|
+
if (!servers[alias]) {
|
|
38
|
+
throw new Error(`L'alias '${alias}' n'existe pas.`);
|
|
39
|
+
}
|
|
40
|
+
delete servers[alias];
|
|
41
|
+
await writeServers(servers);
|
|
42
|
+
return { success: true, message: `Serveur '${alias}' supprimé.` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function listServers() {
|
|
46
|
+
return await readServers();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function getServer(alias) {
|
|
50
|
+
const servers = await readServers();
|
|
51
|
+
const serverConfig = servers[alias];
|
|
52
|
+
if (!serverConfig) {
|
|
53
|
+
throw new Error(`L'alias de serveur '${alias}' est inconnu. Utilisez d'abord 'server_list' pour voir les alias disponibles.`);
|
|
54
|
+
}
|
|
55
|
+
return serverConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default { addServer, removeServer, listServers, getServer };
|
package/sftp.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import SftpClient from 'ssh2-sftp-client';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import queue from './queue.js';
|
|
6
|
+
import serverManager from './servers.js';
|
|
7
|
+
import micromatch from 'micromatch';
|
|
8
|
+
|
|
9
|
+
// Fonction utilitaire pour créer un dossier parent si nécessaire
|
|
10
|
+
async function ensureLocalDir(filePath) {
|
|
11
|
+
const dir = path.dirname(filePath);
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(dir);
|
|
14
|
+
} catch {
|
|
15
|
+
await fs.mkdir(dir, { recursive: true });
|
|
16
|
+
queue.log('info', `Dossier local créé: ${dir}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Fonction pour créer un dossier distant si nécessaire
|
|
21
|
+
async function ensureRemoteDir(sftp, filePath) {
|
|
22
|
+
const dir = path.dirname(filePath);
|
|
23
|
+
try {
|
|
24
|
+
const exists = await sftp.exists(dir);
|
|
25
|
+
if (!exists) {
|
|
26
|
+
await sftp.mkdir(dir, true);
|
|
27
|
+
queue.log('info', `Dossier distant créé: ${dir}`);
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Si le dossier existe déjà, on ignore l'erreur
|
|
31
|
+
if (!err.message.includes('already exists')) {
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fonction pour gérer les patterns glob et listes de fichiers
|
|
38
|
+
async function expandFileList(pattern, basePath = '') {
|
|
39
|
+
// Si c'est une liste (tableau)
|
|
40
|
+
if (Array.isArray(pattern)) {
|
|
41
|
+
const allFiles = [];
|
|
42
|
+
for (const p of pattern) {
|
|
43
|
+
const expanded = await expandFileList(p, basePath);
|
|
44
|
+
allFiles.push(...expanded);
|
|
45
|
+
}
|
|
46
|
+
return allFiles;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fonction interne pour normaliser les chemins
|
|
50
|
+
function normalizePath(filePath, bPath) {
|
|
51
|
+
if (path.isAbsolute(filePath)) {
|
|
52
|
+
return filePath;
|
|
53
|
+
}
|
|
54
|
+
return path.resolve(bPath || process.cwd(), filePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const normalizedBasePath = normalizePath(basePath);
|
|
58
|
+
|
|
59
|
+
// Si c'est un pattern glob
|
|
60
|
+
if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
|
|
61
|
+
const fullPattern = path.join(normalizedBasePath, pattern);
|
|
62
|
+
const files = await glob(fullPattern, { nodir: false });
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Sinon c'est un fichier/dossier simple
|
|
67
|
+
const fullPath = path.join(normalizedBasePath, pattern);
|
|
68
|
+
return [fullPath];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fonction principale de transfert avec support multi-fichiers
|
|
72
|
+
async function executeTransfer(jobId) {
|
|
73
|
+
const job = queue.getJob(jobId);
|
|
74
|
+
if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`);
|
|
75
|
+
|
|
76
|
+
let sftp = null;
|
|
77
|
+
try {
|
|
78
|
+
const serverConfig = await serverManager.getServer(job.alias);
|
|
79
|
+
queue.updateJobStatus(jobId, 'running');
|
|
80
|
+
|
|
81
|
+
sftp = new SftpClient();
|
|
82
|
+
|
|
83
|
+
// Configuration de la connexion
|
|
84
|
+
const config = {
|
|
85
|
+
host: serverConfig.host,
|
|
86
|
+
port: 22,
|
|
87
|
+
username: serverConfig.user,
|
|
88
|
+
readyTimeout: 20000,
|
|
89
|
+
retries: 3,
|
|
90
|
+
retry_factor: 2,
|
|
91
|
+
retry_minTimeout: 2000
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (serverConfig.keyPath) {
|
|
95
|
+
config.privateKey = await fs.readFile(serverConfig.keyPath);
|
|
96
|
+
} else if (serverConfig.password) {
|
|
97
|
+
config.password = serverConfig.password;
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Aucune méthode d'authentification pour '${job.alias}'.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await sftp.connect(config);
|
|
103
|
+
|
|
104
|
+
// Déterminer si on traite plusieurs fichiers
|
|
105
|
+
const files = job.files || [{ local: job.local, remote: job.remote }];
|
|
106
|
+
const isMultiple = Array.isArray(job.files) && job.files.length > 1;
|
|
107
|
+
|
|
108
|
+
let successCount = 0;
|
|
109
|
+
let failedFiles = [];
|
|
110
|
+
let totalFiles = 0;
|
|
111
|
+
|
|
112
|
+
// Traiter chaque fichier
|
|
113
|
+
for (let i = 0; i < files.length; i++) {
|
|
114
|
+
const file = files[i];
|
|
115
|
+
const progress = isMultiple ? ` (${i + 1}/${files.length})` : '';
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (job.direction === 'upload') {
|
|
119
|
+
const localFiles = await expandFileList(file.local);
|
|
120
|
+
totalFiles += localFiles.length;
|
|
121
|
+
for (const localFile of localFiles) {
|
|
122
|
+
queue.log('info', `Transfert${progress}: ${localFile}`);
|
|
123
|
+
await handleUpload(sftp, localFile, file.remote);
|
|
124
|
+
successCount++;
|
|
125
|
+
}
|
|
126
|
+
} else if (job.direction === 'download') {
|
|
127
|
+
const downloadedCount = await handleDownload(sftp, file.remote, file.local);
|
|
128
|
+
totalFiles += downloadedCount;
|
|
129
|
+
successCount += downloadedCount;
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
queue.log('error', `Échec transfert ${file.local || file.remote}: ${err.message}`);
|
|
133
|
+
failedFiles.push({ file: file.local || file.remote, error: err.message });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await sftp.end();
|
|
138
|
+
|
|
139
|
+
// Génération du rapport
|
|
140
|
+
let status = successCount === totalFiles ? 'completed' : 'partial';
|
|
141
|
+
let output = `Transfert ${job.direction}: ${successCount}/${totalFiles} fichiers réussis`;
|
|
142
|
+
|
|
143
|
+
if (failedFiles.length > 0) {
|
|
144
|
+
output += `\nÉchecs: ${failedFiles.map(f => f.file).join(', ')}`;
|
|
145
|
+
if (successCount === 0) status = 'failed';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
queue.updateJobStatus(jobId, status, { output, failedFiles });
|
|
149
|
+
|
|
150
|
+
} catch (err) {
|
|
151
|
+
queue.updateJobStatus(jobId, 'failed', { error: err.message });
|
|
152
|
+
} finally {
|
|
153
|
+
if (sftp) {
|
|
154
|
+
try {
|
|
155
|
+
await sftp.end();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Ignorer les erreurs de fermeture
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Gestion spécifique de l'upload
|
|
164
|
+
async function handleUpload(sftp, localPath, remotePath) {
|
|
165
|
+
try {
|
|
166
|
+
const stats = await fs.stat(localPath);
|
|
167
|
+
|
|
168
|
+
if (stats.isDirectory()) {
|
|
169
|
+
// Upload d'un dossier entier
|
|
170
|
+
await sftp.uploadDir(localPath, remotePath);
|
|
171
|
+
} else {
|
|
172
|
+
// Upload d'un fichier - créer le dossier parent si nécessaire
|
|
173
|
+
await ensureRemoteDir(sftp, remotePath);
|
|
174
|
+
await sftp.put(localPath, remotePath);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Si le fichier local n'existe pas
|
|
178
|
+
if (err.code === 'ENOENT') {
|
|
179
|
+
throw new Error(`Fichier local introuvable: ${localPath}`);
|
|
180
|
+
}
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Gestion spécifique du download
|
|
186
|
+
async function handleDownload(sftp, remotePath, localPath) {
|
|
187
|
+
// Si le chemin distant contient un glob
|
|
188
|
+
if (micromatch.isMatch(remotePath, '**/*')) {
|
|
189
|
+
const parentDir = path.dirname(remotePath);
|
|
190
|
+
const pattern = path.basename(remotePath);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const fileList = await sftp.list(parentDir);
|
|
194
|
+
const matchingFiles = micromatch(fileList.map(f => f.name), [pattern]);
|
|
195
|
+
|
|
196
|
+
if (matchingFiles.length === 0) {
|
|
197
|
+
throw new Error(`Aucun fichier distant ne correspond au pattern: ${remotePath}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// S'assurer que le dossier local existe
|
|
201
|
+
await ensureLocalDir(localPath);
|
|
202
|
+
|
|
203
|
+
// Télécharger chaque fichier correspondant
|
|
204
|
+
for (const fileName of matchingFiles) {
|
|
205
|
+
const remoteFile = path.join(parentDir, fileName);
|
|
206
|
+
const localFile = path.join(localPath, fileName);
|
|
207
|
+
queue.log('info', `Téléchargement (glob): ${remoteFile} -> ${localFile}`);
|
|
208
|
+
await sftp.get(remoteFile, localFile);
|
|
209
|
+
}
|
|
210
|
+
// Retourner le nombre de fichiers téléchargés pour le rapport
|
|
211
|
+
return matchingFiles.length;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// Si le dossier parent n'existe pas, on propage l'erreur
|
|
214
|
+
if (err.code === 2) {
|
|
215
|
+
throw new Error(`Le dossier parent pour le glob n'existe pas: ${parentDir}`);
|
|
216
|
+
}
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} else {
|
|
221
|
+
// Comportement normal pour un fichier ou un dossier unique
|
|
222
|
+
const exists = await sftp.exists(remotePath);
|
|
223
|
+
if (!exists) {
|
|
224
|
+
throw new Error(`Fichier distant introuvable: ${remotePath}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const stats = await sftp.stat(remotePath);
|
|
228
|
+
await ensureLocalDir(localPath);
|
|
229
|
+
|
|
230
|
+
if (stats.isDirectory) {
|
|
231
|
+
await sftp.downloadDir(remotePath, localPath);
|
|
232
|
+
} else {
|
|
233
|
+
await sftp.get(remotePath, localPath);
|
|
234
|
+
}
|
|
235
|
+
return 1; // Un seul item téléchargé
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Nouvelle fonction pour les transferts multiples
|
|
240
|
+
async function executeMultiTransfer(jobId) {
|
|
241
|
+
const job = queue.getJob(jobId);
|
|
242
|
+
if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`);
|
|
243
|
+
|
|
244
|
+
// Utilise la même fonction mais avec support multi-fichiers
|
|
245
|
+
return executeTransfer(jobId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default { executeTransfer, executeMultiTransfer };
|