Subidas CLI eficientes de archivos con software de código abierto
Que un comando de subida devuelva una URL no indica si el archivo descargado coincide con el original.
Este tutorial usa curl y un servidor local temporal
transfer.sh para subir un archivo no vacío, descargarlo y comparar los bytes.
Al terminar, tendrás una copia verificada en disco y una suma de comprobación SHA-256, sin necesidad
de una cuenta en la nube.
Compara herramientas CLI populares para subir archivos
Elige la herramienta según el sistema receptor. curl proporciona el cliente
HTTP; transfer.sh proporciona el servidor del siguiente ejemplo.
| Herramienta | Uso | Qué revisar antes de elegir |
|---|---|---|
s3cmd | Subidas y sincronización con S3 o almacenamiento de objetos compatible | Necesitas un bucket y credenciales con permiso para las operaciones previstas. |
rclone | Copia y sincronización entre backends de almacenamiento en la nube | Las sumas de comprobación y otras capacidades varían según el backend. |
curl | Subidas HTTP a un endpoint existente | El endpoint determina el método, la autenticación y el formato de respuesta. |
rsync | Sincronización de archivos con una máquina que controlas | Una transferencia mediante shell remoto requiere rsync en ambas máquinas. |
lftp | Transferencias FTP o SFTP y creación de réplicas | Elige un protocolo y una cuenta que el destino admita. |
Para un flujo de trabajo respaldado por almacenamiento, consulta exportaciones por lotes a S3 con URL PUT firmadas o rclone con DigitalOcean Spaces. Esas tareas requieren configurar el proveedor. Aquí, el servidor receptor se ejecuta en tu propia máquina y se elimina después del ciclo de subida y descarga.
Usa transfer.sh para compartir archivos rápidamente
Esta es una prueba HTTP anónima sobre la interfaz de bucle local (loopback), con un daemon local de Docker en Linux. La URL es accesible desde esa máquina mientras el contenedor está en ejecución; no es un enlace público para compartir. Usa un archivo de prueba sin datos sensibles que no cambie. Este ejemplo no incluye almacenamiento persistente en el servidor, autenticación, reanudación ni una prueba de rendimiento de velocidad.
La versión fijada rechaza las subidas de cero bytes. El siguiente script las rechaza antes de crear un directorio de resultados o un contenedor. Si los archivos vacíos forman parte de tu tarea de transferencia, elige un destino que los admita.
Los comandos se probaron en Linux x86-64 con Bash 5.3.15, curl 8.22.0, Docker Engine 29.7.2,
GNU coreutils 9.11 y GNU diffutils 3.12. Necesitas docker,
bash, curl, cmp y
sha256sum en tu ruta de búsqueda de ejecutables, además de acceso al daemon local
de Docker. Un contexto remoto de Docker colocaría el servidor en otra máquina. Docker documenta que
las versiones anteriores a 28.0.0 pueden exponer los puertos publicados en localhost a equipos del mismo segmento de red.
Descarga la imagen exacta que se usa a continuación:
docker pull dutchcoders/transfer.sh@sha256:9383e66489ab3a7a56bec1b67d2e27d41c072102d515cdc5ab35f913b72e8a09
Este digest fija transfer.sh v1.6.1,
publicada el 4 de diciembre de 2023. Considéralo un ejercicio local reproducible, no una recomendación
para desplegar esa imagen de forma pública. Las
instrucciones de Docker del proyecto
explican por qué es preferible una imagen con versión a la etiqueta cambiante
latest.
Ejecuta tu propia instancia de transfer.sh
Guarda este código como upload-check.sh en un directorio de pruebas. Ejecútalo con Bash,
en lugar de cargarlo en tu shell mediante source. Crea un directorio de resultados nuevo, asigna
automáticamente un puerto del host, espera a que su propio contenedor esté listo y lo elimina antes
de informar que la operación se completó correctamente.
El almacenamiento local y los archivos temporales de subida comparten un montaje de 64 MiB
respaldado por memoria. Deja espacio para metadatos y archivos temporales; este es un ejercicio con
archivos pequeños, no un servicio para archivos grandes.
El servidor recibe un nombre de archivo fijo, payload.bin, para que los espacios,
los guiones iniciales y los signos de puntuación de URL de tu nombre de archivo local no se
conviertan en sintaxis de URL.
#!/usr/bin/env bash
set -euo pipefail
umask 077
fail() { printf '%s\n' "$*" >&2; exit 1; }
[[ $# -eq 2 ]] || fail 'Usage: bash upload-check.sh INPUT NEW_RESULT_DIRECTORY'
input=$1
result_dir=$2
# Prefix relative paths so a literal "-" is a file, not curl's stdin selector.
[[ $input == /* || $input == ./* || $input == ../* ]] || input=./$input
[[ $result_dir == /* || $result_dir == ./* || $result_dir == ../* ]] || result_dir=./$result_dir
[[ -f $input && -r $input ]] || fail "Input is not a readable regular file: $input"
[[ -s $input ]] || fail "transfer.sh v1.6.1 rejects empty uploads: $input"
port=${UPLOAD_PORT:-0}
[[ $port =~ ^[0-9]{1,5}$ ]] || fail 'UPLOAD_PORT must be 0 or a port from 1 to 65535.'
(( 10#$port <= 65535 )) || fail 'UPLOAD_PORT exceeds 65535.'
port=$((10#$port))
name=${UPLOAD_NAME:-cli-upload-${RANDOM}-${RANDOM}-$$}
image=dutchcoders/transfer.sh@sha256:9383e66489ab3a7a56bec1b67d2e27d41c072102d515cdc5ab35f913b72e8a09
# An existing result directory is never reused or removed.
mkdir -- "$result_dir" || fail "Choose a new result directory: $result_dir"
work=$result_dir/.work
mkdir -- "$work"
verified=0
checksum=''
cleanup() {
status=$?
trap - EXIT
# A CID file identifies only the container this invocation created.
if [[ -s $work/container.id ]]; then
container_id=$(< "$work/container.id")
if ! docker rm --force "$container_id" >/dev/null; then
printf 'Cleanup failed; remove container %s when Docker is available.\n' "$container_id" >&2
status=1
fi
fi
if (( status == 0 && verified == 1 )); then
mv -- "$work/download.part" "$result_dir/download.bin" || status=1
fi
rm -rf -- "$work" || status=1
if (( status == 0 && verified == 1 )); then
printf 'Verified: %s\nSHA-256: %s\n' "$result_dir/download.bin" "$checksum"
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
docker create --cidfile "$work/container.id" --name "$name" --pull=never \
--publish "127.0.0.1:$port:8080" \
--read-only --user 5000:5000 --cap-drop ALL --security-opt no-new-privileges \
--tmpfs /tmp:rw,size=64m,mode=1777 --memory 128m --cpus 1 --pids-limit 64 \
"$image" --provider local --basedir /tmp/data --temp-path /tmp \
> "$work/create.log"
container_id=$(< "$work/container.id")
docker start "$container_id" >/dev/null
binding=$(docker port "$container_id" 8080/tcp)
[[ $binding == 127.0.0.1:* ]] || fail 'Expected a loopback port binding.'
origin=http://$binding
curl_local() {
curl --disable --noproxy '*' --globoff --fail --silent --show-error \
--connect-timeout 2 --max-time 30 "$@"
}
ready=0
for ((attempt=0; attempt<40; attempt++)); do
if [[ $(docker inspect --format '{{.State.Running}}' "$container_id") != true ]]; then
docker logs "$container_id" >&2
fail 'Server exited before readiness.'
fi
if [[ $(curl_local --max-time 1 --output /dev/null --write-out '%{http_code}' \
"$origin/health.html" 2>/dev/null) == 200 ]]; then
ready=1
break
fi
sleep 0.25
done
if (( ready == 0 )); then
docker logs "$container_id" >&2
fail 'Server did not become ready.'
fi
if ! upload_status=$(curl_local --upload-file "$input" --output "$work/url.txt" \
--write-out '%{http_code}' "$origin/payload.bin"); then
docker logs "$container_id" >&2
fail "Upload failed: $input"
fi
[[ $upload_status == 200 ]] || fail "Unexpected upload HTTP status: $upload_status"
url=$(< "$work/url.txt")
path=${url#"$origin/"}
[[ $url == "$origin/"* && $path =~ ^[A-Za-z0-9]+/payload\.bin$ ]] \
|| fail 'Server returned an unexpected download URL.'
if ! download_status=$(curl_local --output "$work/download.part" \
--write-out '%{http_code}' "$url"); then
fail 'Download failed.'
fi
[[ $download_status == 200 ]] || fail "Unexpected download HTTP status: $download_status"
cmp -- "$input" "$work/download.part" || fail "Downloaded bytes differ: $input"
checksum=$(sha256sum < "$work/download.part")
checksum=${checksum%% *}
verified=1
--upload-file hace que curl envíe una solicitud
HTTP PUT.
--fail hace que los errores HTTP provoquen
el fallo del comando, y el script también exige HTTP 200 en cada etapa.
--disable es la primera opción de curl para que
un archivo de configuración personal no pueda añadir redirecciones ni cambiar la solicitud.
--globoff impide que curl expanda los corchetes y las llaves de los nombres de
archivos locales. La URL devuelta por el servidor debe apuntar al origen exacto que Docker asignó.
Por último, cmp comprueba cada byte descargado antes de que el script
conserve download.bin.
El script realiza la limpieza al finalizar normalmente, al pulsar Ctrl+C y al recibir las señales
TERM o HUP. Una señal enviada solo a Bash puede tener que esperar a que termine el comando activo;
cada transferencia de curl tiene un límite de 30 segundos. Elimina los contenedores por el ID
asignado al crearlos, de modo que una colisión de nombres no pueda eliminar el contenedor de otra
persona. SIGKILL, un fallo de la máquina o la pérdida del daemon de Docker pueden impedir la
limpieza. UPLOAD_NAME te permite elegir un nombre reconocible para una ejecución;
UPLOAD_PORT te permite solicitar un puerto específico del host en lugar de la
asignación automática predeterminada. Ninguna de las dos opciones permite reutilizar un contenedor
existente o un puerto ocupado.
Comparte un archivo mediante tu instancia
Crea un pequeño archivo binario de prueba y ejecuta el script guardado. Los paréntesis limitan el
alcance de las opciones del shell; noclobber se niega a sobrescribir un
sample.bin existente. Usa un directorio donde tanto ese nombre de archivo como
upload-result estén sin usar.
(
set -euo pipefail
set -o noclobber
printf '\000\377\001\200\012\015\052\000\101\102' > ./sample.bin
bash ./upload-check.sh ./sample.bin ./upload-result
)
La salida de una ejecución correcta para esos diez bytes es:
Verified: ./upload-result/download.bin
SHA-256: d77823e7a78045d088fa69d1572861efee50fa35240f7555fa860d02d22633d5
Abre upload-result/download.bin o compáralo con sample.bin; el archivo permanece
después de eliminar el servidor. Para subir tu propio archivo, reemplaza
./sample.bin en la invocación de Bash y elige un directorio de resultados nuevo.
Mantén el archivo de entrada sin cambios hasta que termine el comando.
Un fallo devuelve un código distinto de cero, elimina las descargas temporales y deja el directorio
de resultados recién creado sin una copia verificada. Se rechaza una nueva ejecución con ese mismo
directorio de resultados, aunque esté vacío.
Protege tus subidas mediante CLI
La interfaz de bucle local limita este ejemplo al host local; otros procesos y usuarios de ese host aún pueden acceder a su endpoint anónimo. Los archivos almacenados en el contenedor desaparecen cuando se elimina, pero el archivo de entrada original y la descarga verificada permanecen en disco. Aquí se usa HTTP sin cifrar como opción para pruebas locales. Para un servicio accesible desde otras máquinas, elige HTTPS con autenticación y una política de retención antes de enviar datos reales. Una URL imposible de adivinar no autentica a la persona que descarga el archivo.
Resuelve problemas comunes
| Síntoma | Qué revisar |
|---|---|
| Docker no puede crear o iniciar el contenedor | Confirma que se haya descargado la imagen fijada y que el daemon local sea accesible. Un UPLOAD_PORT ocupado o un UPLOAD_NAME existente impiden el inicio; elige otro valor. |
| El servidor termina o nunca llega a estar listo | Lee el diagnóstico del contenedor que se imprime antes de la limpieza. El script no sube el archivo hasta que su propio servidor supera la comprobación de estado. |
| La subida o la descarga falla | Lee el error de curl y el mensaje de la etapa. El montaje temporal de 64 MiB del servidor puede llenarse; prueba con un archivo más pequeño. Las transferencias tienen un tiempo límite de 30 segundos. |
| Los bytes descargados difieren | Comprueba si el archivo de entrada cambió durante la transferencia. Una solicitud HTTP correcta por sí sola no basta; el script descarta esta descarga. |
| El directorio de resultados ya existe | Elige un directorio nuevo. El script conserva el resultado anterior en lugar de reemplazarlo. |
| Necesitas límites de ancho de banda para una tarea en la nube | Usa --limit-rate con s3cmd, o --bwlimit con rclone; estas opciones pertenecen a herramientas distintas. |
Cuando uses esta comprobación en una automatización, básate en su estado de salida y asigna a cada ejecución un directorio de resultados nuevo. Programarla no convierte el contenedor temporal en una copia de seguridad persistente ni en un servicio público de archivos.
Continúa con las subidas desde el navegador
Para una interfaz de subida desde el navegador, Uppy ofrece selección de archivos e información sobre el progreso de subida. El protocolo tus permite reanudar las subidas HTTP cuando el servidor lo admite. Son elecciones independientes para el cliente y el servidor; este ejemplo de PUT con transfer.sh no implementa el protocolo tus. Usa el ciclo local de subida y descarga para comprobar tu flujo de trabajo con CLI y luego elige el servicio receptor según los requisitos de almacenamiento y acceso de tu aplicación.
