Dieser Guide erklärt den praktischen Einsatz von std.cloud.s3 für AWS S3, Cloudflare R2 und MinIO. 54 Funktionen decken alles ab: einfache Up-/Downloads, Multipart für große Dateien, Presigned URLs und vollständig paginiertes Listing.
→ std.cloud.s3 (Unit-Referenz) · Cloud-Guide · Welche Unit?
<WRAP alert>
std.cloud.s3 und std.datetime lassen sich nicht gemeinsam importieren. Beide bringen DaysInMonth mit:
sema error: mehrdeutiges Symbol 'DaysInMonth' — exportiert von 'std.time' und 'std.datetime'
Die Kollision entsteht transitiv: std.cloud.s3 → std.cloud.aws.transport → std.time. Sie ist am Import nicht abzulesen, und einen Ausweg über Alias oder qualifizierten Zugriff gibt es nicht (→ Module & Importe).
Für Zeitangaben in S3-Programmen deshalb std.time nehmen — unix_time(), DatetimeFromUnixSeconds, DatetimeToUnixSeconds. Wer std.datetime braucht, trennt den Code in zwei Units. Nachgemessen mit 1.1.0A.
</WRAP>
S3 ist ein Objekt-Speicher — kein Dateisystem. Objekte haben einen Key (beliebiger String, oft pfadähnlich wie backups/2026/report.pdf), werden in Buckets gespeichert und haben einen zugehörigen Content-Type. Es gibt keine echten Verzeichnisse, aber Keys mit Slash lassen sich durch den Delimiter-Parameter wie Verzeichnisse behandeln.
Region: eu-central-1
└── Bucket: mein-bucket
├── bericht.pdf (Key, Content-Type: application/pdf)
├── fotos/strand.jpg (Key, Content-Type: image/jpeg)
└── backups/2026/db.tar (Key, Content-Type: application/octet-stream)
Maximale Objekt-Größe: 5 TB (Einzelupload: 5 GB; darüber hinaus Multipart-Upload nötig).
import std.cloud.aws.core;
import std.cloud.s3;
// Empfohlen: Credential Chain (Env → ~/.aws/credentials)
var creds: AWSCreds := AWSCredentialsDefault();
var s3: S3Client := S3Connect(creds, "eu-central-1"c);
// Nach der Arbeit immer schließen
S3Close(s3);
AWSCredentialsFree(creds);
Regionen wählen: Bucket-Region und Connect-Region müssen übereinstimmen. S3 gibt bei falscher Region HTTP 301 zurück (Redirect zur richtigen Region) — der Client folgt dem nicht automatisch.
import std.cloud.s3;
var s3: S3Client := S3ConnectR2(
"mein-cf-account-id"c, // Cloudflare Account ID (Dashboard → Rechte Seite)
"r2-access-key-id"c, // R2 API Token (nicht der CF Global API Key!)
"r2-secret-access-key"c
);
// Danach identische API wie AWS S3
R2-Credentials anlegen: Cloudflare Dashboard → R2 → Manage R2 API Tokens → Create API Token.
import std.cloud.s3;
var s3: S3Client := S3ConnectMinio(
"localhost"c, // Host
9000, // Port (MinIO-Default)
"minioadmin"c, // Access Key
"minioadmin"c // Secret Key
);
MinIO starten (Docker):
docker run -d -p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin \
minio/minio server /data --console-address ":9001"
import std.cloud.aws.core;
import std.cloud.s3;
var creds: AWSCreds := AWSCredentialsStatic("test"c, "test"c);
var s3: S3Client := S3ConnectEndpoint(
creds,
"us-east-1"c,
"localhost:4566"c, // LocalStack-Endpoint
0, // kein TLS
1 // path-style (LocalStack-Anforderung)
);
LocalStack starten:
docker run -d -p 4566:4566 localstack/localstack
// Beliebiger S3-kompatibler Dienst:
var s3: S3Client := S3ConnectEndpoint(
creds,
"us-east-1"c, // region (oft irrelevant bei non-AWS)
"storage.example.com"c, // eigener Endpoint (ohne https://)
1, // TLS: 1=ja, 0=nein
0 // path-style: 0=virtual-hosted, 1=path-style
);
import std.cloud.aws.core;
import std.cloud.s3;
import std.alloc;
fn Upload(s3: S3Client): void {
// Aus Datei (einfachster Weg für Dateien)
S3PutObjectFile(s3, "mein-bucket"c, "bericht.pdf"c,
"/tmp/bericht.pdf"c, "application/pdf"c);
// Aus Speicherpuffer
var text: pchar := "Hallo S3!"c;
var len: int64 := 9;
S3PutObject(s3, "mein-bucket"c, "hello.txt"c,
text as int64, len, "text/plain; charset=utf-8"c);
// Fehler prüfen
if (S3LastStatus(s3) != 200) {
Print("Upload-Fehler: "c);
PrintLn(S3LastError(s3));
}
}
Content-Types (Auswahl):
| Content-Type | Typische Erweiterung |
|---|---|
application/pdf | |
application/json | .json |
application/octet-stream | .bin, .tar, .gz (Fallback für alles) |
text/plain; charset=utf-8 | .txt |
text/html; charset=utf-8 | .html |
image/jpeg | .jpg, .jpeg |
image/png | .png |
image/webp | .webp |
video/mp4 | .mp4 |
application/zip | .zip |
fn Download(s3: S3Client): void {
// In Datei speichern (empfohlen für große Objekte)
var ok: int64 := S3GetObjectFile(s3, "mein-bucket"c, "bericht.pdf"c,
"/tmp/download.pdf"c);
if (ok == 0) {
PrintLn("Download fehlgeschlagen"c);
return;
}
// In Speicherpuffer (für kleine Objekte / direkte Verarbeitung)
var buf: int64 := S3GetObject(s3, "mein-bucket"c, "config.json"c);
if (buf != 0) {
// buf zeigt auf allokierten Puffer mit Dateiinhalt
Print(buf as pchar);
free(buf, S3GetObjectSize(s3, "mein-bucket"c, "config.json"c) + 1);
}
}
Nützlich für Logs, CSV-Header oder wenn nur die ersten N Bytes gebraucht werden:
// Nur die ersten 4096 Bytes (z.B. CSV-Header lesen)
var gelesen: int64 := alloc(8);
var header: int64 := S3GetObjectRange(s3, "mein-bucket"c, "data.csv"c, 0, 4095, gelesen);
if (header != 0) {
// header: allokierter Puffer mit genau 4096 Bytes
Print(header as pchar);
free(header, 4096);
}
// Byte-Range innerhalb einer großen Datei (z.B. Video-Seekpoint)
var chunkLen: int64 := alloc(8);
var chunk: int64 := S3GetObjectRange(s3, "mein-bucket"c, "video.mp4"c,
1048576, // Start: 1 MB
1048576, // Laenge: 1 MB
chunkLen); // tatsaechlich gelesen
// Einzelnes Objekt
var ok: int64 := S3DeleteObject(s3, "mein-bucket"c, "alt/datei.pdf"c);
// Mehrere Objekte in einem Request (effizienter als Einzelaufrufe)
var keys: array<pchar> := ["alt/a.pdf"c, "alt/b.pdf"c, "alt/c.pdf"c];
var count: int64 := 3;
S3DeleteObjects(s3, "mein-bucket"c, keys as int64, count);
// Existenzprüfung ohne Datei herunterladen
if (S3ObjectExists(s3, "mein-bucket"c, "bericht.pdf"c) == 0) {
PrintLn("Datei nicht vorhanden"c);
}
// Vollständige Metadaten
var meta: S3ObjectMeta := S3HeadObject(s3, "mein-bucket"c, "bericht.pdf"c);
Print("Größe: "c); PrintLn(IntToStr(meta.size));
Print("Content-Type: "c); PrintLn(meta.contentType as pchar);
Print("ETag: "c); PrintLn(meta.etag as pchar);
Print("Größe in Bytes: "c); PrintLn(IntToStr(S3GetObjectSize(s3, "mein-bucket"c, "bericht.pdf"c)));
// Nur Größe
var groesse: int64 := S3GetObjectSize(s3, "mein-bucket"c, "video.mp4"c);
PrintLn(IntToStr(groesse));
// Kopiert ohne lokalen Download — S3 macht alles serverseitig
S3CopyObject(s3, "quell-bucket"c, "original.pdf"c,
"ziel-bucket"c, "archiv/2026/original.pdf"c);
S3 liefert maximal 1000 Objekte pro Request. Bei größeren Buckets muss paginiert werden.
fn ListeAnzeigen(s3: S3Client): void {
var list: int64 := S3ListObjects(s3, "mein-bucket"c, ""c, 100);
if (list == 0) { PrintLn("Listing fehlgeschlagen"c); return; }
var i: int64 := 0;
while (i < S3ListCount(list)) {
Print(S3ListGetKey(list, i));
Print(" "c);
PrintLn(IntToStr(S3ListGetSize(list, i)));
i := i + 1;
}
if (S3ListIsTruncated(list) == 1) {
PrintLn("(weitere Seiten vorhanden — S3ListAll verwenden)"c);
}
S3ListFree(list);
}
Für Buckets mit mehr als 1000 Objekten — S3ListAll paginiert automatisch durch alle Seiten:
fn AllesListen(s3: S3Client): void {
// Alle Objekte mit Prefix "backups/"
var list: int64 := S3ListAll(s3, "mein-bucket"c, "backups/"c);
if (list == 0) { return; }
var total: int64 := S3ListCount(list);
Print("Gesamt: "c); PrintLn(IntToStr(total));
var i: int64 := 0;
while (i < total) {
Print(S3ListGetKey(list, i));
Print(" "c);
PrintLn(IntToStr(S3ListGetSize(list, i)));
i := i + 1;
}
S3ListFree(list);
}
Sinnvoll wenn der gesamte Bucket sehr groß ist und die Objekte direkt verarbeitet werden sollen, ohne alles in den Speicher zu laden:
fn PaginierteVerarbeitung(s3: S3Client): void {
var token: int64 := 0; // null = erste Seite
while (1 == 1) {
var list: int64 := S3ListObjectsNext(s3, "mein-bucket"c, "logs/"c, token, 1000);
if (list == 0) { break; }
var i: int64 := 0;
while (i < S3ListCount(list)) {
// Objekt direkt verarbeiten — nicht alles sammeln
var key: pchar := S3ListGetKey(list, i) as pchar;
// ... z.B. herunterladen, analysieren, löschen ...
i := i + 1;
}
if (S3ListIsTruncated(list) == 0) {
S3ListFree(list);
break;
}
// Token für nächste Seite merken (Puffer hält Zeiger — List vorher freigeben!)
var nextToken: pchar := S3ListGetToken(list) as pchar;
// nextToken kopieren bevor ListFree — S3ListGetToken gibt internen Zeiger zurück
var kopie: int64 := alloc(256);
StrCopy(kopie as pchar, nextToken);
token := kopie as pchar;
S3ListFree(list);
}
}
S3 hat keine echten Verzeichnisse, aber mit delimiter=/' kann man Prefixes wie Ordner behandeln:
S3PutObject
—-
===== Multipart-Upload =====
Für Dateien über 5 GB ist Multipart-Upload Pflicht. Unter 100 MB ist // Listet nur die "Verzeichnisse" auf der obersten Ebene, keine tiefer liegenden Objekte
var list: int64 := S3ListWithDelimiter(s3, "mein-bucket"c, ""c, "/"c);
// S3ListGetPrefix gibt die CommonPrefixes zurück (= "Verzeichnisse")
var i: int64 := 0;
while (i < S3ListCount(list)) {
// Reguläre Objekte auf der obersten Ebene
PrintLn(S3ListGetKey(list, i) as pchar);
i := i + 1;
}
// CommonPrefixes = virtuelle Unterordner:
// fotos/ backups/ logs/ usw.
S3ListFree(list);
/ S3PutObjectFile einfacher. Im Bereich 100 MB – 5 GB je nach Anforderung:
^ Datei-Größe ^ Empfehlung ^
| < 5 MB | S3PutObject / S3PutObjectFile |
| 5 MB – 5 GB | S3PutObjectMultipart (High-Level, einfach) |
| > 5 GB | Manueller Multipart oder S3PutObjectMultipart |
| > 5 TB | Nicht möglich — S3-Limit |
==== High-Level (empfohlen) ====
S3MultipartAbort
==== Manueller Multipart (für Streaming / eigene Steuerung) ====
import std.cloud.s3;
import std.alloc;
import std.fs;
fn GrosseDateiHochladen(s3: S3Client, localPath: pchar, key: pchar): int64 {
var sz: int64 := FileSize(localPath);
var data: int64 := alloc(sz);
if (data == 0) { return 0; }
ReadFile(localPath, data as pchar, sz);
// Automatisch aufgeteilt in 50-MB-Parts
var ok: int64 := S3PutObjectMultipart(s3, "mein-bucket"c, key,
data, sz,
"application/octet-stream"c,
50000000); // 50 MB pro Part
free(data, sz);
return ok;
}
Wichtig: Nicht abgeschlossene Multipart-Uploads verursachen Speicherkosten! Immer fn ManuellerMultipart(s3: S3Client): void {
var bucket: pchar := "mein-bucket"c;
var key: pchar := "archiv/gross.tar"c;
var PART_SIZE: int64 := 50000000; // 50 MB (Minimum: 5 MB außer letztem Part)
// 1. Upload starten — AWS gibt eine Upload-ID zurück
var uploadId: int64 := S3MultipartBegin(s3, bucket, key, "application/x-tar"c);
if (uploadId == 0) {
PrintLn("Multipart-Start fehlgeschlagen"c);
return;
}
// 2. Parts hochladen (hier simuliert mit fixem Puffer)
var partETags: int64 := alloc(100 * 8); // 100 Zeiger auf ETag-Strings
var partNum: int64 := 1;
// In echtem Code: Datei in PART_SIZE-Stücken lesen
var partData: int64 := alloc(PART_SIZE);
// ... Daten befüllen ...
var etag: int64 := S3MultipartUploadPart(s3, bucket, key,
uploadId, partNum,
partData, PART_SIZE);
poke64(partETags, etag);
free(partData, PART_SIZE);
partNum := partNum + 1;
// 3. Upload abschließen
var ok: int64 := S3MultipartComplete(s3, bucket, key,
uploadId, partETags, partNum - 1);
if (ok == 0) {
// Bei Fehler: Upload abbrechen (sonst entstehen Kosten für die Parts!)
S3MultipartAbort(s3, bucket, key, uploadId);
PrintLn("Multipart-Abschluss fehlgeschlagen — abgebrochen"c);
}
free(partETags, 100 * 8);
free(uploadId, StrLen(uploadId as pchar) + 1);
}
aufrufen wenn der Upload fehlschlägt. In AWS kann man Lifecycle-Rules konfigurieren, die unvollständige Uploads nach N Tagen automatisch löschen.
—-
===== Presigned URLs =====
Presigned URLs ermöglichen zeitlich befristeten Zugriff auf S3-Objekte ohne Credentials — ideal für Browser-Uploads, temporäre Download-Links und öffentliche Freigaben.
0
Ablauf „Browser-Upload direkt zu S3“:
import std.cloud.aws.core;
import std.cloud.s3;
fn PresignedBeispiele(s3: S3Client): void {
var bucket: pchar := "mein-bucket"c;
var key: pchar := "report.pdf"c;
// GET-URL: Benutzer kann Datei herunterladen (1 Stunde gültig)
var dlUrl: int64 := S3PresignGet(s3, bucket, key, 3600);
if (dlUrl != 0) {
Print("Download: "c); PrintLn(dlUrl as pchar);
free(dlUrl, StrLen(dlUrl as pchar) + 1);
}
// PUT-URL: Browser-Upload direkt nach S3 (ohne Server als Proxy, 15 Minuten)
var ulUrl: int64 := S3PresignPut(s3, bucket, "uploads/foto.jpg"c, 900);
if (ulUrl != 0) {
Print("Upload-URL: "c); PrintLn(ulUrl as pchar);
free(ulUrl, StrLen(ulUrl as pchar) + 1);
}
// HEAD-URL: Metadaten abrufen (z.B. für Content-Length-Check)
var headUrl: int64 := S3PresignHead(s3, bucket, key, 300);
if (headUrl != 0) {
free(headUrl, StrLen(headUrl as pchar) + 1);
}
// DELETE-URL: Delegiertes Löschen (mit Vorsicht verwenden!)
var delUrl: int64 := S3PresignDelete(s3, bucket, "temp/draft.pdf"c, 60);
if (delUrl != 0) {
free(delUrl, StrLen(delUrl as pchar) + 1);
}
}
Der Server bekommt die Datei nie zu sehen — sie landet direkt in S3. Spart Bandbreite und Serverkosten.
Ablauf „temporärer Download-Link“:
Browser Eigener Server S3
│ │ │
│── POST /upload-token ──►│ │
│ │── S3PresignPut ─►│
│ │◄── Presigned URL ─┘
│◄── Upload-URL ──────────┤ │
│ │
│────── PUT {presigned-url} (Datei) ────────►│
│◄───── 200 OK ───────────────────────────────┘
—-
===== Bucket-Verwaltung =====
Server S3
│── S3PresignGet ───►│
│◄── Presigned URL ──┘
│
Server sendet URL an Nutzer/App:
"https://mein-bucket.s3.eu-central-1.amazonaws.com/report.pdf?X-Amz-Signature=..."
URL ist 1 Stunde gültig, danach 403.
—-
===== Fehlerbehandlung =====
S3-Funktionen geben bei Fehler // Bucket anlegen (in der mit S3Connect angegebenen Region)
S3BucketCreate(s3, "neuer-bucket"c);
// Existenz prüfen
if (S3BucketExists(s3, "mein-bucket"c) == 0) {
PrintLn("Bucket existiert nicht"c);
}
// Alle Buckets auflisten
var buckets: S3BucketList := S3ListBuckets(s3);
var i: int64 := 0;
while (i < S3BucketListCount(buckets)) {
PrintLn(S3BucketListGetName(buckets, i) as pchar);
i := i + 1;
}
// Bucket löschen (muss leer sein!)
S3BucketDelete(s3, "alter-bucket"c);
zurück (bei int64-Ergebnissen). Den HTTP-Statuscode und Fehlertext danach abfragen:
AccessDenied
Häufige Fehlercodes:
^ HTTP ^ S3-Fehlercode ^ Bedeutung ^ Lösung ^
| 403 | fn MitFehlerbehandlung(s3: S3Client): void {
var ok: int64 := S3PutObjectFile(s3, "mein-bucket"c, "datei.pdf"c,
"/tmp/datei.pdf"c, "application/pdf"c);
if (ok == 0) {
var status: int64 := S3LastStatus(s3);
var errText: pchar := S3LastError(s3) as pchar;
var errCode: pchar := S3LastErrorCode(s3) as pchar;
Print("HTTP "c); Print(IntToStr(status));
Print(": "c); PrintLn(errCode);
PrintLn(errText);
}
}
| Fehlende Berechtigung | IAM-Policy / Bucket-Policy prüfen |
| 403 | InvalidAccessKeyId | Access Key ungültig | Credentials prüfen |
| 404 | NoSuchBucket | Bucket existiert nicht | Bucket-Name und Region prüfen |
| 404 | NoSuchKey | Objekt nicht vorhanden | Key-Pfad prüfen |
| 409 | BucketAlreadyExists | Bucket-Name vergeben | Bucket-Namen sind global eindeutig |
| 413 | EntityTooLarge | Datei > 5 GB bei Einzelupload | Multipart verwenden |
| 503 | ServiceUnavailable | S3 temporär nicht erreichbar | Retry mit Backoff |
—-
===== Provider-Vergleich: AWS S3 / R2 / MinIO =====
^ Kriterium ^ AWS S3 ^ Cloudflare R2 ^ MinIO ^
| Egress-Kosten | ja (pro GB) | nein (0,00 USD) | nein (self-hosted) |
| Speicherkosten | $0,023/GB/Monat | $0,015/GB/Monat | eigene Hardware |
| Latenz zu CF Workers | mittel | sehr niedrig | — |
| Presigned URLs | ja | ja | ja |
| Multipart-Upload | ja | ja | ja |
| Lyx-Connect | S3Connect | S3ConnectR2 | S3ConnectMinio |
| Geeignet für | Standard, AWS-Ökosystem | Statische Assets, CDN-Ursprung | Entwicklung, on-premise |
| API-Unterschied | keine | keine | keine (identische API) |
Faustregel:
* AWS S3 → wenn EC2, Lambda und andere AWS-Dienste ebenfalls genutzt werden
* Cloudflare R2 → wenn Egress-Kosten relevant sind oder Daten nahe bei CF Workers liegen sollen
* MinIO → für lokale Entwicklung oder self-hosted Infrastruktur
—-
===== Typische Workflows =====
==== Workflow: Backup-Job ====
S3GetObject
==== Workflow: Temporärer Download-Link ====
import std.cloud.aws.core;
import std.cloud.s3;
import std.alloc;
import std.fs;
import std.os;
fn DatenbankBackup(s3: S3Client, dbDumpPfad: pchar): int64 {
// Key mit Datum (YYYY-MM-DD)
// Datum als Unix-Sekunden — std.datetime ist hier nicht importierbar,
// siehe Kasten unten.
var jetzt: int64 := unix_time();
var key: int64 := alloc(128);
StrConcat(key as pchar, "backups/"c, IntToStr(jetzt));
StrConcat(key as pchar, key as pchar, "/db.dump.gz"c);
var sz: int64 := FileSize(dbDumpPfad);
if (sz == 0) { return 0; }
// > 100 MB → Multipart; kleinere Dumps direkt
var ok: int64;
if (sz > 104857600) {
var data: int64 := alloc(sz);
ReadFile(dbDumpPfad, data as pchar, sz);
ok := S3PutObjectMultipart(s3, "backups"c, key, data, sz,
"application/gzip"c, 50000000);
free(data, sz);
} else {
ok := S3PutObjectFile(s3, "backups"c, key, dbDumpPfad,
"application/gzip"c);
}
free(key, 128);
return ok;
}
==== Workflow: Alte Objekte aufräumen ====
fn DownloadLinkErzeugen(s3: S3Client, key: pchar, ablaufSek: int64): int64 {
var url: int64 := S3PresignGet(s3, "mein-bucket"c, key, ablaufSek);
if (url == 0) {
Print("Presign fehlgeschlagen: "c);
PrintLn(S3LastError(s3));
}
// Caller übernimmt Besitz — mit free() freigeben
return url;
}
==== Workflow: Datei-Synchronisation (Existenzprüfung vor Upload) ====
fn AlteLogsLoeschen(s3: S3Client, prefix: pchar, maxAlterTage: int64): int64 {
var list: int64 := S3ListAll(s3, "log-bucket"c, prefix);
if (list == 0) { return 0; }
var grenze: int64 := unix_time() - (maxAlterTage * 86400);
var grenzDatum: pchar := "2026-01-01"c; // im echten Code aus `grenze` gebildet
var geloescht: int64 := 0;
var i: int64 := 0;
while (i < S3ListCount(list)) {
var mtime: pchar := S3ListGetModified(list, i) as pchar;
// S3 liefert ISO-8601; ohne std.datetime hier nur die ersten
// zehn Zeichen als Datum vergleichen (YYYY-MM-DD sortiert lexikografisch).
if (StrCmp(mtime, grenzDatum) < 0) {
var key: pchar := S3ListGetKey(list, i) as pchar;
S3DeleteObject(s3, "log-bucket"c, key);
geloescht := geloescht + 1;
}
i := i + 1;
}
S3ListFree(list);
Print("Gelöscht: "c); PrintLn(IntToStr(geloescht));
return geloescht;
}
—-
===== Speicherverwaltung =====
Funktionen, die Daten aus S3 zurückgeben, allokieren Speicher. Der Aufrufer ist für die Freigabe verantwortlich:
^ Funktion ^ Rückgabe ^ Freigabe ^
| fn SyncDatei(s3: S3Client, localPath: pchar, bucket: pchar, key: pchar): int64 {
// Nur hochladen wenn Größe unterschiedlich (einfacher ETag-Vergleich fehlt hier)
if (S3ObjectExists(s3, bucket, key) == 1) {
var remoteSize: int64 := S3GetObjectSize(s3, bucket, key);
var localSize: int64 := FileSize(localPath);
if (remoteSize == localSize) {
PrintLn("Datei bereits aktuell"c);
return 1;
}
}
return S3PutObjectFile(s3, bucket, key, localPath, "application/octet-stream"c);
}
| allokierter Puffer | free(buf, size + 1) |
| S3GetObjectRange | allokierter Puffer | free(buf, rangeLen + 1) |
| S3PresignGet / Put / … | allokierter URL-String | free(url, StrLen+1) |
| S3LastError / S3LastErrorCode | interner Puffer (nicht freigeben!) | — |
| S3ListAll / S3ListObjects | Listing-Handle | S3ListFree(list) |
| S3GetETag | allokierter String | free(etag, len+1) |
| S3GetObjectSize | int64 (kein Speicher) | — |
Faustregel: Wenn die Funktion einen Puffer > 0'' zurückgibt, gehört er dem Aufrufer.
Letzte Aktualisierung: 2026-06-12