@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/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 };