Exportar arquivos para o Supabase com cURL
Para exportar um arquivo local para o Supabase Storage privado, envie seus bytes com um POST autenticado, depois baixe o objeto e compare-o com o original. Este passo a passo usa dois usuários comuns do Auth: um faz upload e lê o arquivo, e o outro verifica se a política de acesso nega o mesmo download.
Configurar um bucket privado e suas políticas
Você precisa de um projeto Supabase que administre e de dois usuários distintos do Auth com e-mail e senha que já consigam fazer login. Conclua a confirmação de e-mail quando seu projeto exigir; consulte a documentação de Auth baseado em senha. Criar uma conta Supabase, por si só, não cria esses usuários da aplicação.
Crie um novo bucket privado do Storage chamado curl-exports, seguindo o
guia de início rápido de buckets. Permita
application/octet-stream se você restringir os tipos MIME e escolha um arquivo que respeite o
limite de tamanho do bucket. Use um bucket novo neste exemplo. Revise também as políticas existentes
de storage.objects: uma regra permissiva que se aplique a este bucket também pode
conceder acesso. A verificação com o segundo usuário abaixo testa as permissões efetivas de leitura.
Execute este SQL uma vez como administrador do projeto. Ele permite uploads e leituras apenas dentro de uma pasta cujo nome é o UUID do usuário autenticado:
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
);
A documentação de controle de acesso do Storage
explica as políticas de storage.objects. O script de upload abaixo usa o token de acesso
de um usuário; uma chave secreta ou de função de serviço contornaria a RLS e invalidaria a verificação
de privacidade.
Verificar suas ferramentas de shell e credenciais
Use Bash no Linux com cURL, jq
e os utilitários comuns cmp, mktemp e
rm já instalados. As versões testadas são Bash 5.3.15, cURL 8.22.0 e
jq 1.8.2. São versões testadas, não requisitos de versões exatas; o script usa a opção
--fail-with-body do cURL e verifica cada ferramenta necessária antes de criar arquivos
temporários. As requisições do script ignoram as configurações de .curlrc.
Execute os comandos de configuração no Bash.
curl --disable --version && jq --version && bash --version
Este passo a passo usa uma chave de API legada anon já existente, de acordo
com o stack local testado. O Supabase planeja marcar as chaves legadas como obsoletas até o fim de 2026;
o guia de chaves de API aborda sua obtenção e migração. Defina a URL
do projeto, a chave e o bucket desse stack no shell em que você executará o script:
export SUPABASE_URL='https://YOUR_PROJECT_ID.supabase.co' \
SUPABASE_KEY='YOUR_ANON_KEY' \
SUPABASE_BUCKET='curl-exports'
Configure seu gerenciador de credenciais ou executor de tarefas para fornecer
SUPABASE_EMAIL, SUPABASE_PASSWORD,
SUPABASE_OTHER_EMAIL e SUPABASE_OTHER_PASSWORD como variáveis de ambiente. O primeiro
par pertence ao usuário que faz o upload; o segundo, ao usuário cujo acesso deve ser negado. Evite
colocar senhas no histórico do shell. A chave do projeto não substitui o JWT do usuário no cabeçalho
Authorization.
Este exemplo foi testado com Supabase Auth v2.196.0 e Storage v1.74.0 locais, com PostgreSQL 17.6. A criação de um projeto hospedado, o TLS, a configuração do painel e as cotas do plano são pré-requisitos separados. Use HTTPS em um projeto hospedado; HTTP via loopback é adequado para um stack de desenvolvimento local.
Fazer upload, baixar e verificar um arquivo
Salve o conteúdo a seguir em um novo arquivo chamado export-to-supabase.sh. Execute-o com
Bash, em vez de carregá-lo no shell atual com source. O primeiro argumento é o arquivo local; o
segundo é o caminho do objeto sob o UUID do usuário que faz o upload. Os segmentos do caminho do
objeto devem começar com uma letra ASCII ou um dígito e conter apenas letras ASCII, dígitos,
espaços, pontos, sublinhados e hifens. Os nomes dos arquivos locais não precisam seguir essa regra
mais restrita de nomenclatura 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'
Substitua ./report.bin pelo arquivo escolhido. Mantenha esse arquivo inalterado
enquanto o script estiver em execução. O nome do arquivo remoto pode ser diferente do nome local:
bash export-to-supabase.sh './report.bin' 'exports/report.bin'
Uma execução bem-sucedida imprime:
Verified upload and private access.
O objeto permanece em curl-exports/<uploader-uuid>/exports/report.bin. A cópia baixada e as respostas do Auth são
temporárias e removidas quando o script termina. Se houver divergência ou se o outro usuário
conseguir baixar o arquivo inesperadamente, o script termina com código diferente de zero, sem
imprimir essa mensagem de sucesso. O
endpoint de download privado
usa o JWT do usuário e a política de leitura; a chave de API, por si só, não concede esse acesso.
Lidar com objetos existentes sem perder exportações anteriores
O POST cria um novo objeto. Executar o mesmo comando novamente usa o mesmo UUID e caminho de objeto, por isso um objeto existente é rejeitado. O objeto anterior permanece armazenado. Use outro caminho de objeto para uma nova exportação.
O guia de upload padrão do Supabase
documenta a substituição intencional com x-upsert: true, que também exige políticas
SELECT e UPDATE. Este passo a passo não concede
permissão de atualização nem sobrescreve objetos. Se ocorrer uma falha de conexão depois que o
servidor armazenar o arquivo, o resultado do upload será incerto. Inspecione e baixe esse objeto
exato antes de decidir se vai mantê-lo ou usar um novo caminho; uma resposta de duplicidade em uma
nova tentativa pode significar que o primeiro upload foi bem-sucedido.
Entender os limites de tamanho dos arquivos
Escolha um arquivo pequeno que respeite tanto o limite global do projeto quanto o limite do bucket.
Os limites máximos dos planos hospedados e as restrições dos buckets estão descritos na
documentação de limites de arquivos do Supabase.
A opção --data-binary @file do cURL
armazena a entrada em buffer na memória,
e este exemplo não retoma uma transferência interrompida. Para arquivos maiores, siga o
guia separado de uploads retomáveis.
Automatizar a exportação de arquivos em um fluxo de trabalho diário
O script salvo faz login a cada invocação, de modo que a automação não depende de um JWT colado continuar válido. Configure seu agendador para fornecer as variáveis de ambiente, use caminhos absolutos para o script e o arquivo de entrada e escolha um novo caminho de objeto para cada exportação. Mantenha a segunda conta para verificar as permissões e trate um encerramento com código diferente de zero como falha na verificação, mesmo quando o arquivo do upload já existir remotamente.
Solucionar problemas comuns
Erros de autenticação (401 não autorizado)
Verifique primeiro a URL do projeto e a chave de API. Se o login com senha falhar, confira o e-mail, a senha e o status de confirmação do usuário. O script para quando um login falha; ele não usa uma chave de administrador como substituta. Se o token expirar durante uma transferência, execute novamente com um novo caminho de objeto depois de verificar se o upload anterior foi efetivado.
Erros de arquivo duplicado
Um caminho duplicado é um erro, normalmente HTTP 400 no Storage. Altere o segundo argumento do script para uma nova exportação. Não adicione novas tentativas automáticas nem upsert apenas para ocultar a colisão.
Limites de tamanho de arquivo (413 conteúdo da requisição muito grande)
Um upload acima do limite de tamanho pode produzir HTTP 413. A versão testada do Storage retornou
HTTP 400 com status 413 no corpo da resposta em uma recusa devido ao limite de tamanho do bucket.
Verifique os dois limites de tamanho e confira a lista de tipos MIME permitidos do bucket se a
recusa estiver relacionada ao tipo. Este script envia application/octet-stream; ele não detecta
o tipo de mídia do arquivo. Consulte os códigos de erro do Storage
para ver os diagnósticos do serviço.
Permissão negada
Confira se o bucket é privado e se as políticas se aplicam ao nome exato dele, à pasta com o UUID do
usuário que faz o upload e a owner_id. Um objeto privado inacessível pode parecer
inexistente: a versão testada do Storage retornou HTTP 400 com status 404 no corpo da resposta para
o segundo usuário. O script primeiro usa a conta do proprietário para ler e comparar o mesmo objeto,
por isso um objeto ausente não pode passar nessa verificação. Se o outro usuário receber HTTP 200,
inspecione as políticas existentes e a visibilidade do bucket antes de exportar dados sensíveis.
