Exporta archivos a Supabase con cURL
Para exportar un archivo local a Supabase Storage privado, envía sus bytes mediante un POST autenticado, luego descarga el objeto y compáralo con el original. Este tutorial utiliza dos usuarios comunes de Auth: uno sube y lee el archivo, y el otro comprueba que la política de acceso deniegue la misma descarga.
Configura un bucket privado y sus políticas
Necesitas un proyecto de Supabase que administres y dos usuarios distintos de Auth con email y contraseña que ya puedan iniciar sesión. Completa la confirmación del email si tu proyecto lo requiere; consulta Auth con contraseña. Crear una cuenta de Supabase no basta para crear estos usuarios de la aplicación.
Crea un bucket de Storage nuevo y privado llamado curl-exports, siguiendo la
guía rápida de buckets. Permite
application/octet-stream si restringes los tipos MIME y elige un archivo que respete su límite
de tamaño. Usa un bucket nuevo para este ejemplo. Revisa también las políticas existentes de
storage.objects: una regla permisiva que se aplique a este bucket también puede
conceder acceso. La comprobación con el segundo usuario que aparece más adelante pone a prueba
los permisos efectivos de lectura.
Ejecuta este SQL una vez como administrador del proyecto. Concede permisos de subida y lectura solo dentro de una carpeta cuyo nombre es el UUID del usuario que inició sesión:
create policy "curl_exports_insert"
on storage.objects for insert to authenticated
with check (
bucket_id = 'curl-exports'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "curl_exports_select"
on storage.objects for select to authenticated
using (
bucket_id = 'curl-exports'
and (storage.foldername(name))[1] = (select auth.uid())::text
and owner_id = (select auth.uid())::text
);
El control de acceso de Storage
explica las políticas de storage.objects. El procedimiento de subida que aparece a
continuación usa el token de acceso de un usuario; una clave secreta o de rol de servicio omitiría
RLS e invalidaría la comprobación de privacidad.
Comprueba las herramientas de tu shell y tus credenciales
Usa Bash en Linux con cURL, jq
y las utilidades habituales cmp, mktemp y
rm ya instaladas. Las versiones probadas son Bash 5.3.15, cURL 8.22.0 y
jq 1.8.2. Son versiones probadas, no versiones exactas fijadas como requisito; el script usa la
opción --fail-with-body de cURL y comprueba cada herramienta necesaria antes de crear
archivos temporales. Sus solicitudes ignoran la configuración de .curlrc.
Ejecuta los comandos de configuración en Bash.
curl --disable --version && jq --version && bash --version
Este tutorial usa una clave de API heredada anon existente, que coincide
con el stack local probado. Supabase prevé declarar en desuso las claves heredadas para finales de
2026; la guía de claves de API explica cómo obtenerlas y migrarlas.
Define la URL del proyecto, la clave y el bucket de ese stack en el shell desde el que ejecutarás
el script:
export SUPABASE_URL='https://YOUR_PROJECT_ID.supabase.co' \
SUPABASE_KEY='YOUR_ANON_KEY' \
SUPABASE_BUCKET='curl-exports'
Configura tu gestor de credenciales o ejecutor de tareas para que proporcione
SUPABASE_EMAIL, SUPABASE_PASSWORD,
SUPABASE_OTHER_EMAIL y SUPABASE_OTHER_PASSWORD como variables de entorno. El primer
par pertenece al usuario que sube el archivo; el segundo, al usuario al que se debe denegar el
acceso. Evita incluir contraseñas en el historial del shell. La clave del proyecto no sustituye
al JWT del usuario en el encabezado Authorization.
Este ejemplo se probó con Supabase Auth v2.196.0 y Storage v1.74.0 locales, con PostgreSQL 17.6. La creación de un proyecto alojado, TLS, la configuración del panel y las cuotas del plan son requisitos previos independientes. Usa HTTPS para un proyecto alojado; HTTP mediante la interfaz de loopback es adecuado para un stack de desarrollo local.
Sube, descarga y verifica un archivo
Guarda lo siguiente en un archivo nuevo llamado export-to-supabase.sh. Ejecútalo con Bash,
en lugar de cargarlo en tu shell actual. Su primer argumento es el archivo local; el segundo es
la ruta del objeto dentro de la carpeta del UUID del usuario que lo sube. Los segmentos de la
ruta del objeto deben empezar con una letra ASCII o un dígito y contener únicamente letras ASCII,
dígitos, espacios, puntos, guiones bajos y guiones. Los nombres de los archivos locales no tienen
que cumplir esta regla más restrictiva para los nombres de objetos.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
die() { printf '%s\n' "$1" >&2; exit 1; }
for tool in curl jq cmp mktemp rm; do
command -v "$tool" >/dev/null || die "Missing required tool: $tool"
done
[[ $# -eq 2 ]] || die 'Usage: bash export-to-supabase.sh LOCAL_FILE OBJECT_PATH'
: "${SUPABASE_URL:?Set SUPABASE_URL}" "${SUPABASE_KEY:?Set SUPABASE_KEY}"
: "${SUPABASE_BUCKET:?Set SUPABASE_BUCKET}"
: "${SUPABASE_EMAIL:?Set SUPABASE_EMAIL}" "${SUPABASE_PASSWORD:?Set SUPABASE_PASSWORD}"
: "${SUPABASE_OTHER_EMAIL:?Set SUPABASE_OTHER_EMAIL}"
: "${SUPABASE_OTHER_PASSWORD:?Set SUPABASE_OTHER_PASSWORD}"
file=$1
case "$file" in /*) ;; *) file="./$file" ;; esac
[[ -f "$file" && -r "$file" ]] || die 'Select a readable regular file'
object_path=$2
path_pattern='^[A-Za-z0-9][A-Za-z0-9._ -]*(/[A-Za-z0-9][A-Za-z0-9._ -]*)*$'
[[ "$object_path" =~ $path_pattern ]] || die 'Unsupported object path'
[[ "$SUPABASE_BUCKET" =~ ^[a-z0-9][a-z0-9-]*$ ]] || die 'Unsupported bucket name'
base_url=${SUPABASE_URL%/}
umask 077
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
sign_in() {
AUTH_EMAIL=$1 AUTH_PASSWORD=$2 jq -n \
'{email: env.AUTH_EMAIL, password: env.AUTH_PASSWORD}' |
curl --disable --fail-with-body --silent --show-error \
"$base_url/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_KEY" -H 'Content-Type: application/json' \
--data-binary @- --output "$3" || die 'Password sign-in failed'
jq -e '.user.role == "authenticated"' "$3" >/dev/null || die 'Expected an authenticated Auth user'
}
sign_in "$SUPABASE_EMAIL" "$SUPABASE_PASSWORD" "$work/owner.json"
sign_in "$SUPABASE_OTHER_EMAIL" "$SUPABASE_OTHER_PASSWORD" "$work/other.json"
token=$(jq -er '.access_token | select(type == "string" and length > 0)' "$work/owner.json")
other_token=$(jq -er '.access_token | select(type == "string" and length > 0)' "$work/other.json")
user_id=$(jq -er '.user.id | select(type == "string" and length > 0)' "$work/owner.json")
other_id=$(jq -er '.user.id | select(type == "string" and length > 0)' "$work/other.json")
[[ "$user_id" != "$other_id" ]] || die 'Use two distinct Auth users'
encoded_path=$(jq -rn --arg path "$user_id/$object_path" \
'$path | split("/") | map(@uri) | join("/")')
object_url="$base_url/storage/v1/object/$SUPABASE_BUCKET/$encoded_path"
download_url="$base_url/storage/v1/object/authenticated/$SUPABASE_BUCKET/$encoded_path"
curl --disable --fail-with-body --silent --show-error --request POST "$object_url" \
-H "apikey: $SUPABASE_KEY" -H "Authorization: Bearer $token" \
-H 'Content-Type: application/octet-stream' \
--data-binary "@$file" --output "$work/upload.json" || die 'File upload failed'
curl --disable --fail-with-body --silent --show-error "$download_url" \
-H "apikey: $SUPABASE_KEY" -H "Authorization: Bearer $token" \
--output "$work/download.bin" || die 'Owner download failed'
cmp -s -- "$file" "$work/download.bin" || die 'Downloaded bytes differ from the selected file'
denied_http=$(curl --disable --silent --show-error "$download_url" \
-H "apikey: $SUPABASE_KEY" -H "Authorization: Bearer $other_token" \
--output "$work/denied.json" --write-out '%{http_code}') || die 'Access-check request failed'
case "$denied_http" in
400)
jq -e '(.statusCode | tostring) == "403" or (.statusCode | tostring) == "404"' \
"$work/denied.json" >/dev/null || die 'Unexpected Storage error'
;;
403|404) ;;
*) die "Expected access denial; received HTTP $denied_http" ;;
esac
printf 'Verified upload and private access.\n'
Sustituye ./report.bin por el archivo que hayas elegido. No lo modifiques mientras
se ejecuta el script. El nombre del archivo remoto puede diferir de su nombre local:
bash export-to-supabase.sh './report.bin' 'exports/report.bin'
Una ejecución correcta imprime:
Verified upload and private access.
El objeto permanece en curl-exports/<uploader-uuid>/exports/report.bin. La copia descargada y las respuestas de Auth
son temporales y se eliminan cuando el script finaliza. Si hay una discrepancia o el otro usuario
logra descargar el archivo de forma inesperada, el script finaliza con un código distinto de cero
sin imprimir este mensaje de éxito. El
endpoint de descarga privada
usa el JWT del usuario y la política de lectura; la clave de API por sí sola no concede ese acceso.
Gestiona los objetos existentes sin perder exportaciones anteriores
El POST crea un objeto nuevo. Volver a ejecutar el mismo comando apunta al mismo UUID y a la misma ruta de objeto, por lo que se rechaza la operación si el objeto ya existe. El objeto anterior permanece almacenado. Usa otra ruta de objeto para una nueva exportación.
La guía de subida estándar de Supabase
documenta la sustitución intencional con x-upsert: true, que también requiere
políticas de SELECT y UPDATE. Este tutorial no concede
permisos de actualización ni sobrescribe objetos. Si una conexión falla después de que el
servidor haya almacenado el archivo, el resultado de la subida es incierto. Inspecciona y descarga
ese objeto exacto antes de decidir si conservarlo o usar una ruta nueva; una respuesta de objeto
duplicado al reintentar puede significar que la primera subida se completó correctamente.
Comprende los límites de tamaño de archivo
Elige un archivo pequeño que respete tanto el límite global del proyecto como el del bucket.
Los límites máximos de los planes alojados y las restricciones de los buckets se describen en la
documentación de límites de archivo de Supabase. La opción
--data-binary @file de cURL
almacena la entrada en un búfer en memoria,
y este ejemplo no reanuda una transferencia interrumpida. Para archivos más grandes, sigue la
guía de subidas reanudables, que es independiente de este tutorial.
Automatiza la exportación de archivos en un flujo de trabajo diario
El script guardado inicia sesión en cada ejecución, por lo que la automatización no depende de que un JWT pegado siga siendo válido. Configura tu programador de tareas para que proporcione las variables de entorno, usa rutas absolutas para el script y el archivo de entrada, y elige una ruta de objeto nueva para cada exportación. Conserva la segunda cuenta para comprobar los permisos y considera un código de salida distinto de cero como una verificación fallida, incluso si la subida ya existe en el servidor remoto.
Soluciona problemas comunes
Errores de autenticación (401, no autorizado)
Comprueba primero la URL del proyecto y la clave de API. Si falla el inicio de sesión con contraseña, comprueba el email, la contraseña y el estado de confirmación del usuario. El script se detiene si falla el inicio de sesión; no utiliza una clave de administrador en su lugar. Si el token caduca durante una transferencia, vuelve a ejecutar el script con una ruta de objeto nueva tras comprobar si la subida anterior se guardó.
Errores de archivos duplicados
Una ruta duplicada es un error, habitualmente HTTP 400 en Storage. Cambia el segundo argumento del script para una nueva exportación. No añadas reintentos automáticos ni una operación upsert solo para ocultar la colisión.
Límites de tamaño de archivo (413, carga útil demasiado grande)
Una subida demasiado grande puede producir HTTP 413. La versión de Storage probada devolvió
HTTP 400 con el estado 413 en el cuerpo de la respuesta al rechazar una subida por el límite de
tamaño del bucket. Comprueba ambos límites de tamaño y revisa la lista de tipos MIME permitidos
del bucket si el rechazo se debe al tipo de archivo. Este script envía
application/octet-stream; no detecta el tipo de medio del archivo. Consulta los
códigos de error de Storage
para conocer los diagnósticos del servicio.
Permiso denegado
Comprueba que el bucket sea privado y que las políticas se apliquen a su nombre exacto, a la
carpeta del UUID del usuario que sube el archivo y a owner_id. Un objeto
privado inaccesible puede parecer inexistente: la versión de Storage probada devolvió HTTP 400
con el estado 404 en el cuerpo de la respuesta para el segundo usuario. El script primero lee y
compara el mismo objeto como su propietario, por lo que un objeto inexistente no puede superar
esa comprobación. Si el otro usuario recibe HTTP 200, inspecciona las políticas existentes y la
visibilidad del bucket antes de exportar datos sensibles.
