Exporta archivos a SFTP en Node.js con ssh2-sftp-client
Sube un archivo local con put(), descárgalo con
get() y compara los bytes antes de informar que la operación se completó
correctamente. Este tutorial usa ssh2-sftp-client con autenticación mediante clave SSH
y un servidor OpenSSH local temporal, para que puedas probar la transferencia completa sin una
cuenta SFTP existente.
Autentica tanto al servidor como al usuario
SFTP transfiere archivos a través de SSH. Tu clave de cliente te identifica ante el servidor; la clave
de host del servidor lo identifica ante ti. El cliente ssh2
subyacente acepta automáticamente las claves de host a menos que proporciones un
hostVerifier, por lo que el ejemplo comprueba una explícitamente.
Para un servidor existente, obtén la huella digital de su clave de host del administrador a través de
un canal de confianza. Con hostHash: 'sha256', ssh2 pasa al
verificador un resumen hexadecimal de 64 caracteres en minúsculas de los bytes sin procesar de la
clave pública SSH. Esa representación es distinta de la huella digital en base64
SHA256: que imprimen las herramientas de OpenSSH. No obtengas el valor esperado
de una primera conexión sin verificar.
Crea el proyecto del cliente
Los siguientes comandos usan Bash, Node.js 26.8.1, Yarn 4.12.0, ssh-keygen y
Docker Engine 28 o posterior en Linux. El cliente está fijado a la versión
ssh2-sftp-client 12.1.1.
Node ejecuta el archivo TypeScript directamente; no se necesita un
paso de compilación. El ejemplo mantiene ambas copias del archivo en memoria, así que usa un archivo
pequeño que quepa holgadamente en la RAM.
Ejecuta esto en un directorio donde aún no exista node-sftp-demo. La cadena
&& detiene la configuración si falla la creación del directorio o el
acceso a él. El archivo de bloqueo vacío mantiene este proyecto de Yarn separado, incluso cuando su
directorio padre es otro proyecto.
mkdir node-sftp-demo &&
cd node-sftp-demo &&
printf '{"private":true,"type":"module"}\n' > package.json &&
touch yarn.lock &&
yarn add --exact ssh2-sftp-client@12.1.1 &&
ssh-keygen -q -t ed25519 -N '' -f client_key &&
printf 'Hello over SFTP.\n' > example.txt
Permanece en este directorio para ejecutar los comandos restantes. client_key es
una clave privada desechable y sin cifrar para este ejercicio local. Solo su parte pública se copia
al contenedor. Mantén las claves privadas reales fuera del control de versiones y usa la
configuración de autenticación aprobada para tu servidor.
Inicia un servidor SFTP local
Ejecuta lo siguiente en la primera terminal. Instala OpenSSH dentro de un contenedor Ubuntu 24.04,
crea el usuario demo y le proporciona un directorio
/home/demo/incoming privado y con permiso de escritura.
ForceCommand internal-sftp restringe las sesiones a SFTP;
el inicio de sesión con contraseña y el reenvío están deshabilitados.
docker run --rm --name node-sftp-demo \
--publish 127.0.0.1::22 \
--mount "type=bind,src=$PWD/client_key.pub,dst=/client_key.pub,readonly" \
ubuntu:24.04 bash -euc '
if ! command -v sshd >/dev/null; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server
fi
useradd -m -s /bin/sh demo
passwd -d demo
install -d -m 700 -o demo -g demo /home/demo/.ssh /home/demo/incoming
install -m 600 -o demo -g demo /client_key.pub /home/demo/.ssh/authorized_keys
mkdir -p /run/sshd
ssh-keygen -q -t ed25519 -N "" -f /etc/ssh/demo_host_key
exec /usr/sbin/sshd -D -e -f /dev/null \
-o HostKey=/etc/ssh/demo_host_key \
-o PasswordAuthentication=no \
-o KbdInteractiveAuthentication=no \
-o PermitRootLogin=no \
-o AllowUsers=demo \
-o DisableForwarding=yes \
-o "Subsystem=sftp internal-sftp" \
-o ForceCommand=internal-sftp
'
Déjalo en ejecución después de que imprima una línea que comience con
Server listening on. Si termina antes, resuelve el error indicado antes de continuar.
Docker elige un puerto disponible en el host y
lo vincula a la interfaz de bucle local. Los archivos y la clave de
host del servidor solo existen en este contenedor y desaparecen cuando se elimina.
Abre una segunda terminal en node-sftp-demo. Consulta el puerto asignado y copia la
clave pública del host a través de tu conexión local de Docker, que es el canal administrativo de
confianza para este ejemplo:
docker port node-sftp-demo 22/tcp &&
docker cp node-sftp-demo:/etc/ssh/demo_host_key.pub server_host_key.pub
El primer comando imprime una dirección como 127.0.0.1:32768. Usa su puerto real a
continuación. Al copiar la clave pública, se reemplaza cualquier server_host_key.pub
existente en este directorio de demostración. Cada contenedor nuevo tiene una clave de host nueva,
así que repite este paso cuando lo vuelvas a crear.
Sube y verifica un archivo
Guarda esto como transfer.ts. La ruta local y la ruta completa del archivo remoto
son argumentos de línea de comandos. El directorio padre remoto ya debe existir y permitir que tu
cuenta escriba y lea archivos.
import { readFile } from 'node:fs/promises'
import Client from 'ssh2-sftp-client'
let stage = 'configuration'
async function main(): Promise<void> {
const [localPath, remotePath] = process.argv.slice(2)
const { SFTP_HOST, SFTP_PORT, SFTP_USERNAME, SFTP_KEY_FILE, SFTP_HOST_SHA256 } = process.env
const port = Number(SFTP_PORT ?? '22')
if (
!localPath || !remotePath || !SFTP_HOST || !SFTP_USERNAME || !SFTP_KEY_FILE ||
!/^[a-f0-9]{64}$/.test(SFTP_HOST_SHA256 ?? '') ||
!Number.isInteger(port) || port < 1 || port > 65535
) {
throw new Error('Provide two paths, SFTP settings, and a verified SHA-256 hex fingerprint')
}
stage = 'reading local files'
const original = await readFile(localPath)
const privateKey = await readFile(SFTP_KEY_FILE)
const sftp = new Client()
try {
stage = 'connect'
await sftp.connect({
host: SFTP_HOST,
port,
username: SFTP_USERNAME,
privateKey,
hostHash: 'sha256',
hostVerifier: (fingerprint: string) => fingerprint === SFTP_HOST_SHA256,
readyTimeout: 10000,
})
stage = 'upload'
await sftp.put(original, remotePath)
stage = 'download verification'
// Version 12.1.1 can return an empty array for a zero-byte download.
const downloaded = Buffer.from(await sftp.get(remotePath))
if (!original.equals(downloaded)) {
throw new Error('Downloaded bytes differ from the uploaded bytes')
}
} finally {
await sftp.end()
}
console.log(`Verified ${original.length} bytes at ${remotePath}`)
}
main().catch(() => {
console.error(`SFTP transfer failed during ${stage}; check the settings and server logs`)
process.exitCode = 1
})
put() reemplaza un archivo remoto existente en la ruta elegida.
Usa un destino que puedas sobrescribir sin riesgo. Se trata de una escritura directa: una
transferencia interrumpida puede dejar un archivo truncado o parcial, y una verificación fallida no
revierte la escritura. La copia descargada permanece en memoria; el script no crea ni sobrescribe un
archivo de descarga local. Buffer.from() normaliza tanto el resultado de una descarga
vacía como los búferes binarios habituales, por lo que un archivo válido de cero bytes pasa la
verificación.
El paquete documenta put() y get().
Aquí se ejecutan secuencialmente en una sola conexión. El bloque finally llama a
end() tanto si la operación tiene éxito como si falla, y los fallos hacen que
el proceso termine con un código de salida distinto de cero. El valor
readyTimeout de la conexión limita el tiempo de negociación SSH, no el de toda la
transferencia; usa un plazo máximo para la tarea si la ejecutas sin supervisión.
Configura los datos de conexión en la segunda terminal. Sustituye 32768 por
el puerto que imprimió Docker. El comando decodifica el campo base64 de la clave pública de
confianza y calcula el hash de esos bytes, en lugar del hash del texto del archivo
.pub:
export SFTP_HOST=127.0.0.1 SFTP_PORT=32768 SFTP_USERNAME=demo SFTP_KEY_FILE=client_key
SFTP_HOST_SHA256=$(node --input-type=module -e '
import { createHash } from "node:crypto"
import { readFileSync } from "node:fs"
const [, key] = readFileSync("server_host_key.pub", "utf8").trim().split(/\s+/)
console.log(createHash("sha256").update(Buffer.from(key, "base64")).digest("hex"))
') &&
export SFTP_HOST_SHA256 &&
yarn node transfer.ts example.txt /home/demo/incoming/example.txt
Para el archivo proporcionado, una ejecución correcta imprime:
Verified 17 bytes at /home/demo/incoming/example.txt
Para transferir tu propio archivo pequeño, sustituye los dos argumentos de ruta y escribe entre comillas las rutas que contengan espacios. Una comparación correcta demuestra que el servidor devolvió los bytes que subiste en ese momento; no demuestra la durabilidad de una copia de seguridad ni que otro proceso haya consumido el archivo.
Diagnostica una transferencia fallida
| Etapa del fallo | Qué comprobar |
|---|---|
configuration | Proporciona ambas rutas, un puerto entero entre 1 y 65535 y la huella digital en formato hexadecimal. |
reading local files | Comprueba que el archivo de origen y la clave privada existan y se puedan leer. |
connect | Comprueba el puerto, la clave de host de confianza, el nombre de usuario y la clave de cliente autorizada. Un cambio de clave de host requiere verificación con el administrador. |
upload | Comprueba el directorio padre remoto y los permisos de escritura. El script no crea directorios. |
download verification | Comprueba los permisos de lectura y si otro proceso movió o modificó el archivo remoto. |
Una desconexión del servidor puede hacer que falle cualquiera de los dos pasos de transferencia.
Inspecciona la terminal del servidor antes de volver a intentarlo y comprueba si hay un archivo de
destino parcial. No elimines el verificador de host para eludir un error de conexión. Para un
servidor existente, usa las rutas tal como las ve esa cuenta SFTP; una cuenta restringida mediante
chroot puede ver /incoming/example.txt aunque su administrador vea una ruta más larga en el
sistema de archivos.
Detén el servidor local
Cuando termines, ejecuta esto en la segunda terminal:
docker stop node-sftp-demo
Como el servidor se inició con --rm, al detenerlo se eliminan el contenedor
y los archivos subidos. Tu proyecto local, el archivo de ejemplo y la clave de cliente desechable
permanecen en node-sftp-demo.
