S3-Guide — Objekt-Speicher mit std.cloud.s3

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.s3std.cloud.aws.transportstd.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 nehmenunix_time(), DatetimeFromUnixSeconds, DatetimeToUnixSeconds. Wer std.datetime braucht, trennt den Code in zwei Units. Nachgemessen mit 1.1.0A. </WRAP>


Konzept: Buckets und Objekte

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).


Verbindung herstellen

AWS S3

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.

Cloudflare R2 (kein Egress-Preis)

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.

MinIO (lokal / self-hosted)

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"

LocalStack (AWS-Emulation für Tests)

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

Custom Endpoint (andere S3-kompatible Dienste)

// 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
);


Basis-Operationen

Hochladen

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 .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

Herunterladen

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);
    }
}

Teilweise herunterladen (Range-Download)

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

Löschen

// 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);

Metadaten ohne Download (HEAD)

// 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));

Kopieren (server-seitig)

// Kopiert ohne lokalen Download — S3 macht alles serverseitig
S3CopyObject(s3, "quell-bucket"c, "original.pdf"c,
                 "ziel-bucket"c,  "archiv/2026/original.pdf"c);


Listing und Pagination

S3 liefert maximal 1000 Objekte pro Request. Bei größeren Buckets muss paginiert werden.

Einfaches Listing (bis 1000 Objekte)

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);
}

Vollständiges Listing (automatisch paginiert)

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);
}

Manuelle Pagination (für Streaming-Verarbeitung)

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);
    }
}

Verzeichnis-Simulation mit Delimiter

S3 hat keine echten Verzeichnisse, aber mit delimiter=/' kann man Prefixes wie Ordner behandeln:

// 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);
—- ===== Multipart-Upload ===== Für Dateien über 5 GB ist Multipart-Upload Pflicht. Unter 100 MB ist S3PutObject / 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) ====
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;
}
==== Manueller Multipart (für Streaming / eigene Steuerung) ====
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);
}
Wichtig: Nicht abgeschlossene Multipart-Uploads verursachen Speicherkosten! Immer
S3MultipartAbort 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.
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);
    }
}
Ablauf „Browser-Upload direkt zu S3“:
Browser                 Eigener Server          S3
   │                         │                  │
   │── POST /upload-token ──►│                  │
   │                         │── S3PresignPut ─►│
   │                         │◄── Presigned URL ─┘
   │◄── Upload-URL ──────────┤                  │
   │                                            │
   │────── PUT {presigned-url} (Datei) ────────►│
   │◄───── 200 OK ───────────────────────────────┘
Der Server bekommt die Datei nie zu sehen — sie landet direkt in S3. Spart Bandbreite und Serverkosten. Ablauf „temporärer Download-Link“:
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.
—- ===== Bucket-Verwaltung =====
// 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);
—- ===== Fehlerbehandlung ===== S3-Funktionen geben bei Fehler
0 zurück (bei int64-Ergebnissen). Den HTTP-Statuscode und Fehlertext danach abfragen:
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);
    }
}
Häufige Fehlercodes: ^ HTTP ^ S3-Fehlercode ^ Bedeutung ^ Lösung ^ | 403 |
AccessDenied | 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 ====
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: Temporärer Download-Link ====
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: Alte Objekte aufräumen ====
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;
}
==== Workflow: Datei-Synchronisation (Existenzprüfung vor Upload) ====
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);
}
—- ===== Speicherverwaltung ===== Funktionen, die Daten aus S3 zurückgeben, allokieren Speicher. Der Aufrufer ist für die Freigabe verantwortlich: ^ Funktion ^ Rückgabe ^ Freigabe ^ |
S3GetObject | 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.


Weiterführend