Importer efficacement des fichiers de Supabase en Python
Pour importer des fichiers de Supabase en Python, téléchargez leurs octets avec le SDK officiel open source et enregistrez-les localement. Le script ci-dessous prend en charge un seul objet ou chaque objet visible sous un dossier, y compris les objets des pages suivantes de la liste et des dossiers imbriqués. Il refuse d’écraser les fichiers locaux et renvoie un code de sortie indiquant un échec si une importation s’interrompt.
Configurer Supabase et Python
Vous avez besoin d’un projet Supabase, d’un bucket privé et d’un utilisateur authentifié par
adresse e-mail et mot de passe qui peut lire ses objets. Ce tutoriel utilise un shell POSIX et un
système de fichiers local prenant en charge les liens physiques. Il a été testé sous Linux avec
Python 3.12.13, supabase 2.13.0 et storage3 0.11.3.
Utilisez une version corrective de Python 3.12 toujours maintenue ; la version exacte ci-dessus est
celle qui a été testée.
1. Installer les bibliothèques Python requises
Dans un répertoire que vous contrôlez, avec python3.12 et son module
venv installés, collez :
(
command -v python3.12 >/dev/null || exit 1
if [ -e .venv ] || [ -L .venv ]; then
printf 'Choose a directory without an existing .venv.\n' >&2
exit 1
fi
python3.12 -m venv .venv &&
.venv/bin/python -m pip install 'supabase==2.13.0'
)
Le sous-shell laisse votre répertoire courant et les options de votre shell inchangés. Un échec
d’installation peut laisser un environnement .venv incomplet ; conservez-le
à des fins de diagnostic et réessayez dans un nouveau répertoire. Les commandes ci-dessous utilisent
explicitement cet environnement virtuel, sans l’activer.
2. Configurer les informations d’accès et les autorisations dans Supabase
Fournissez ces variables d’environnement via l’environnement de votre processus ou votre gestionnaire de secrets :
| Variable | Valeur |
|---|---|
SUPABASE_URL | L’URL de votre projet |
SUPABASE_PROJECT_KEY | La clé JWT historique anon du projet |
SUPABASE_USER_EMAIL | L’adresse e-mail d’un utilisateur confirmé |
SUPABASE_USER_PASSWORD | Le mot de passe de cet utilisateur |
SUPABASE_SOURCE_PREFIX | L’UUID de cet utilisateur suivi de /images |
Cette procédure cible l’interface de clés JWT historiques de la version figée du client.
La version 2.13.0 valide les clés de projet comme des JWT,
les clés sb_publishable_… nécessitent donc un client plus récent. Utilisez ici la clé
anon et authentifiez l’utilisateur ordinaire ; n’utilisez pas de clés du
rôle de service dans l’importateur.
Supabase prévoit de déprécier les clés historiques à la fin de 2026 ;
cette procédure s’applique aux projets qui les exposent encore.
Pour un exemple minimal, demandez au propriétaire de votre projet de créer un bucket privé
my-bucket et d’y téléverser image.jpg et
nested/photo.jpg sous <user-uuid>/images. L’UUID de l’utilisateur correspond
à son identifiant d’utilisateur Auth, et non à son adresse e-mail. Le propriétaire du projet peut
accorder l’accès en lecture au dossier UUID de chaque utilisateur avec cette politique dans
l’éditeur SQL, en remplaçant my-bucket si nécessaire :
create policy "Read own UUID folder"
on storage.objects for select to authenticated
using (
bucket_id = 'my-bucket'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
L’énumération et le
téléchargement nécessitent tous deux l’autorisation
SELECT sur storage.objects. L’importateur n’a besoin d’aucune
autorisation de création de bucket ni de téléversement. Vérifiez aussi toutes les politiques
existantes : les politiques permissives peuvent accorder un accès plus large que le seul dossier
prévu dans cet exemple. Consultez le guide du contrôle d’accès de Storage.
3. Établir une connexion à votre bucket Supabase
Enregistrez ce programme complet sous le nom import_supabase.py. Il s’authentifie avant
de créer l’objet d’accès au bucket. Conservez le contrôle des répertoires parents de la destination
et exécutez les importations l’une après l’autre, sans qu’un autre processus ne modifie ces
répertoires. Le script refuse les répertoires parents qui sont des liens symboliques et les
composants non sûrs des chemins distants.
import argparse
import os
import sys
import tempfile
from pathlib import Path
from urllib.parse import quote
from supabase import create_client
VALID_NAME_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-!.*'() &$=@;+,?"
def validate_path(value: str, allow_empty: bool = False) -> None:
if allow_empty and value == '':
return
for name in value.split('/'):
if (not name or name in {'.', '..'}
or any(char not in VALID_NAME_CHARACTERS for char in name)):
raise ValueError('Unsafe remote path component')
def download_file(bucket, key: str, target: Path) -> None:
validate_path(key)
print(f'Downloading {key}', flush=True)
for parent in [target.parent, *target.parent.parents]:
if parent.is_symlink():
raise ValueError('Symlink destination parent')
target.parent.mkdir(parents=True, exist_ok=True)
data = bucket.download(quote(key, safe='/'))
with tempfile.TemporaryDirectory(dir=target.parent) as staging:
temporary = Path(staging) / 'download'
temporary.write_bytes(data)
# Linking publishes a complete file and refuses an existing destination.
os.link(temporary, target)
print(f'Downloaded {key}')
def import_directory(bucket, prefix: str, root: Path) -> int:
prefix = prefix.rstrip('/')
validate_path(prefix, allow_empty=True)
pending = [(prefix, root)]
count = 0
while pending:
remote, local = pending.pop()
offset = 0
while True:
rows = bucket.list(path=remote, options={
'limit': 100, 'offset': offset,
'sortBy': {'column': 'name', 'order': 'asc'},
})
for row in rows:
name = row['name']
validate_path(name)
if '/' in name:
raise ValueError('Expected an immediate child name')
key = f'{remote}/{name}' if remote else name
target = local / name
if row.get('id') is None:
pending.append((key, target))
continue
download_file(bucket, key, target)
count += 1
if len(rows) < 100:
break
offset += 100
return count
def main() -> None:
parser = argparse.ArgumentParser(description='Download Supabase objects without overwriting')
parser.add_argument('mode', choices=['file', 'directory'])
parser.add_argument('bucket')
parser.add_argument('source')
parser.add_argument('destination')
args = parser.parse_args()
client = create_client(os.environ['SUPABASE_URL'], os.environ['SUPABASE_PROJECT_KEY'])
client.auth.sign_in_with_password({
'email': os.environ['SUPABASE_USER_EMAIL'],
'password': os.environ['SUPABASE_USER_PASSWORD'],
})
bucket = client.storage.from_(args.bucket)
destination = Path(args.destination).absolute()
if args.mode == 'file':
download_file(bucket, args.source, destination)
count = 1
else:
count = import_directory(bucket, args.source, destination)
print(f'Imported {count} file(s)')
if __name__ == '__main__':
try:
main()
except Exception as error:
print(f'Import stopped ({type(error).__name__}). Check credentials, '
'Storage SELECT policies, and destination paths.', file=sys.stderr)
sys.exit(1)
download() renvoie l’objet entier sous forme d’octets. Le fichier temporaire est
écrit en premier ; os.link() crée ensuite la destination sans remplacer un
fichier, un répertoire ou un lien symbolique déjà présent à cet emplacement. Le répertoire temporaire
est supprimé à la sortie de son bloc de contexte, avec le fichier temporaire qu’il contient.
Cela nécessite la prise en charge des liens physiques par le système de fichiers de destination ;
le script ne télécharge pas en flux continu.
Comment importer des fichiers de Supabase avec Python
Importer un seul fichier
Une fois les variables d’environnement ci-dessus définies, exécutez :
.venv/bin/python import_supabase.py file my-bucket "$SUPABASE_SOURCE_PREFIX/image.jpg" local/image.jpg
Les octets de l’objet sont enregistrés dans local/image.jpg. Une exécution réussie
indique la clé téléchargée et un total d’un fichier importé. Une destination existante déclenche
FileExistsError et entraîne un code de sortie de 1 ; choisissez une nouvelle
destination au lieu de supprimer un fichier dont vous avez besoin.
Importer plusieurs fichiers d’un répertoire
Utilisez le même programme pour copier toute l’arborescence visible du dossier :
.venv/bin/python import_supabase.py directory my-bucket "$SUPABASE_SOURCE_PREFIX" downloads
Pour la configuration à deux objets ci-dessus, vous obtenez downloads/image.jpg et
downloads/nested/photo.jpg, avec un total de deux fichiers importés. Les entrées de la liste
dont l’identifiant d’objet est null représentent des dossiers ; le script énumère leurs éléments
enfants au lieu de télécharger ces entrées. Chaque dossier est paginé indépendamment, et le
décalage inclut les entrées de dossiers comme celles de fichiers.
Cet exemple accepte les
noms ASCII autorisés par Storage,
notamment ceux qui contiennent des espaces et des points d’interrogation. Il les encode pour le
téléchargement sans modifier leurs noms locaux. Les fichiers vides sont valides.
Les vérifications des chemins rejettent en outre les composants vides, .
et .., les barres obliques inverses, les deux-points, les signes de
pourcentage, les croisillons et les caractères non ASCII. Les fichiers de destination existants
sont conservés ; en cas de collision, le script interrompt l’importation au lieu d’ignorer un
fichier et de signaler une réussite.
Limites et considérations relatives au stockage
Chaque téléchargement garde un objet entier en mémoire, et chaque liste contient au plus 100 éléments enfants directs. Choisissez des fichiers qui tiennent dans la mémoire disponible. Une arborescence volumineuse accumule aussi des chemins de dossiers en attente ; cet exemple ne fixe aucune limite de mémoire ni de durée pour l’ensemble de l’importation.
Gardez l’arborescence distante stable pendant l’importation. La pagination par décalage n’est pas un instantané : des téléversements, renommages ou suppressions simultanés peuvent entraîner des entrées manquantes ou répétées. En cas d’échec, l’importation s’arrête avec le code de sortie 1 et les fichiers déjà entièrement importés sont conservés ; l’importation du répertoire n’est pas une transaction. Après avoir corrigé la cause, utilisez une nouvelle destination pour que ces fichiers déjà importés ne bloquent pas une nouvelle exécution.
Diagnostiquer une importation interrompue ou vide
Une erreur d’authentification nécessite de vérifier l’URL du projet, la clé historique
anon et les informations d’accès de l’utilisateur confirmé. Une erreur de
Storage nécessite de vérifier le bucket, le chemin exact de l’objet et les politiques
SELECT de cet utilisateur. Une erreur OSError,
y compris FileExistsError, nécessite de vérifier les autorisations locales, les
destinations existantes, l’espace disque disponible et la prise en charge des liens physiques.
La dernière clé dont l’importation a été tentée apparaît dans la sortie de progression, ce qui
vous permet d’identifier un objet en échec. Les réponses du fournisseur et les informations
d’accès ne sont pas affichées par le script.
Un total de zéro fichier importé signifie que l’énumération n’a révélé aucun objet. Cela ne prouve pas que le bucket est vide : RLS peut masquer des objets pour cet utilisateur. Confirmez l’inventaire attendu auprès du propriétaire du projet avant de considérer une copie locale comme complète. Les restrictions MIME du bucket et les limites de taille des téléversements régissent les téléversements ; elles ne valident pas le contenu des fichiers téléchargés.
Utiliser les fichiers importés
Lisez les fichiers enregistrés avec les bibliothèques Python habituelles de votre application. Si vous avez besoin d’un processus de traitement multimédia distinct après le téléchargement, vous pouvez consulter la documentation du SDK Python de Transloadit pour en savoir plus ; le téléchargeur ci-dessus n’envoie pas de fichiers à traiter.
