@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.8.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 +27 -15
- package/CHANGELOG.md +174 -0
- package/README.fr.md +356 -0
- package/README.md +314 -60
- package/apis.js +20 -64
- package/atomicJsonStore.js +117 -0
- package/compareEngine.js +132 -0
- package/config.js +40 -4
- package/diagnose.js +89 -0
- package/diffEngine.js +141 -0
- package/diffFormatter.js +114 -0
- package/fileOps.js +275 -0
- package/fleet.js +170 -0
- package/groups.js +25 -0
- package/guide.js +236 -0
- package/history.js +5 -44
- package/infraTopology.js +336 -0
- package/inventory.js +151 -0
- package/lhm.plugin.json +7 -0
- package/notes.js +3 -0
- package/package.json +11 -26
- package/policies.js +8 -0
- package/projects.js +108 -0
- package/queue.js +180 -125
- package/server.js +2552 -469
- package/servers.js +30 -33
- package/sftp.js +284 -86
- package/shellSessions.js +265 -0
- package/snapshotManager.js +320 -0
- package/sourceAdapter.js +410 -0
- package/ssh.js +184 -86
- package/sshPool.js +161 -148
- package/sshTrust.js +214 -0
- package/toolMetadata.js +76 -0
- package/tunnels.js +243 -0
- package/utils.js +215 -0
- package/workSession.js +139 -0
- package/publish1github.md +0 -216
- package/test_features.js +0 -112
package/servers.js
CHANGED
|
@@ -1,49 +1,35 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import config from './config.js';
|
|
3
|
+
import jsonStore from './atomicJsonStore.js';
|
|
4
|
+
|
|
4
5
|
const SERVERS_FILE_PATH = path.join(config.dataDir, 'servers.json');
|
|
6
|
+
const EMPTY = {};
|
|
5
7
|
|
|
6
8
|
async function readServers() {
|
|
7
|
-
|
|
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
|
-
}
|
|
9
|
+
return jsonStore.readJson(SERVERS_FILE_PATH, EMPTY);
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
async function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.` };
|
|
12
|
+
async function addServer(alias, serverConfig) {
|
|
13
|
+
let existed = false;
|
|
14
|
+
await jsonStore.updateJson(SERVERS_FILE_PATH, EMPTY, (servers) => {
|
|
15
|
+
existed = !!servers[alias];
|
|
16
|
+
servers[alias] = existed ? { ...servers[alias], ...serverConfig } : serverConfig;
|
|
17
|
+
return servers;
|
|
18
|
+
});
|
|
19
|
+
return { success: true, message: `Serveur '${alias}' ${existed ? 'mis à jour' : 'ajouté'} avec succès.` };
|
|
33
20
|
}
|
|
34
21
|
|
|
35
22
|
async function removeServer(alias) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
await writeServers(servers);
|
|
23
|
+
await jsonStore.updateJson(SERVERS_FILE_PATH, EMPTY, (servers) => {
|
|
24
|
+
if (!servers[alias]) throw new Error(`L'alias '${alias}' n'existe pas.`);
|
|
25
|
+
delete servers[alias];
|
|
26
|
+
return servers;
|
|
27
|
+
});
|
|
42
28
|
return { success: true, message: `Serveur '${alias}' supprimé.` };
|
|
43
29
|
}
|
|
44
30
|
|
|
45
31
|
async function listServers() {
|
|
46
|
-
return
|
|
32
|
+
return readServers();
|
|
47
33
|
}
|
|
48
34
|
|
|
49
35
|
async function getServer(alias) {
|
|
@@ -55,4 +41,15 @@ async function getServer(alias) {
|
|
|
55
41
|
return serverConfig;
|
|
56
42
|
}
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
/** true si l'alias est marqué readonly:true dans servers.json */
|
|
45
|
+
async function isAliasReadOnly(alias) {
|
|
46
|
+
if (!alias) return false;
|
|
47
|
+
try {
|
|
48
|
+
const sc = await getServer(alias);
|
|
49
|
+
return sc.readonly === true || sc.readOnly === true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default { addServer, removeServer, listServers, getServer, isAliasReadOnly };
|
package/sftp.js
CHANGED
|
@@ -4,7 +4,10 @@ import path from 'path';
|
|
|
4
4
|
import { glob } from 'glob';
|
|
5
5
|
import queue from './queue.js';
|
|
6
6
|
import serverManager from './servers.js';
|
|
7
|
+
import sourceAdapter from './sourceAdapter.js';
|
|
8
|
+
import fileOps from './fileOps.js';
|
|
7
9
|
import micromatch from 'micromatch';
|
|
10
|
+
import utils from './utils.js';
|
|
8
11
|
|
|
9
12
|
// Fonction utilitaire pour créer un dossier parent si nécessaire
|
|
10
13
|
async function ensureLocalDir(filePath) {
|
|
@@ -17,20 +20,31 @@ async function ensureLocalDir(filePath) {
|
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
function hasGlobPattern(str) {
|
|
24
|
+
return /[*?[\]]/.test(str);
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
// Fonction pour créer un dossier distant si nécessaire
|
|
21
28
|
async function ensureRemoteDir(sftp, filePath) {
|
|
22
|
-
const dir = path.dirname(filePath);
|
|
29
|
+
const dir = path.posix.dirname(filePath); // Utiliser posix pour les chemins distants
|
|
30
|
+
|
|
31
|
+
if (dir === '/' || dir === '.') {
|
|
32
|
+
return; // Pas besoin de créer la racine
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
try {
|
|
24
36
|
const exists = await sftp.exists(dir);
|
|
25
37
|
if (!exists) {
|
|
26
|
-
await sftp.mkdir(dir, true);
|
|
38
|
+
await sftp.mkdir(dir, true); // true pour créer récursivement
|
|
27
39
|
queue.log('info', `Dossier distant créé: ${dir}`);
|
|
28
40
|
}
|
|
29
41
|
} catch (err) {
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// Vérifier si c'est vraiment une erreur ou si le dossier existe déjà
|
|
43
|
+
const exists = await sftp.exists(dir).catch(() => false);
|
|
44
|
+
if (!exists) {
|
|
45
|
+
throw new Error(`Impossible de créer le dossier distant ${dir}: ${err.message}`);
|
|
33
46
|
}
|
|
47
|
+
// Sinon, le dossier existe, on continue
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -58,16 +72,95 @@ async function expandFileList(pattern, basePath = '') {
|
|
|
58
72
|
|
|
59
73
|
// Si c'est un pattern glob
|
|
60
74
|
if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
|
|
61
|
-
const fullPattern = path.
|
|
75
|
+
const fullPattern = path.resolve(normalizedBasePath, pattern);
|
|
76
|
+
sourceAdapter.assertLocalPathAllowed(fullPattern);
|
|
62
77
|
const files = await glob(fullPattern, { nodir: false });
|
|
78
|
+
for (const f of files) sourceAdapter.assertLocalPathAllowed(f);
|
|
63
79
|
return files;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
// Sinon c'est un fichier/dossier simple
|
|
67
|
-
const fullPath = path.
|
|
83
|
+
const fullPath = path.resolve(normalizedBasePath, pattern);
|
|
84
|
+
sourceAdapter.assertLocalPathAllowed(fullPath);
|
|
68
85
|
return [fullPath];
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Copie une entrée remote -> remote via le pool SFTP partagé.
|
|
90
|
+
*
|
|
91
|
+
* - fichier : respecte force:false et append le basename si la cible est un dossier ;
|
|
92
|
+
* - dossier : copie récursivement les fichiers sous la racine cible ; force:false
|
|
93
|
+
* refuse une cible déjà existante afin d'éviter les merges/écrasements implicites.
|
|
94
|
+
*
|
|
95
|
+
* Note : les symlinks inclus dans un arbre sont volontairement ignorés par
|
|
96
|
+
* listFilesRecursive pour l'instant. La future stratégie direct/rsync devra les
|
|
97
|
+
* préserver ; le résultat expose un warning pour ne pas promettre une copie bit-à-bit.
|
|
98
|
+
*/
|
|
99
|
+
async function transferServerToServerEntry(file, job, force) {
|
|
100
|
+
const srcAlias = file.source_alias || job.source_alias;
|
|
101
|
+
const srcPath = file.source_path || file.local || file.remote;
|
|
102
|
+
const tgtAlias = job.alias;
|
|
103
|
+
const tgtPath = file.remote;
|
|
104
|
+
|
|
105
|
+
if (!srcAlias) throw new Error("source_alias requis pour server_to_server.");
|
|
106
|
+
if (!srcPath) throw new Error("Chemin source requis (local/source_path) pour server_to_server.");
|
|
107
|
+
if (!tgtPath) throw new Error("Chemin destination remote requis pour server_to_server.");
|
|
108
|
+
|
|
109
|
+
const srcKind = await sourceAdapter.exists({ type: 'remote', alias: srcAlias, path: srcPath });
|
|
110
|
+
if (!srcKind) throw new Error(`Source introuvable: ${srcAlias}:${srcPath}`);
|
|
111
|
+
|
|
112
|
+
if (srcKind === 'd') {
|
|
113
|
+
const dstKind = await sourceAdapter.exists({ type: 'remote', alias: tgtAlias, path: tgtPath });
|
|
114
|
+
if (dstKind && dstKind !== 'd') {
|
|
115
|
+
throw new Error(`La destination ${tgtAlias}:${tgtPath} existe et n'est pas un dossier.`);
|
|
116
|
+
}
|
|
117
|
+
if (dstKind && !force) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Le dossier destination ${tgtAlias}:${tgtPath} existe déjà. ` +
|
|
120
|
+
`Utilisez force:true pour fusionner/écraser explicitement.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rels = await sourceAdapter.listFilesRecursive(
|
|
125
|
+
{ type: 'remote', alias: srcAlias, path: srcPath },
|
|
126
|
+
{ recursive: true, ignorePatterns: [] }
|
|
127
|
+
);
|
|
128
|
+
let success = 0;
|
|
129
|
+
const failures = [];
|
|
130
|
+
for (const rel of rels) {
|
|
131
|
+
const from = path.posix.join(srcPath, rel);
|
|
132
|
+
const to = path.posix.join(tgtPath, rel);
|
|
133
|
+
try {
|
|
134
|
+
const buf = await sourceAdapter.readFile({ type: 'remote', alias: srcAlias, path: from });
|
|
135
|
+
await sourceAdapter.writeFile({ type: 'remote', alias: tgtAlias, path: to }, buf.content, { createDirs: true });
|
|
136
|
+
success++;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
failures.push({ file: from, target: to, error: e.message });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
total: rels.length, success, failures,
|
|
143
|
+
warning: 'Copie dossier SFTP: symlinks et dossiers vides non préservés; utiliser une future stratégie rsync/tar pour une réplication bit-à-bit.'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let finalTarget = tgtPath;
|
|
148
|
+
const dstKind = await sourceAdapter.exists({ type: 'remote', alias: tgtAlias, path: tgtPath });
|
|
149
|
+
if (dstKind === 'd') {
|
|
150
|
+
finalTarget = path.posix.join(tgtPath, path.posix.basename(srcPath));
|
|
151
|
+
const nestedKind = await sourceAdapter.exists({ type: 'remote', alias: tgtAlias, path: finalTarget });
|
|
152
|
+
if (nestedKind && !force) {
|
|
153
|
+
throw new Error(`Le fichier distant ${tgtAlias}:${finalTarget} existe déjà. Utilisez force:true.`);
|
|
154
|
+
}
|
|
155
|
+
} else if (dstKind && !force) {
|
|
156
|
+
throw new Error(`Le fichier distant ${tgtAlias}:${tgtPath} existe déjà. Utilisez force:true.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const buf = await sourceAdapter.readFile({ type: 'remote', alias: srcAlias, path: srcPath });
|
|
160
|
+
await sourceAdapter.writeFile({ type: 'remote', alias: tgtAlias, path: finalTarget }, buf.content, { createDirs: true });
|
|
161
|
+
return { total: 1, success: 1, failures: [] };
|
|
162
|
+
}
|
|
163
|
+
|
|
71
164
|
// Fonction principale de transfert avec support multi-fichiers
|
|
72
165
|
async function executeTransfer(jobId) {
|
|
73
166
|
const job = queue.getJob(jobId);
|
|
@@ -75,68 +168,101 @@ async function executeTransfer(jobId) {
|
|
|
75
168
|
|
|
76
169
|
let sftp = null;
|
|
77
170
|
try {
|
|
78
|
-
const serverConfig = await serverManager.getServer(job.alias);
|
|
79
171
|
queue.updateJobStatus(jobId, 'running');
|
|
80
172
|
|
|
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
173
|
// Déterminer si on traite plusieurs fichiers
|
|
105
174
|
const files = job.files || [{ local: job.local, remote: job.remote }];
|
|
106
175
|
const isMultiple = Array.isArray(job.files) && job.files.length > 1;
|
|
107
|
-
|
|
176
|
+
const force = job.force === true;
|
|
177
|
+
|
|
108
178
|
let successCount = 0;
|
|
109
179
|
let failedFiles = [];
|
|
110
180
|
let totalFiles = 0;
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
181
|
+
|
|
182
|
+
// server_to_server : 100% pool via sourceAdapter (fichiers + dossiers)
|
|
183
|
+
if (job.direction === 'server_to_server') {
|
|
184
|
+
const warnings = [];
|
|
185
|
+
for (let i = 0; i < files.length; i++) {
|
|
186
|
+
const file = files[i];
|
|
187
|
+
const progress = isMultiple ? ` (${i + 1}/${files.length})` : '';
|
|
188
|
+
const srcAlias = file.source_alias || job.source_alias;
|
|
189
|
+
const srcPath = file.source_path || file.local || file.remote;
|
|
190
|
+
try {
|
|
191
|
+
queue.log('info', `Transfert server_to_server${progress}: ${srcAlias}:${srcPath} → ${job.alias}:${file.remote}`);
|
|
192
|
+
const result = await transferServerToServerEntry(file, job, force);
|
|
193
|
+
totalFiles += result.total;
|
|
194
|
+
successCount += result.success;
|
|
195
|
+
if (result.failures?.length) failedFiles.push(...result.failures);
|
|
196
|
+
if (result.warning && !warnings.includes(result.warning)) warnings.push(result.warning);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
totalFiles += 1; // l'item lui-même a été tenté, même si son contenu n'a pas pu être énuméré
|
|
199
|
+
queue.log('error', `Échec transfert ${srcPath || file.remote}: ${err.message}`);
|
|
200
|
+
failedFiles.push({ file: srcPath || file.remote, error: err.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
job.transferWarnings = warnings;
|
|
204
|
+
} else {
|
|
205
|
+
// upload/download : SftpClient (dirs/glob) avec port configurable
|
|
206
|
+
const serverConfig = await serverManager.getServer(job.alias);
|
|
207
|
+
sftp = new SftpClient();
|
|
208
|
+
const sftpConfig = {
|
|
209
|
+
host: serverConfig.host,
|
|
210
|
+
port: utils.resolveSshPort(serverConfig),
|
|
211
|
+
username: serverConfig.user,
|
|
212
|
+
readyTimeout: 20000,
|
|
213
|
+
retries: 3,
|
|
214
|
+
retry_factor: 2,
|
|
215
|
+
retry_minTimeout: 2000
|
|
216
|
+
};
|
|
217
|
+
if (serverConfig.keyPath) {
|
|
218
|
+
sftpConfig.privateKey = await fs.readFile(serverConfig.keyPath);
|
|
219
|
+
} else if (serverConfig.password) {
|
|
220
|
+
sftpConfig.password = serverConfig.password;
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error(`Aucune méthode d'authentification pour '${job.alias}'.`);
|
|
223
|
+
}
|
|
224
|
+
await sftp.connect(sftpConfig);
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < files.length; i++) {
|
|
227
|
+
const file = files[i];
|
|
228
|
+
const progress = isMultiple ? ` (${i + 1}/${files.length})` : '';
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (job.direction === 'upload') {
|
|
232
|
+
const localFiles = await expandFileList(file.local);
|
|
233
|
+
totalFiles += localFiles.length;
|
|
234
|
+
for (const localFile of localFiles) {
|
|
235
|
+
queue.log('info', `Transfert${progress}: ${localFile}`);
|
|
236
|
+
// Fichier simple (pas dir) → pool sourceAdapter quand possible
|
|
237
|
+
let isDir = false;
|
|
238
|
+
try {
|
|
239
|
+
const st = await fs.stat(localFile);
|
|
240
|
+
isDir = st.isDirectory();
|
|
241
|
+
} catch { /* handled in handleUpload */ }
|
|
242
|
+
if (!isDir && !hasGlobPattern(file.remote || '')) {
|
|
243
|
+
const content = await fs.readFile(localFile);
|
|
244
|
+
let remotePath = file.remote;
|
|
245
|
+
// Si remote est un dossier existant, append basename — handleUpload fait ça ;
|
|
246
|
+
// pour pool on délègue à handleUpload si exists dir, sinon writeFile
|
|
247
|
+
await handleUpload(sftp, localFile, file.remote, force);
|
|
248
|
+
} else {
|
|
249
|
+
await handleUpload(sftp, localFile, file.remote, force);
|
|
250
|
+
}
|
|
251
|
+
successCount++;
|
|
252
|
+
}
|
|
253
|
+
} else if (job.direction === 'download') {
|
|
254
|
+
const downloadedCount = await handleDownload(sftp, file.remote, file.local, force);
|
|
255
|
+
totalFiles += downloadedCount;
|
|
256
|
+
successCount += downloadedCount;
|
|
125
257
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
successCount += downloadedCount;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
queue.log('error', `Échec transfert ${file.local || file.remote}: ${err.message}`);
|
|
260
|
+
failedFiles.push({ file: file.local || file.remote, error: err.message });
|
|
130
261
|
}
|
|
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
262
|
}
|
|
135
263
|
}
|
|
136
264
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Génération du rapport
|
|
265
|
+
// Génération du rapport (end() uniquement dans finally pour éviter double close)
|
|
140
266
|
let status = successCount === totalFiles ? 'completed' : 'partial';
|
|
141
267
|
let output = `Transfert ${job.direction}: ${successCount}/${totalFiles} fichiers réussis`;
|
|
142
268
|
|
|
@@ -145,7 +271,10 @@ async function executeTransfer(jobId) {
|
|
|
145
271
|
if (successCount === 0) status = 'failed';
|
|
146
272
|
}
|
|
147
273
|
|
|
148
|
-
|
|
274
|
+
if (job.transferWarnings?.length) {
|
|
275
|
+
output += `\nAvertissements: ${job.transferWarnings.join(' | ')}`;
|
|
276
|
+
}
|
|
277
|
+
queue.updateJobStatus(jobId, status, { output, failedFiles, warnings: job.transferWarnings || [] });
|
|
149
278
|
|
|
150
279
|
} catch (err) {
|
|
151
280
|
queue.updateJobStatus(jobId, 'failed', { error: err.message });
|
|
@@ -161,31 +290,65 @@ async function executeTransfer(jobId) {
|
|
|
161
290
|
}
|
|
162
291
|
|
|
163
292
|
// Gestion spécifique de l'upload
|
|
164
|
-
async function handleUpload(sftp, localPath, remotePath) {
|
|
293
|
+
async function handleUpload(sftp, localPath, remotePath, force = false) {
|
|
294
|
+
sourceAdapter.assertLocalPathAllowed(localPath);
|
|
295
|
+
let localStats;
|
|
165
296
|
try {
|
|
166
|
-
|
|
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
|
-
}
|
|
297
|
+
localStats = await fs.stat(localPath);
|
|
176
298
|
} catch (err) {
|
|
177
|
-
// Si le fichier local n'existe pas
|
|
178
299
|
if (err.code === 'ENOENT') {
|
|
179
300
|
throw new Error(`Fichier local introuvable: ${localPath}`);
|
|
180
301
|
}
|
|
181
302
|
throw err;
|
|
182
303
|
}
|
|
304
|
+
|
|
305
|
+
if (localStats.isDirectory()) {
|
|
306
|
+
const remoteExists = await sftp.exists(remotePath).catch(() => false);
|
|
307
|
+
if (remoteExists) {
|
|
308
|
+
try {
|
|
309
|
+
const remoteStats = await sftp.stat(remotePath);
|
|
310
|
+
if (!remoteStats.isDirectory) {
|
|
311
|
+
throw new Error(`Impossible d'envoyer un dossier local vers un fichier distant existant: ${remotePath}. Spécifiez un dossier de destination.`);
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
if (e.message && e.message.includes('Impossible d')) throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await sftp.uploadDir(localPath, remotePath);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let finalRemotePath;
|
|
322
|
+
const remoteExists = await sftp.exists(remotePath).catch(() => false);
|
|
323
|
+
|
|
324
|
+
if (remoteExists) {
|
|
325
|
+
let remoteStats;
|
|
326
|
+
try {
|
|
327
|
+
remoteStats = await sftp.stat(remotePath);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
remoteStats = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (remoteStats && remoteStats.isDirectory) {
|
|
333
|
+
finalRemotePath = path.posix.join(remotePath, path.basename(localPath));
|
|
334
|
+
} else {
|
|
335
|
+
if (!force) {
|
|
336
|
+
throw new Error(`Le fichier distant ${remotePath} existe déjà. Utilisez force:true pour l'écraser.`);
|
|
337
|
+
}
|
|
338
|
+
finalRemotePath = remotePath;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
finalRemotePath = remotePath;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await ensureRemoteDir(sftp, finalRemotePath);
|
|
345
|
+
await sftp.put(localPath, finalRemotePath);
|
|
183
346
|
}
|
|
184
347
|
|
|
185
348
|
// Gestion spécifique du download
|
|
186
|
-
async function handleDownload(sftp, remotePath, localPath) {
|
|
187
|
-
|
|
188
|
-
if (
|
|
349
|
+
async function handleDownload(sftp, remotePath, localPath, force = false) {
|
|
350
|
+
sourceAdapter.assertLocalPathAllowed(localPath);
|
|
351
|
+
if (hasGlobPattern(remotePath)) {
|
|
189
352
|
const parentDir = path.dirname(remotePath);
|
|
190
353
|
const pattern = path.basename(remotePath);
|
|
191
354
|
|
|
@@ -197,20 +360,23 @@ async function handleDownload(sftp, remotePath, localPath) {
|
|
|
197
360
|
throw new Error(`Aucun fichier distant ne correspond au pattern: ${remotePath}`);
|
|
198
361
|
}
|
|
199
362
|
|
|
200
|
-
|
|
201
|
-
await ensureLocalDir(localPath);
|
|
363
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
202
364
|
|
|
203
|
-
// Télécharger chaque fichier correspondant
|
|
204
365
|
for (const fileName of matchingFiles) {
|
|
205
|
-
const remoteFile = path.join(parentDir, fileName);
|
|
366
|
+
const remoteFile = path.posix.join(parentDir, fileName);
|
|
206
367
|
const localFile = path.join(localPath, fileName);
|
|
368
|
+
sourceAdapter.assertLocalPathAllowed(localFile);
|
|
369
|
+
|
|
370
|
+
const localFileExists = await fs.access(localFile).then(() => true).catch(() => false);
|
|
371
|
+
if (localFileExists && !force) {
|
|
372
|
+
throw new Error(`Le fichier local ${localFile} existe déjà. Utilisez force:true pour l'écraser.`);
|
|
373
|
+
}
|
|
374
|
+
|
|
207
375
|
queue.log('info', `Téléchargement (glob): ${remoteFile} -> ${localFile}`);
|
|
208
376
|
await sftp.get(remoteFile, localFile);
|
|
209
377
|
}
|
|
210
|
-
// Retourner le nombre de fichiers téléchargés pour le rapport
|
|
211
378
|
return matchingFiles.length;
|
|
212
379
|
} catch (err) {
|
|
213
|
-
// Si le dossier parent n'existe pas, on propage l'erreur
|
|
214
380
|
if (err.code === 2) {
|
|
215
381
|
throw new Error(`Le dossier parent pour le glob n'existe pas: ${parentDir}`);
|
|
216
382
|
}
|
|
@@ -218,21 +384,53 @@ async function handleDownload(sftp, remotePath, localPath) {
|
|
|
218
384
|
}
|
|
219
385
|
|
|
220
386
|
} else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!exists) {
|
|
387
|
+
const remoteExists = await sftp.exists(remotePath);
|
|
388
|
+
if (!remoteExists) {
|
|
224
389
|
throw new Error(`Fichier distant introuvable: ${remotePath}`);
|
|
225
390
|
}
|
|
226
391
|
|
|
227
|
-
const
|
|
228
|
-
await ensureLocalDir(localPath);
|
|
392
|
+
const remoteStats = await sftp.stat(remotePath);
|
|
229
393
|
|
|
230
|
-
if (
|
|
394
|
+
if (remoteStats.isDirectory) {
|
|
395
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
231
396
|
await sftp.downloadDir(remotePath, localPath);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let finalLocalPath;
|
|
401
|
+
const localExists = await fs.access(localPath).then(() => true).catch(() => false);
|
|
402
|
+
|
|
403
|
+
if (localExists) {
|
|
404
|
+
let localStats;
|
|
405
|
+
try {
|
|
406
|
+
localStats = await fs.stat(localPath);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
localStats = null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (localStats && localStats.isDirectory()) {
|
|
412
|
+
finalLocalPath = path.join(localPath, path.basename(remotePath));
|
|
413
|
+
} else {
|
|
414
|
+
if (!force) {
|
|
415
|
+
throw new Error(`Le fichier local ${localPath} existe déjà. Utilisez force:true pour l'écraser.`);
|
|
416
|
+
}
|
|
417
|
+
finalLocalPath = localPath;
|
|
418
|
+
}
|
|
232
419
|
} else {
|
|
233
|
-
|
|
420
|
+
const hasExt = path.extname(localPath) !== '';
|
|
421
|
+
if (hasExt) {
|
|
422
|
+
finalLocalPath = localPath;
|
|
423
|
+
} else {
|
|
424
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
425
|
+
finalLocalPath = path.join(localPath, path.basename(remotePath));
|
|
426
|
+
}
|
|
234
427
|
}
|
|
235
|
-
|
|
428
|
+
|
|
429
|
+
sourceAdapter.assertLocalPathAllowed(finalLocalPath);
|
|
430
|
+
const localDir = path.dirname(finalLocalPath);
|
|
431
|
+
await fs.mkdir(localDir, { recursive: true });
|
|
432
|
+
await sftp.get(remotePath, finalLocalPath);
|
|
433
|
+
return 1;
|
|
236
434
|
}
|
|
237
435
|
}
|
|
238
436
|
|