Exportar archivos a buckets de Supabase con Ruby
Para exportar un archivo a Supabase desde Ruby, envía sus bytes a la API REST de Storage, descarga el objeto seleccionado y compáralo con el archivo de entrada. Este ejemplo inicia sesión como un usuario común, rechaza las rutas de objetos existentes y deja una subida verificada en el bucket. La eliminación es un comando separado.
Requisitos previos
- Ruby 4.0.7, Bundler, un compilador y
makepara instalar la gema JSON. La página de descargas de Ruby enumera las versiones actuales y las opciones de instalación. La configuración de Linux se ha probado con Bundler 4.0.20 y también con Ruby 3.3.12 y Bundler 2.5.22. - Bash para los comandos de shell siguientes. Comienza en un directorio con permiso de escritura,
sin un subdirectorio
supabase-storage-demoexistente y sin variables exportadasBUNDLE_*,GEM_HOMEoGEM_PATHque modifiquen la configuración. - Un proyecto de Supabase, un usuario de Auth con email y contraseña y el email confirmado, y un
bucket privado de Storage dedicado llamado
ruby-exports. Sigue la guía oficial de creación de buckets para crearlo; permite el tipo de contenidoapplication/octet-streamy archivos de hasta 1 MiB. - La URL y la clave del proyecto, y el email y la contraseña del usuario, proporcionados como variables de entorno.
Para proyectos alojados, usa una clave publicable del proyecto.
Debe ir en el encabezado apikey; el JWT del usuario que ha iniciado sesión debe ir en Authorization: Bearer.
Supabase anuncia que las claves heredadas anon y service_role quedarán obsoletas a finales de 2026.
Los proyectos existentes pueden usar ambos sistemas de claves durante la migración. La verificación
nativa de este ejemplo usa un proyecto local autoalojado con una clave heredada
anon, no un proyecto alojado con una clave publicable. Aquí no se han probado
el aprovisionamiento de claves para proyectos alojados ni TLS.
Ejecuta el siguiente SQL como administrador del proyecto para ese bucket dedicado. Estas
políticas de Storage permiten subir archivos bajo el UUID del usuario
y que ese usuario lea y elimine sus propios objetos. La subida requiere INSERT;
la lectura de verificación requiere SELECT. La eliminación también requiere
SELECT y DELETE, como se documenta en la
referencia de eliminación de Storage.
Comprueba primero si existen políticas más amplias: las políticas permisivas se combinan, por lo
que estas reglas no restringen el acceso ya concedido por otra política.
create policy ruby_exports_insert on storage.objects
for insert to authenticated with check (
bucket_id = 'ruby-exports'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy ruby_exports_select on storage.objects
for select to authenticated using (
bucket_id = 'ruby-exports'
and (storage.foldername(name))[1] = (select auth.uid())::text
and owner_id = (select auth.uid())::text
);
create policy ruby_exports_delete on storage.objects
for delete to authenticated using (
bucket_id = 'ruby-exports'
and (storage.foldername(name))[1] = (select auth.uid())::text
and owner_id = (select auth.uid())::text
);
Proporciona SUPABASE_URL, SUPABASE_PROJECT_KEY, SUPABASE_EMAIL y SUPABASE_PASSWORD mediante
tu gestor de secretos o el entorno protegido del proceso. Usa la URL raíz del proyecto, como
https://PROJECT_REF.supabase.co. No incluyas contraseñas, tokens de usuario ni claves administrativas en
el código fuente o el historial del shell. El programa obtiene una nueva sesión de usuario mediante
la concesión de contraseña de Auth cada vez que se ejecuta; no necesita
una clave secreta ni una clave service_role.
Configura tu entorno
Pega este bloque en Bash. El subshell mantiene intactos tu directorio actual y las opciones del shell, incluso si falla la configuración. Si el directorio del proyecto ya existe, se conserva.
(
command -v ruby >/dev/null &&
command -v bundle >/dev/null &&
mkdir supabase-storage-demo &&
cd supabase-storage-demo &&
bundle init &&
bundle config set --local path vendor/bundle
)
Reemplaza el supabase-storage-demo/Gemfile recién creado por:
source 'https://rubygems.org'
gem 'json', '3.0.2'
gem 'net-http', '0.10.0'
Las versiones fijadas corresponden a las versiones actuales de JSON y
Net::HTTP comprobadas el 12 de octubre de 2026.
Instálalas en el vendor/bundle de este proyecto, sin instalar gemas en todo el sistema:
(cd supabase-storage-demo && bundle install)
Si la instalación falla, resuelve el problema de red o del requisito del compilador indicado y vuelve a ejecutar ese bloque de instalación en el mismo proyecto. Conserva su Gemfile y la configuración local de Bundler; no repitas el bloque de creación del directorio ni elimines un proyecto existente para volver a intentarlo.
Configura un cliente de REST API en Ruby
Guarda este programa completo como supabase-storage-demo/storage.rb. Procesa un archivo a la vez,
incluidos archivos vacíos y binarios, con un límite de aplicación de 1 MiB. Los nombres de archivos
locales deben contener bytes UTF-8 válidos sin caracteres de control; la validación no depende de
la configuración regional del shell. Las claves de objetos usan letras ASCII, dígitos, espacios,
_, -, . y /, sin segmentos de carpeta vacíos,
. ni ... Elige una clave admitida independientemente del nombre del archivo local:
la versión de Storage probada rechaza Unicode y los signos de porcentaje en las claves.
El programa codifica cada segmento de la clave admitida para la URL.
require 'digest'
require 'json'
require 'net/http'
require 'uri'
class StorageError < StandardError
end
class SupabaseStorage
MAX_BYTES = 1024 * 1024
attr_reader :user_id
def initialize
@url = ENV.fetch('SUPABASE_URL').chomp('/')
origin = URI(@url)
secure = origin.is_a?(URI::HTTPS)
local = origin.is_a?(URI::HTTP) && %w[localhost 127.0.0.1 [::1]].include?(origin.host)
unless (secure || local) && origin.userinfo.nil? && origin.query.nil? &&
origin.fragment.nil? && origin.path.empty?
raise StorageError, 'Use an HTTPS project root URL, or HTTP on localhost for local testing'
end
@project_key = ENV.fetch('SUPABASE_PROJECT_KEY')
login = Net::HTTP::Post.new('/auth/v1/token?grant_type=password')
login['Content-Type'] = 'application/json'
login.body = JSON.generate(email: ENV.fetch('SUPABASE_EMAIL'),
password: ENV.fetch('SUPABASE_PASSWORD'))
session = JSON.parse(request(login).body)
@token = session.fetch('access_token')
user = JSON.parse(request(Net::HTTP::Get.new('/auth/v1/user')).body)
@user_id = user.fetch('id')
end
def upload(bucket, key, path)
bytes = input_bytes(path)
upload = Net::HTTP::Post.new(object_path(bucket, key))
upload['Content-Type'] = 'application/octet-stream'
upload['x-upsert'] = 'false'
upload.body = bytes
request(upload)
verify_bytes(bucket, key, bytes)
end
def verify(bucket, key, path)
verify_bytes(bucket, key, input_bytes(path))
end
def delete(bucket, key)
key = text(key)
object_path(bucket, key)
deletion = Net::HTTP::Delete.new("/storage/v1/object/#{segment(bucket)}")
deletion['Content-Type'] = 'application/json'
deletion.body = JSON.generate(prefixes: [key])
deleted = JSON.parse(request(deletion).body)
unless deleted.is_a?(Array) && deleted.any? { |object| object['name'] == key }
raise StorageError, 'Deletion was not confirmed; the object may be missing or inaccessible'
end
puts 'Deletion confirmed'
end
private
def request(message)
message['apikey'] = @project_key
message['Authorization'] = "Bearer #{@token}" if @token
uri = URI(@url)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
open_timeout: 10, read_timeout: 30, write_timeout: 30) do |http|
http.max_retries = 0
response = http.request(message)
unless response.is_a?(Net::HTTPSuccess)
raise StorageError, "Request failed (HTTP #{response.code})"
end
response
end
end
def text(value)
value = value.dup.force_encoding(Encoding::UTF_8)
unless value.valid_encoding? && !value.match?(/[\x00-\x1f\x7f]/)
raise StorageError, 'Paths must be valid UTF-8 without control characters'
end
value
end
def segment(value)
URI.encode_www_form_component(text(value)).gsub('+', '%20')
end
def object_path(bucket, key)
raise StorageError, 'Invalid bucket name' unless bucket.match?(/\A[a-z0-9][a-z0-9-]*\z/)
key = text(key)
unless key.match?(/\A[A-Za-z0-9_.\/ -]+\z/)
raise StorageError, 'Use an ASCII object key with letters, digits, spaces, _, -, ., and /'
end
parts = key.split('/', -1)
if parts.any? { |part| ['', '.', '..'].include?(part) } || key.include?('\\')
raise StorageError, 'Use a nonempty object key with ordinary folder segments'
end
"/storage/v1/object/#{segment(bucket)}/#{parts.map { |part| segment(part) }.join('/')}"
end
def input_bytes(path)
path = text(path)
raise StorageError, 'Input must be a regular file' unless File.file?(path)
bytes = File.binread(path, MAX_BYTES + 1) || ''.b
raise StorageError, 'Input exceeds the 1 MiB application limit' if bytes.bytesize > MAX_BYTES
bytes
end
def verify_bytes(bucket, key, expected)
download = Net::HTTP::Get.new(object_path(bucket, key))
download['Accept-Encoding'] = 'identity'
actual = request(download).body || ''.b
unless actual.b == expected.b
raise StorageError, 'Stored bytes differ from the input; inspect the object before retrying'
end
puts "Verified #{expected.bytesize} bytes; SHA-256 #{Digest::SHA256.hexdigest(actual)}"
end
end
begin
command, bucket, key, path = ARGV
valid = (command == 'whoami' && ARGV.length == 1) ||
(%w[upload verify].include?(command) && ARGV.length == 4) ||
(command == 'delete' && ARGV.length == 3)
unless valid
raise StorageError, 'Usage: storage.rb whoami | upload/verify BUCKET KEY FILE | delete BUCKET KEY'
end
storage = SupabaseStorage.new
case command
when 'whoami' then puts storage.user_id
when 'upload' then storage.upload(bucket, key, path)
when 'verify' then storage.verify(bucket, key, path)
when 'delete' then storage.delete(bucket, key)
end
rescue StandardError => error
# Only our deliberate messages are safe to print; library errors can contain private data.
safe = error.instance_of?(StorageError) ? error.message : 'Check configuration, input, and connectivity'
warn "Storage command failed: #{safe}"
exit 1
end
La subida envía bytes sin procesar, no JSON ni campos multipart. La lectura de verificación usa el endpoint autenticado de objetos, con los permisos del mismo usuario. Cada operación de red tiene un tiempo de espera de conexión de 10 segundos y tiempos de espera por inactividad de lectura y escritura de 30 segundos, no un plazo para la transferencia completa. Los reintentos automáticos están desactivados: una respuesta perdida puede dejar un objeto aunque el comando indique un fallo.
Ejemplos de uso
Comprueba las credenciales proporcionadas y obtén el UUID del usuario:
(cd supabase-storage-demo && bundle exec ruby storage.rb whoami)
Coloca un archivo llamado report.bin dentro del proyecto. Reemplaza
USER_UUID a continuación por el UUID impreso; debe ser la primera carpeta de la clave del objeto
para cumplir las políticas anteriores.
(cd supabase-storage-demo &&
bundle exec ruby storage.rb upload ruby-exports 'USER_UUID/exports/report.bin' ./report.bin)
Un comando exitoso imprime la cantidad de bytes y el SHA-256 del objeto descargado y termina con código de salida cero. El objeto permanece en la clave seleccionada. Para comprobarlo de nuevo sin subirlo ni reemplazarlo:
(cd supabase-storage-demo &&
bundle exec ruby storage.rb verify ruby-exports 'USER_UUID/exports/report.bin' ./report.bin)
Mantén el archivo de origen sin cambios mientras lo comparas. Pon entre comillas los argumentos de
ruta que contengan espacios y usa ./ para un nombre de archivo local que comience con un guion.
Este ejemplo no enumera carpetas ni crea enlaces de descarga públicos.
Manejo de errores y validación de archivos
Toda subida rechazada, lectura de verificación fallida o eliminación no confirmada termina con un código de salida distinto de cero. Las subidas estándar rechazan las claves existentes; el programa no solicita upsert ni reemplaza los bytes anteriores. Ejecuta los comandos de forma secuencial.
Un fallo HTTP no identifica por sí solo la causa. Comprueba la URL del proyecto, las credenciales del usuario, las restricciones del bucket, el prefijo UUID de la clave y las políticas aplicables. Un objeto ausente o inaccesible puede producir la misma respuesta. El programa omite los cuerpos de respuesta sin procesar del servidor, las credenciales y los errores de las bibliotecas en su mensaje de diagnóstico.
El límite de 1 MiB es inclusivo y permite cero bytes. Es el límite de este ejemplo; tu proyecto y
bucket pueden imponer restricciones adicionales. El encabezado MIME declara contenido binario
genérico y no reconoce formatos de archivo ni determina la seguridad del contenido. Si la subida
tiene éxito pero la verificación falla, inspecciona esa clave con el comando
verify antes de volver a intentarlo: la subida no se revierte ni se elimina silenciosamente.
Buenas prácticas
Para exportaciones periódicas de informes, elige una clave nueva para cada informe, por ejemplo
USER_UUID/exports/2026/10/report-12.bin. Una carpeta con la fecha mantiene organizadas las exportaciones; un
identificador adicional de tarea distingue varios informes del mismo día. Conserva la clave junto
al registro de la tarea para que la verificación y la eliminación posteriores se dirijan al mismo
objeto.
Manejo de archivos grandes
Este ejemplo para archivos pequeños almacena en memoria tanto la entrada como la lectura de verificación y no puede reanudar subidas interrumpidas. Para transferencias más grandes, usa el protocolo de subida reanudable de Supabase con un cliente compatible con el protocolo tus. Los fragmentos independientes enviados al endpoint estándar de objetos no implementan ese protocolo. Las transferencias reanudables quedan fuera del alcance verificado de este ejemplo.
Elimina una exportación explícitamente
Cuando el archivo exportado ya no sea necesario, elimina únicamente su clave seleccionada:
(cd supabase-storage-demo &&
bundle exec ruby storage.rb delete ruby-exports 'USER_UUID/exports/report.bin')
Para que la operación tenga éxito, la respuesta de eliminación debe identificar ese objeto. Un array vacío puede significar que no existe o que las políticas lo ocultan, por lo que el comando indica un fallo en ese caso. Eliminar una exportación no elimina su bucket, otros objetos, usuarios ni políticas.
