Dateien effizient aus Supabase mit Python importieren
Um Dateien aus Supabase mit Python zu importieren, laden Sie deren Bytes mit dem offiziellen Open-Source-SDK herunter und speichern sie lokal. Das folgende Skript verarbeitet ein einzelnes Objekt oder alle sichtbaren Objekte unterhalb eines Ordners, einschließlich weiterer Seiten der Auflistung und verschachtelter Ordner. Es überschreibt keine lokalen Dateien und gibt einen Fehler-Exit-Status zurück, wenn ein Import stoppt.
Supabase und Python einrichten
Sie benötigen ein Supabase-Projekt, einen privaten Bucket und einen Benutzer mit E-Mail-Adresse und
Passwort, der dessen Objekte lesen darf. Diese Anleitung verwendet eine POSIX-Shell und ein lokales
Dateisystem, das Hardlinks unterstützt. Getestet wurde sie unter Linux mit Python 3.12.13,
supabase 2.13.0 und storage3 0.11.3.
Verwenden Sie eine gepflegte Patch-Version von Python 3.12; die oben genannte genaue Patch-Version
ist die getestete Version.
1. Erforderliche Python-Bibliotheken installieren
Fügen Sie in einem Verzeichnis unter Ihrer Kontrolle mit installiertem
python3.12 und dessen Modul venv Folgendes ein:
(
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'
)
Die Subshell lässt Ihr aktuelles Verzeichnis und Ihre Shell-Optionen unverändert. Eine fehlgeschlagene
Installation kann ein unvollständiges .venv hinterlassen.
Bewahren Sie es zur Diagnose auf und versuchen Sie es in einem neuen Verzeichnis erneut.
Die folgenden Befehle verwenden diese virtuelle Umgebung explizit, ohne sie zu aktivieren.
2. Zugangsdaten und Berechtigungen in Supabase konfigurieren
Stellen Sie diese Umgebungsvariablen über Ihre Prozessumgebung oder Ihren Secret-Manager bereit:
| Variable | Wert |
|---|---|
SUPABASE_URL | Ihre Projekt-URL |
SUPABASE_PROJECT_KEY | Der Legacy-JWT-Schlüssel anon des Projekts |
SUPABASE_USER_EMAIL | Die E-Mail-Adresse eines bestätigten Benutzers |
SUPABASE_USER_PASSWORD | Das Passwort dieses Benutzers |
SUPABASE_SOURCE_PREFIX | Die UUID dieses Benutzers, gefolgt von /images |
Diese Anleitung nutzt die Schnittstelle für Legacy-JWT-Schlüssel des Clients in der festgelegten
Version. Version 2.13.0
validiert Projektschlüssel als JWTs,
daher erfordern Schlüssel vom Typ sb_publishable_… einen neueren Client.
Verwenden Sie hier den Schlüssel anon und authentifizieren Sie den
normalen Benutzer; verwenden Sie im Importer keine Schlüssel mit Service-Rolle.
Supabase plant, Legacy-Schlüssel Ende 2026 abzukündigen;
diese Anleitung gilt für Projekte, die sie noch bereitstellen.
Für ein kleines Beispiel bitten Sie den Projektverantwortlichen, einen privaten Bucket namens
my-bucket zu erstellen und image.jpg und
nested/photo.jpg unterhalb von <user-uuid>/images hochzuladen.
Die UUID des Benutzers ist seine Auth-Benutzer-ID, nicht seine E-Mail-Adresse. Der Projektverantwortliche
kann mit dieser Richtlinie im SQL-Editor jedem Benutzer Lesezugriff auf dessen UUID-Ordner gewähren.
Ersetzen Sie bei Bedarf my-bucket:
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
);
Sowohl das Auflisten als auch das
Herunterladen erfordern die Berechtigung
SELECT für storage.objects.
Der Importer benötigt keine Berechtigung zum Erstellen von Buckets oder zum Hochladen.
Prüfen Sie auch vorhandene Richtlinien: Permissive Richtlinien können Zugriff über die
Ordnerbeschränkung dieses Beispiels hinaus gewähren. Weitere Informationen finden Sie in der
Anleitung zur Zugriffskontrolle für Storage.
3. Eine Verbindung zu Ihrem Supabase-Bucket herstellen
Speichern Sie dieses vollständige Programm als import_supabase.py.
Es meldet den Benutzer an, bevor es das Bucket-Handle erstellt.
Behalten Sie die Kontrolle über die übergeordneten Verzeichnisse des Ziels und führen Sie Importe
nacheinander aus, ohne dass ein anderer Prozess diese Verzeichnisse verändert. Das Skript lehnt
übergeordnete Verzeichnisse ab, die symbolische Links sind, sowie unsichere entfernte Pfadkomponenten.
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() gibt das gesamte Objekt als Bytes zurück.
Zuerst wird die temporäre Datei geschrieben; anschließend erstellt os.link()
das Ziel, ohne eine bereits unter diesem Pfad vorhandene Datei, ein Verzeichnis oder einen
symbolischen Link zu ersetzen. Beim Verlassen des Blocks wird die temporäre Datei entfernt.
Dies erfordert Hardlink-Unterstützung im Zieldateisystem; Streaming-Downloads werden nicht verwendet.
Wie importieren Sie Dateien aus Supabase mit Python?
Eine einzelne Datei importieren
Führen Sie bei gesetzten Umgebungsvariablen aus dem obigen Abschnitt Folgendes aus:
.venv/bin/python import_supabase.py file my-bucket "$SUPABASE_SOURCE_PREFIX/image.jpg" local/image.jpg
Die Bytes des Objekts werden unter local/image.jpg gespeichert.
Ein erfolgreicher Durchlauf meldet den heruntergeladenen Schlüssel und eine Importanzahl von eins.
Ein vorhandenes Ziel verursacht FileExistsError und den Exit-Status 1;
wählen Sie ein neues Ziel, statt eine noch benötigte Datei zu entfernen.
Mehrere Dateien aus einem Verzeichnis importieren
Verwenden Sie dasselbe Programm, um den gesamten sichtbaren Ordnerbaum zu kopieren:
.venv/bin/python import_supabase.py directory my-bucket "$SUPABASE_SOURCE_PREFIX" downloads
Mit den zwei Objekten aus dem obigen Beispiel erhalten Sie downloads/image.jpg und
downloads/nested/photo.jpg bei einer Importanzahl von zwei. Einträge der Auflistung mit einer
Objekt-ID mit dem Wert null stehen für Ordner; das Skript listet deren untergeordnete Elemente auf,
statt diese Einträge herunterzuladen. Jeder Ordner wird unabhängig paginiert, und der Offset umfasst
sowohl Ordnereinträge als auch Dateien.
Dieses Beispiel akzeptiert
ASCII-Namen, die Storage zulässt,
einschließlich Leerzeichen und Fragezeichen. Es codiert sie für den Download, lässt ihre lokalen
Namen aber unverändert. Leere Dateien sind gültig.
Die Pfadprüfungen lehnen zusätzlich leere Komponenten, . und
.., Backslashes, Doppelpunkte, Prozentzeichen, Rautenzeichen und
Nicht-ASCII-Zeichen ab. Vorhandene Zieldateien bleiben erhalten; eine Kollision stoppt den Import,
statt eine Datei zu überspringen und Erfolg zu melden.
Einschränkungen von Storage und wichtige Hinweise
Jeder Download hält ein vollständiges Objekt im Arbeitsspeicher, und jede Auflistung hält bis zu 100 unmittelbar untergeordnete Elemente. Wählen Sie Dateien, die in den verfügbaren Arbeitsspeicher passen. Bei einem großen Ordnerbaum sammeln sich außerdem noch ausstehende Ordnerpfade an; dieses Beispiel setzt kein Speicher- oder Zeitlimit für den gesamten Import.
Halten Sie den entfernten Ordnerbaum während des Imports unverändert. Offset-Paginierung ist keine Momentaufnahme: Gleichzeitige Uploads, Umbenennungen oder Löschungen können zu fehlenden oder wiederholten Einträgen führen. Ein Fehler stoppt den Import mit Exit-Status 1 und lässt bereits vollständig gespeicherte Dateien bestehen; der Verzeichnisimport ist keine Transaktion. Verwenden Sie nach Behebung der Ursache ein neues Ziel, damit diese fertigen Dateien einen erneuten Durchlauf nicht blockieren.
Einen gestoppten oder leeren Import diagnostizieren
Prüfen Sie bei einem Authentifizierungsfehler die Projekt-URL, den Legacy-Schlüssel
anon und die Zugangsdaten des bestätigten Benutzers.
Prüfen Sie bei einem Storage-Fehler den Bucket, den genauen Objektpfad und die Richtlinien
SELECT dieses Benutzers. Prüfen Sie bei einem
OSError, einschließlich FileExistsError, die lokalen
Berechtigungen, vorhandene Ziele, freien Speicherplatz auf dem Datenträger und die
Hardlink-Unterstützung. Der zuletzt versuchte Schlüssel erscheint in der Fortschrittsausgabe,
damit Sie ein fehlgeschlagenes Objekt identifizieren können. Das Skript gibt weder Antworten des
Anbieters noch Zugangsdaten aus.
Eine Importanzahl von null bedeutet, dass die Auflistung keine Objekte sichtbar gemacht hat. Sie beweist nicht, dass der Bucket leer ist: RLS kann Objekte vor diesem Benutzer verbergen. Bestätigen Sie den erwarteten Bestand mit dem Projektverantwortlichen, bevor Sie eine lokale Kopie als vollständig betrachten. MIME-Beschränkungen des Buckets und Upload-Größenlimits gelten für Uploads; sie validieren nicht den Inhalt heruntergeladener Dateien.
Die importierten Dateien verwenden
Lesen Sie die gespeicherten Dateien mit den üblichen Python-Bibliotheken Ihrer Anwendung. Wenn Sie nach dem Download einen separaten Workflow zur Medienverarbeitung benötigen, finden Sie weiterführende Informationen im Python-SDK von Transloadit; der obige Downloader übermittelt keine Dateien zur Verarbeitung.
