Exportar arquivos para buckets do Supabase em Ruby
Para exportar um arquivo para o Supabase usando Ruby, envie seus bytes à REST API do Storage, depois baixe o objeto selecionado e compare-o com a entrada. Este exemplo faz login como um usuário comum, recusa caminhos de objetos já existentes e deixa um upload verificado no bucket. A exclusão é um comando separado.
Pré-requisitos
- Ruby 4.0.7, Bundler, um compilador e
makepara instalar a gem JSON. A página de downloads do Ruby lista as versões atuais e as opções de instalação. A configuração no Linux foi testada com Bundler 4.0.20 e também com Ruby 3.3.12 e Bundler 2.5.22. - Bash para os comandos de shell abaixo. Comece em um diretório com permissão de gravação, sem
um subdiretório
supabase-storage-demoexistente e sem sobrescritas exportadas paraBUNDLE_*,GEM_HOMEouGEM_PATH. - Um projeto Supabase, um usuário confirmado do Auth com e-mail e senha e um bucket privado e
dedicado do Storage chamado
ruby-exports. Siga o guia oficial de criação de buckets para criá-lo; permita o tipo de conteúdoapplication/octet-streame arquivos de até 1 MiB. - A URL e a chave do projeto, além do e-mail e da senha do usuário, fornecidos como variáveis de ambiente.
Para projetos hospedados, use uma chave publicável do projeto.
Ela deve ficar no cabeçalho apikey; o JWT do usuário autenticado deve ficar em
Authorization: Bearer.
A Supabase anuncia a depreciação das chaves legadas anon e service_role até o fim de 2026.
Projetos existentes podem usar os dois sistemas de chaves durante a migração. A verificação nativa
deste exemplo usa um projeto local com hospedagem própria e uma chave legada
anon, não um projeto hospedado com chave publicável. O provisionamento de
chaves para projetos hospedados e o TLS não foram testados aqui.
Execute o SQL a seguir como administrador do projeto para esse bucket dedicado. Estas
políticas do Storage permitem uploads sob o UUID do usuário e
permitem que ele leia e exclua seus próprios objetos. O upload exige INSERT;
a leitura do objeto armazenado exige SELECT. A exclusão também exige
SELECT e DELETE, conforme documentado na
referência de remoção do Storage.
Primeiro, verifique se já existem políticas mais abrangentes: políticas permissivas se combinam,
portanto, essas regras não restringem o acesso já concedido por outra 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
);
Forneça SUPABASE_URL, SUPABASE_PROJECT_KEY,
SUPABASE_EMAIL e SUPABASE_PASSWORD por meio do seu gerenciador de segredos
ou do ambiente protegido do processo. Use a URL raiz do projeto, como
https://PROJECT_REF.supabase.co. Não coloque senhas, tokens de usuário ou chaves administrativas no
código-fonte nem no histórico do shell. O programa obtém uma nova sessão de usuário por meio da
concessão por senha do Auth a cada execução; ele não precisa de uma
chave secreta nem de uma chave service_role.
Configurar seu ambiente
Cole este bloco no Bash. O subshell mantém seu diretório atual e as opções do shell intactos, inclusive se a configuração falhar. Um diretório de projeto existente é preservado.
(
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
)
Substitua o supabase-storage-demo/Gemfile recém-criado por:
source 'https://rubygems.org'
gem 'json', '3.0.2'
gem 'net-http', '0.10.0'
As versões fixadas correspondem às versões atuais de JSON e
Net::HTTP, conferidas em 12 de outubro de 2026.
Instale-as em vendor/bundle neste projeto, sem instalar gems globalmente no sistema:
(cd supabase-storage-demo && bundle install)
Se a instalação falhar, resolva o problema relatado de rede ou de pré-requisito do compilador e execute novamente esse bloco de instalação no mesmo projeto. Mantenha o Gemfile e a configuração local do Bundler; não repita o bloco de criação do diretório nem remova um projeto existente para tentar novamente.
Configurar um cliente da REST API em Ruby
Salve este programa completo como supabase-storage-demo/storage.rb. Ele processa um arquivo por vez,
incluindo arquivos vazios e binários, com um limite de 1 MiB imposto pela aplicação. Os nomes dos
arquivos locais devem conter bytes UTF-8 válidos, sem caracteres de controle; a validação não
depende da configuração regional do shell. As chaves dos objetos usam letras ASCII, dígitos,
espaços, _, -,
. e /, sem segmentos de pasta vazios,
. ou ... Escolha uma chave compatível
independentemente do nome do arquivo local: a
versão testada do Storage rejeita Unicode e sinais de porcentagem nas chaves.
O programa codifica cada segmento compatível da chave para a 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
O upload envia bytes brutos, não JSON nem campos multipart. A leitura do objeto armazenado usa o endpoint autenticado de objetos, com as permissões do mesmo usuário. Cada operação de rede tem um tempo limite de conexão de 10 segundos e tempos limite de inatividade de leitura e gravação de 30 segundos, mas não um prazo para a transferência inteira. As novas tentativas automáticas estão desativadas: a perda de uma resposta pode deixar um objeto armazenado, mesmo que o comando informe uma falha.
Exemplos de uso
Verifique as credenciais fornecidas e obtenha o UUID do usuário:
(cd supabase-storage-demo && bundle exec ruby storage.rb whoami)
Coloque um arquivo chamado report.bin dentro do projeto. Substitua
USER_UUID abaixo pelo UUID exibido; ele deve ser a primeira pasta na chave do
objeto para atender às políticas acima.
(cd supabase-storage-demo &&
bundle exec ruby storage.rb upload ruby-exports 'USER_UUID/exports/report.bin' ./report.bin)
Um comando bem-sucedido exibe a contagem de bytes e o SHA-256 do objeto baixado e termina com código zero. O objeto permanece na chave selecionada. Para verificá-lo novamente sem fazer upload nem substituí-lo:
(cd supabase-storage-demo &&
bundle exec ruby storage.rb verify ruby-exports 'USER_UUID/exports/report.bin' ./report.bin)
Mantenha o arquivo de origem inalterado durante a comparação. Coloque entre aspas os argumentos de
caminho que contêm espaços e use ./ para um nome de arquivo local que
começa com hífen. Este exemplo não lista pastas nem cria links públicos de download.
Tratamento de erros e validação de arquivos
Qualquer upload recusado, falha na leitura do objeto armazenado ou exclusão não confirmada faz o processo terminar com código diferente de zero. Chaves existentes são recusadas pelos uploads padrão; o programa não solicita upsert nem substitui os bytes anteriores. Execute os comandos sequencialmente.
Uma falha HTTP, por si só, não identifica a causa. Verifique a URL do projeto, as credenciais do usuário, as restrições do bucket, o prefixo UUID da chave e as políticas aplicáveis. Um objeto ausente ou inacessível pode gerar a mesma resposta. O programa omite os corpos brutos das respostas do servidor, as credenciais e os erros de bibliotecas do seu diagnóstico.
O limite de 1 MiB inclui esse valor exato e permite zero bytes. Esse é o limite deste exemplo;
seu projeto e seu bucket podem impor restrições adicionais. O cabeçalho MIME declara conteúdo
binário genérico e não identifica formatos de arquivo nem comprova a segurança do conteúdo. Se o
upload for bem-sucedido, mas a verificação falhar, inspecione essa chave usando o comando
verify antes de tentar novamente: o upload não é revertido nem excluído
silenciosamente.
Boas práticas
Para exportações recorrentes de relatórios, escolha uma nova chave para cada relatório, por exemplo,
USER_UUID/exports/2026/10/report-12.bin. Uma pasta com a data mantém as exportações organizadas; um
identificador adicional de tarefa distingue vários relatórios do mesmo dia. Guarde a chave junto
ao registro da tarefa para que a verificação e a exclusão posteriores tenham como alvo o mesmo objeto.
Lidar com arquivos grandes
Este exemplo para arquivos pequenos mantém em buffer tanto a entrada quanto os dados da leitura do objeto armazenado e não consegue retomar uploads interrompidos. Para transferências maiores, use o protocolo de upload retomável do Supabase com um cliente tus compatível. Blocos independentes enviados ao endpoint padrão de objetos não implementam esse protocolo. Transferências retomáveis estão fora do escopo verificado deste exemplo.
Excluir uma exportação explicitamente
Quando o arquivo exportado não for mais necessário, exclua apenas a chave selecionada:
(cd supabase-storage-demo &&
bundle exec ruby storage.rb delete ruby-exports 'USER_UUID/exports/report.bin')
Para que a operação seja considerada bem-sucedida, a resposta de exclusão precisa identificar esse objeto. Um array vazio pode significar que ele está ausente ou oculto pelas políticas, por isso o comando informa uma falha nesse caso. Excluir uma exportação não remove seu bucket, outros objetos, usuários nem políticas.
