Skip to main content
Glama
sftp.js9.48 kB
import SftpClient from 'ssh2-sftp-client'; import fs from 'fs/promises'; import { createWriteStream } from 'fs'; import path from 'path'; import { glob } from 'glob'; import queue from './queue.js'; import serverManager from './servers.js'; import micromatch from 'micromatch'; // Fonction utilitaire pour créer un dossier parent si nécessaire async function ensureLocalDir(filePath) { const dir = path.dirname(filePath); try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); queue.log('info', `Dossier local créé: ${dir}`); } } // Fonction pour créer un dossier distant si nécessaire async function ensureRemoteDir(sftp, filePath) { const dir = path.posix.dirname(filePath); // Utiliser posix pour les chemins distants if (dir === '/' || dir === '.') { return; // Pas besoin de créer la racine } try { const exists = await sftp.exists(dir); if (!exists) { await sftp.mkdir(dir, true); // true pour créer récursivement queue.log('info', `Dossier distant créé: ${dir}`); } } catch (err) { // Vérifier si c'est vraiment une erreur ou si le dossier existe déjà const exists = await sftp.exists(dir).catch(() => false); if (!exists) { throw new Error(`Impossible de créer le dossier distant ${dir}: ${err.message}`); } // Sinon, le dossier existe, on continue } } // Fonction pour gérer les patterns glob et listes de fichiers async function expandFileList(pattern, basePath = '') { // Si c'est une liste (tableau) if (Array.isArray(pattern)) { const allFiles = []; for (const p of pattern) { const expanded = await expandFileList(p, basePath); allFiles.push(...expanded); } return allFiles; } // Fonction interne pour normaliser les chemins function normalizePath(filePath, bPath) { if (path.isAbsolute(filePath)) { return filePath; } return path.resolve(bPath || process.cwd(), filePath); } const normalizedBasePath = normalizePath(basePath); // Si c'est un pattern glob if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) { const fullPattern = path.resolve(normalizedBasePath, pattern); const files = await glob(fullPattern, { nodir: false }); return files; } // Sinon c'est un fichier/dossier simple const fullPath = path.resolve(normalizedBasePath, pattern); return [fullPath]; } // Fonction principale de transfert avec support multi-fichiers async function executeTransfer(jobId) { const job = queue.getJob(jobId); if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`); let sftp = null; try { const serverConfig = await serverManager.getServer(job.alias); queue.updateJobStatus(jobId, 'running'); sftp = new SftpClient(); // Configuration de la connexion const config = { host: serverConfig.host, port: 22, username: serverConfig.user, readyTimeout: 20000, retries: 3, retry_factor: 2, retry_minTimeout: 2000 }; if (serverConfig.keyPath) { config.privateKey = await fs.readFile(serverConfig.keyPath); } else if (serverConfig.password) { config.password = serverConfig.password; } else { throw new Error(`Aucune méthode d'authentification pour '${job.alias}'.`); } await sftp.connect(config); // Déterminer si on traite plusieurs fichiers const files = job.files || [{ local: job.local, remote: job.remote }]; const isMultiple = Array.isArray(job.files) && job.files.length > 1; let successCount = 0; let failedFiles = []; let totalFiles = 0; // Traiter chaque fichier for (let i = 0; i < files.length; i++) { const file = files[i]; const progress = isMultiple ? ` (${i + 1}/${files.length})` : ''; try { if (job.direction === 'upload') { const localFiles = await expandFileList(file.local); totalFiles += localFiles.length; for (const localFile of localFiles) { queue.log('info', `Transfert${progress}: ${localFile}`); await handleUpload(sftp, localFile, file.remote); successCount++; } } else if (job.direction === 'download') { const downloadedCount = await handleDownload(sftp, file.remote, file.local); totalFiles += downloadedCount; successCount += downloadedCount; } } catch (err) { queue.log('error', `Échec transfert ${file.local || file.remote}: ${err.message}`); failedFiles.push({ file: file.local || file.remote, error: err.message }); } } await sftp.end(); // Génération du rapport let status = successCount === totalFiles ? 'completed' : 'partial'; let output = `Transfert ${job.direction}: ${successCount}/${totalFiles} fichiers réussis`; if (failedFiles.length > 0) { output += `\nÉchecs: ${failedFiles.map(f => f.file).join(', ')}`; if (successCount === 0) status = 'failed'; } queue.updateJobStatus(jobId, status, { output, failedFiles }); } catch (err) { queue.updateJobStatus(jobId, 'failed', { error: err.message }); } finally { if (sftp) { try { await sftp.end(); } catch (e) { // Ignorer les erreurs de fermeture } } } } // Gestion spécifique de l'upload async function handleUpload(sftp, localPath, remotePath) { try { const stats = await fs.stat(localPath); if (stats.isDirectory()) { // Upload d'un dossier entier await sftp.uploadDir(localPath, remotePath); } else { // Upload d'un fichier - créer le dossier parent si nécessaire await ensureRemoteDir(sftp, remotePath); await sftp.put(localPath, remotePath); } } catch (err) { // Si le fichier local n'existe pas if (err.code === 'ENOENT') { throw new Error(`Fichier local introuvable: ${localPath}`); } throw err; } } // Gestion spécifique du download async function handleDownload(sftp, remotePath, localPath) { // Si le chemin distant contient un glob if (micromatch.isMatch(remotePath, '**/*')) { const parentDir = path.dirname(remotePath); const pattern = path.basename(remotePath); try { const fileList = await sftp.list(parentDir); const matchingFiles = micromatch(fileList.map(f => f.name), [pattern]); if (matchingFiles.length === 0) { throw new Error(`Aucun fichier distant ne correspond au pattern: ${remotePath}`); } // S'assurer que le dossier local existe await fs.mkdir(localPath, { recursive: true }); // Télécharger chaque fichier correspondant for (const fileName of matchingFiles) { const remoteFile = path.join(parentDir, fileName); const localFile = path.join(localPath, fileName); queue.log('info', `Téléchargement (glob): ${remoteFile} -> ${localFile}`); const destinationStream = createWriteStream(localFile); await sftp.get(remoteFile, destinationStream); } return matchingFiles.length; } catch (err) { if (err.code === 2) { throw new Error(`Le dossier parent pour le glob n'existe pas: ${parentDir}`); } throw err; } } else { // Comportement pour un fichier ou un dossier unique const exists = await sftp.exists(remotePath); if (!exists) { throw new Error(`Fichier distant introuvable: ${remotePath}`); } const stats = await sftp.stat(remotePath); if (stats.isDirectory) { await fs.mkdir(localPath, { recursive: true }); await sftp.downloadDir(remotePath, localPath); } else { // Correction : localPath est le dossier, on ajoute le nom du fichier distant const finalLocalPath = path.join(localPath, path.basename(remotePath)); const localDir = path.dirname(finalLocalPath); await fs.mkdir(localDir, { recursive: true }); const destinationStream = createWriteStream(finalLocalPath); await sftp.get(remotePath, destinationStream); } return 1; } } // Nouvelle fonction pour les transferts multiples async function executeMultiTransfer(jobId) { const job = queue.getJob(jobId); if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`); // Utilise la même fonction mais avec support multi-fichiers return executeTransfer(jobId); } export default { executeTransfer, executeMultiTransfer };

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fkom13/mcp-sftp-orchestrator'

If you have feedback or need assistance with the MCP directory API, please join our Discord server