Zum Hauptinhalt springen

Object Storage — Cloudflare R2 / RustFS (Archiv / inaktiv)

Aktueller Stand (seit 2026-05-28, Plan 84): Production läuft auf lokalem Storage (PUBLIC_STORAGE_DRIVER=local). Files liegen physisch unter storage/app/public/ und werden über ein 2-Level-Sharding verteilt — siehe Local-Filesystem-Sharding.

Sowohl Cloudflare R2 (s3) als auch RustFS (rustfs) sind inaktiv und werden nicht mehr angesprochen. Beide Treiber bleiben im Code konfigurierbar (PUBLIC_STORAGE_DRIVER=s3 bzw. rustfs) als Rückkehr-Option und für Disaster-Recovery; die zugehörige RustFS-Monitoring-Seite (/admin/rustfs-monitoring) liegt im Admin-Bereich Archiv. Das Dokument unten bleibt als historische Referenz erhalten.

Postbox nutzte historisch einen S3-kompatiblen Object Storage für alle öffentlichen Medien (Profilbilder, Video-Thumbnails, OG-Images). Der Storage-Treiber ist env-gesteuert (PUBLIC_STORAGE_DRIVER) umschaltbar zwischen local (aktueller Production-Default), s3 (Cloudflare R2 — Legacy), dual-write (R2 + RustFS-Mirror, transient während der damaligen Migration) und rustfs (Self-Hosted S3, 2026-04-26 bis 2026-05-28 Production).

Wichtig (Legacy R2-Konfiguration): Falls R2 jemals als Fallback reaktiviert wird, müssen alle R2-Buckets ausschließlich EU-Jurisdiction verwenden (Data Residency). Der Endpoint enthält .eu. — ohne dieses Prefix antwortet R2 mit 403 Forbidden.

RustFS — Setup (Quickref, derzeit inaktiv)

Hinweis: Dieses Setup ist aktuell nicht aktiv (Production = local). Nur relevant, falls bewusst auf PUBLIC_STORAGE_DRIVER=rustfs zurückgewechselt wird.

AspektWert
TreiberPUBLIC_STORAGE_DRIVER=rustfs
Env-VarsRUSTFS_ACCESS_KEY_ID, RUSTFS_SECRET_ACCESS_KEY, RUSTFS_BUCKET, RUSTFS_ENDPOINT
CDNcdn.postbox.so (Cloudflare Proxy davor)
MonitoringOTel Prometheus Endpoint → Legacy-Seite /admin/rustfs-monitoring (im Admin-Archiv). Das aktive /admin/storage-monitoring zeigt local-Metriken (Disk/Inodes/Bild-Zähler).
Health-Endpoint/health (RustFS native, nicht MinIO-Pfad)

Die Storage-Driver-Registrierung (Storage::extend('rustfs', ...) und Storage::extend('dual-write', ...)) liegt in app/Providers/AppServiceProvider.php und bleibt für die Rückkehr-Option erhalten.

Cloudflare R2 — Legacy-Dokumentation

Die folgende Dokumentation beschreibt den ehemaligen R2-Setup. Sie bleibt erhalten als Referenz für Disaster-Recovery und falls R2 als Fallback reaktiviert wird.


Architektur

graph TD
A["Laravel App"] -->|"Storage::disk('public')"| B{PUBLIC_STORAGE_DRIVER}
B -->|"local"| C["storage/app/public/<br>Symlink: public/storage/"]
B -->|"s3"| D["Cloudflare R2<br>postbox-media Bucket<br>(EU-Jurisdiction)"]
B -->|"dual-write"| DW["DualWriteAdapter"]
DW -->|"Primary (Reads + Writes)"| D
DW -->|"Secondary (Writes only)"| RF["RustFS<br>(Self-Hosted S3)"]
B -->|"rustfs"| RF
D -->|"Custom Domain"| E["cdn.postbox.so"]
D -->|"storage:sync-r2-backup"| F["Backup Bucket<br>postbox-backup<br>(EU-Jurisdiction)"]
C -->|"storage:migrate-to-r2"| D
G["Cloudflare GraphQL API"] -->|"cloudflare:fetch-r2-metrics"| H["Cache + DB<br>(r2_metric_snapshots +<br>r2_metric_hourly_snapshots)"]
H -->|"Admin Dashboard"| I["/admin/cloudflare-management"]
H -->|"R2MetricsChart"| J["ApexCharts<br>Storage | Operations | Kosten"]
G -->|"cloudflare:backfill-r2-metrics"| H

R2 Buckets einrichten

Bucket 1: postbox-media (Primary)

  1. Cloudflare Dashboard → R2 Object Storage → Create bucket
  2. Name: postbox-media
  3. Location: EU (Jurisdiction)
  4. Storage Class: Standard

Custom Domain einrichten

  1. Bucket öffnen → SettingsPublic accessCustom Domains
  2. Connect Domain klicken → Domain eingeben (z.B. cdn.postbox.so)
  3. Cloudflare erstellt automatisch CNAME-Record + SSL

Voraussetzung: Die Domain muss als Zone im selben Cloudflare-Account liegen. Public Access wird automatisch aktiviert.

R2 API Token erstellen

  1. Cloudflare Dashboard → R2 → Manage R2 API TokensCreate API Token
  2. Permissions: Object Read & Write
  3. Scope: Auf postbox-media + postbox-backup Buckets beschränken
  4. Access Key ID + Secret Access Key notieren

Cloudflare API Token (Analytics)

Für das Admin Dashboard wird ein separater (oder erweiterter) API Token benötigt:

  1. Cloudflare Dashboard → My ProfileAPI TokensCreate Token
  2. Permissions:
    • Account Analytics: Read (für GraphQL R2 Metriken)
    • Account R2 Storage: Read (für REST /r2/metrics)
    • Billing: Read (optional, für Billing-Abgleich)
  3. Account ID notieren

CORS-Policy konfigurieren

  1. Bucket öffnen → SettingsCORS PolicyAdd/Edit CORS policy
  2. Folgende JSON-Konfiguration eintragen:
[
{
"AllowedOrigins": [
"https://app.postbox.so"
],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 86400
}
]
FeldWertBegründung
AllowedOriginshttps://app.postbox.soNur die App-Domain, kein Wildcard *
AllowedMethodsGET, HEADLese-Zugriff reicht — kein PUT/DELETE vom Browser
AllowedHeaders*Erlaubt alle Request-Headers (z.B. Range für Partial Requests)
MaxAgeSeconds86400Browser cacht Preflight-Response 24 Stunden

Sicherheitshinweis: Kein "AllowedOrigins": ["*"] verwenden — das würde externem JavaScript erlauben, Bucket-Inhalte per fetch() zu laden (Hotlinking via JS).


Bucket 2: postbox-backup (Backup)

  1. Create bucket → Name: postbox-backup
  2. Location: EU (gleiche Jurisdiction wie Primary)
  3. Kein Public Access — kein Custom Domain nötig
  4. Wird nur server-seitig über storage:sync-r2-backup befüllt
  5. Kann denselben API Token verwenden

.env Konfiguration

# R2 Primary Bucket (Medien-Storage: Profilbilder, Video-Thumbnails, OG-Images)
PUBLIC_STORAGE_DRIVER=s3
R2_ACCESS_KEY_ID=dein_access_key_id
R2_SECRET_ACCESS_KEY=dein_secret_access_key
R2_BUCKET=postbox-media
R2_ENDPOINT=https://ACCOUNT_ID.eu.r2.cloudflarestorage.com

# Öffentliche URL (Custom Domain des R2 Buckets — nur für URL-Generierung, nicht für S3-API)
PUBLIC_STORAGE_URL=https://cdn.postbox.so

# S3 URL-Schema: true = path-style (default, empfohlen für R2), false = virtual-hosted
# R2_USE_PATH_STYLE_ENDPOINT=true

# R2 Backup Bucket (Disaster Recovery, kein Public Access)
R2_BACKUP_ACCESS_KEY_ID=dein_access_key_id
R2_BACKUP_SECRET_ACCESS_KEY=dein_secret_access_key
R2_BACKUP_BUCKET=postbox-backup
R2_BACKUP_ENDPOINT=https://ACCOUNT_ID.eu.r2.cloudflarestorage.com

# Cloudflare API (für R2-Metriken im Admin Dashboard)
CLOUDFLARE_API_TOKEN=dein_api_token
CLOUDFLARE_ACCOUNT_ID=dein_account_id

# EU-Jurisdiction: 'eu' prefix wird für GraphQL-Queries an Bucket-Namen angehängt
# Ohne dieses Prefix liefert GraphQL keine Daten für EU-Jurisdiction Buckets
R2_JURISDICTION=eu

# --- RustFS (Self-Hosted S3-kompatibler Storage) ---
# Dual-Write: PUBLIC_STORAGE_DRIVER=dual-write (R2 + RustFS parallel, Reads von R2)
# RustFS-Only: PUBLIC_STORAGE_DRIVER=rustfs (nur RustFS, R2 wird nicht kontaktiert)

# RustFS S3-Credentials
RUSTFS_ACCESS_KEY_ID=dein_rustfs_access_key
RUSTFS_SECRET_ACCESS_KEY=dein_rustfs_secret_key
RUSTFS_BUCKET=postbox-media
RUSTFS_ENDPOINT=https://rustfs.dein-server.com
# AWS SDK Pflichtfeld, wird von Self-Hosted S3 (RustFS/MinIO) ignoriert
RUSTFS_REGION=us-east-1
RUSTFS_USE_PATH_STYLE_ENDPOINT=true

# Nur fuer dual-write: welcher Disk als Mirror-Ziel (default: rustfs)
# Bei rustfs-only nicht noetig — auskommentieren.
# STORAGE_MIRROR_DISK=rustfs

Die R2_ENDPOINT URL findet man unter: Cloudflare Dashboard → R2 → Bucket → SettingsS3 API.

WICHTIG — EU Data Residency: Alle Buckets verwenden ausschließlich EU-Jurisdiction. Der Endpoint enthält .eu. Subdomain:

JurisdictionEndpoint-Format
Automatic/Globalhttps://<account-id>.r2.cloudflarestorage.com
EU (verwendet)https://<account-id>.eu.r2.cloudflarestorage.com

Wenn der Endpoint ohne .eu. verwendet wird, antwortet R2 mit 403 Forbidden. Die GraphQL API benötigt zusätzlich den eu_ Prefix vor dem Bucket-Namen (automatisch durch R2_JURISDICTION=eu).


Filesystem-Konfiguration

Location: config/filesystems.php

Disks

DiskDriverZweck
publiclocal, s3, dual-write oder rustfs (env-gesteuert)Primary Storage fuer alle oeffentlichen Dateien
rustfss3 (fix)Self-Hosted S3 (RustFS), direkt oder als Mirror-Ziel fuer dual-write
local-publiclocal (fix)Nur fuer storage:migrate-to-r2 — liest immer vom lokalen Filesystem
r2-backups3 (fix)Backup-Bucket, nur fuer storage:sync-r2-backup

Storage-Driver Modi

PUBLIC_STORAGE_DRIVERLiest vonSchreibt aufCredentialsUse Case
localLokales FilesystemLokales FilesystemKeineEntwicklung
s3R2R2R2_* KeysR2-only Produktion
dual-writeR2R2 + RustFSR2_* + RUSTFS_*Parallelbetrieb (Migration)
rustfsRustFSRustFSRUSTFS_* KeysRustFS-only Produktion

Migrationspfad: s3dual-writerustfs

Dual-Write Modus

Der dual-write Driver (DualWriteFilesystemAdapter) ermoeglicht transparente Replikation zu einem zweiten S3-kompatiblen Backend (z.B. RustFS). Aktivierung per PUBLIC_STORAGE_DRIVER=dual-write.

Verhalten:

  • Reads: Nur von Primary (R2)
  • Writes (write, delete, move, copy, createDirectory, setVisibility): Gehen an beide
  • Fehler am Secondary: Werden geloggt (Log::warning), aber nicht propagiert — Primary ist autoritativ
  • Streams: writeStream() buffert den Stream-Inhalt, da ein Stream nur einmal gelesen werden kann

Location: app/Storage/DualWriteFilesystemAdapter.php, registriert in AppServiceProvider::boot()

RustFS-Only Modus

Der rustfs Driver nutzt die Credentials des rustfs Disks (RUSTFS_* Keys) und erbt die oeffentliche URL (PUBLIC_STORAGE_URL) vom public Disk. R2 wird nicht kontaktiert.

Aktivierung: PUBLIC_STORAGE_DRIVER=rustfs

Rollback: Jederzeit zurueck auf dual-write oder s3 moeglich — nur .env aendern + php artisan config:clear.

Location: Registriert in AppServiceProvider::boot()


Commands

CommandBeschreibungSchedule
cloudflare:fetch-r2-metricsR2-Metriken via Cloudflare GraphQL/REST API abrufen + Hourly SnapshotStuendlich
cloudflare:backfill-r2-metricsHistorische stuendliche R2-Snapshots aus Cloudflare GraphQL nachfuellen (bis 31 Tage)Manuell
storage:collect-metricsRustFS Prometheus-Metriken scrapen und Snapshot speichernAlle 15 Min
storage:purge-orphansVerwaiste Dateien ohne DB-Eintrag finden und loeschen (--dry-run, --prefix)Manuell
storage:reset-imagesAlle Profilbilder + Video-Thumbnails loeschen (DB + Storage). Re-Download beim naechsten Crawl.Manuell
storage:migrate-to-r2Lokale Dateien nach R2 migrierenManuell
storage:verify-r2R2-Integritaet gegen lokale Dateien pruefenManuell
storage:sync-r2-backupPrimary → Backup Bucket SyncSonntag 04:30

cloudflare:fetch-r2-metrics

Ersetzt den alten cloudflare:snapshot-metrics Command (entfernt). Statt 820K+ Objekte via S3 ListObjectsV2 zu scannen (~821 API-Calls, 5-10 Min), werden 3 leichtgewichtige API-Calls ausgeführt (~3-5 Sekunden):

SchrittAPIDaten
1. REST /r2/metricsAccount-Level Storage (Standard + Infrequent Access)~1s
2. GraphQL r2StorageAdaptiveGroupsPer-Bucket Storage Snapshot (Objects, Payload, Metadata)~1-3s
3. GraphQL r2OperationsAdaptiveGroupsOperations nach actionType (30d) mit Class A/B Mapping~1-3s

Zusätzlich:

  • Backup-Bucket Storage (GraphQL)
  • Anomalie-Erkennung (>20% Abweichung → Admin-Notification)
  • Täglicher DB-Snapshot in r2_metric_snapshots
  • Stündlicher DB-Snapshot in r2_metric_hourly_snapshots (für Charts)
  • Storage-Jump-Alert (E3): >5% Änderung innerhalb 1 Stunde → Admin-Notification
  • Heartbeat-Aufzeichnung (cron:heartbeat:r2_metrics)

cloudflare:backfill-r2-metrics

Füllt historische stündliche R2-Snapshots aus Cloudflare GraphQL nach (bis 31 Tage Retention). Nützlich nach Erstinstallation oder wenn stündliche Daten fehlen.

# Alle 31 Tage backfüllen (Standard)
php artisan cloudflare:backfill-r2-metrics

# Nur letzte 7 Tage
php artisan cloudflare:backfill-r2-metrics --days=7
  • Nutzt r2StorageAdaptiveGroups mit dimensions: [datetimeHour] für Per-Stunde-Daten
  • Upsert mit uniqueBy: [recorded_at, bucket_name] — Duplikate werden übersprungen
  • Operations-Daten (Class A/B) sind nicht über das Backfill verfügbar (nur Storage-Daten)

Vergleich alt vs. neu:

MetrikAlt (S3 Scan)Neu (API)
API-Calls pro Scan~8213-5
Laufzeit5-10 Min3-5 Sek
Memory-Peak~50-80 MB~2-5 MB
Schedule2x täglichStündlich

storage:collect-metrics

Scraped RustFS-Metriken vom OTel Prometheus Collector und prueft den Health-Endpoint. Speichert einen Snapshot mit ~20 Metriken in der DB. Leichtgewichtig (~2 HTTP-Calls, <2s).

# Metriken abrufen und speichern
php artisan storage:collect-metrics

# Nur anzeigen, nicht speichern
php artisan storage:collect-metrics --dry-run

Datenquellen:

QuelleEndpointAuthDaten
HealthGET /health (RUSTFS_ENDPOINT)Kein AuthStatus, Ready, IAM/Storage-Details, Version
MetricsGET /metrics (RUSTFS_METRICS_ENDPOINT)IP-AllowlistPrometheus-Text vom OTel Collector

Gescrapte Metriken (OTel Prometheus, Prefix testing_endpoint_rustfs_):

KategorieMetriken
Storagebucket_api_usage_bytes, bucket_api_objects_total (per Bucket)
Clustercluster_capacity_used_bytes, cluster_capacity_free_bytes, cluster_capacity_raw_total_bytes
Requestsapi_requests_total nach Methode (GET/PUT/DELETE/HEAD)
S3 Opss3_operations_total per Bucket (GetObject, PutObject, DeleteObject, HeadObject, ListObjectsV2)
Latenzio_latency_p95_ms, io_latency_p99_ms
Trafficrequest_body_bytes_total nach direction (request/response)
Prozessprocess_cpu_percent, process_memory_bytes, process_uptime_seconds
Diskdrive_runtime_state (online/offline), io_queue_utilization_percent

Schedule: Alle 15 Minuten, Heartbeat storage_metrics, Retention 1 Jahr (MassPrunable). ~4,6 MB/Jahr bei 15-Min-Intervall.

Feature-Gate: Sowohl der Command als auch der Heartbeat sind an den Public-Disk-Driver gekoppelt:

  • CollectStorageMetrics::handle() returnt frueh mit Info-Message wenn filesystems.disks.public.driver nicht rustfs oder dual-write ist (z.B. nach Switch auf Local-Storage). Kein cURL-Versuch gegen einen toten RustFS-Endpoint.
  • CronHeartbeatMonitorService::isFeatureEnabled() liefert fuer storage_metrics den Status not_required solange RustFS nicht aktiv ist — keine False-Positive-Failure-Alerts. Sobald PUBLIC_STORAGE_DRIVER=rustfs oder dual-write greift, ist der Heartbeat wieder pflicht. Analog zum r2_metrics-Heartbeat (gilt nur bei driver === 's3' mit endpoint).

Dashboard: /admin/storage-monitoring zeigt KPIs + ApexCharts mit 4 Tabs (Storage, Requests, Latenz, System) und 5 Zeitraeumen (1T/7T/30T/90T/1J). Tagesaggregate fuer 30d/90d/1y.

storage:purge-orphans

Scannt den S3/RustFS-Bucket nach verwaisten Dateien ohne passenden DB-Eintrag. Prueft drei Bereiche:

# Dry-Run: nur anzeigen, nichts loeschen
php artisan storage:purge-orphans --dry-run

# Alle Bereiche pruefen und loeschen
php artisan storage:purge-orphans

# Nur einen Bereich pruefen
php artisan storage:purge-orphans --prefix=social_profiles

# Verbose: einzelne Orphan-Pfade anzeigen
php artisan storage:purge-orphans --dry-run -v

Geprueft werden:

PrefixDB-TabelleGeprueft gegen
social_profiles/social_profile_imagespath, path_large, path_medium, path_thumbnail
youtube_videos/youtube_videosthumbnail_path, thumbnail_path_*
og-images/public_explorer_profilesog_image_path (nur publizierte, nicht geloeschte)

Schedule: Manuell (kein automatischer Schedule). Empfohlen nach grossen Cleanup-Aktionen.

storage:reset-images

Loescht alle Profilbilder und Video-Thumbnails aus DB und Storage (Clean Slate). Die Bilder werden beim naechsten regulaeren Crawling-Cycle automatisch neu heruntergeladen inkl. aller Varianten (WebP large/medium/thumbnail).

# Dry-Run: zeigt was geloescht wuerde
php artisan storage:reset-images --dry-run

# Alles loeschen (fragt nochmal nach)
php artisan storage:reset-images

# Nur Profilbilder oder nur Video-Thumbnails
php artisan storage:reset-images --only=profiles
php artisan storage:reset-images --only=videos

Was geloescht wird:

BereichDB-AktionStorage-Aktion
ProfilbilderAlle social_profile_images-Eintraege loeschensocial_profiles/ komplett leeren
Video-Thumbnailsthumbnail_path/hash/path_medium/path_thumbnail auf NULLyoutube_videos/ komplett leeren

Was erhalten bleibt: thumbnail_url auf social_profiles und youtube_videos (externe Quell-URL). OG-Images bleiben ebenfalls erhalten.

Automatisches Nachladen:

  • Instagram: Collector erkennt "kein Bild in DB" → laedt beim naechsten Crawl neu
  • YouTube: Sync erkennt thumbnail_hash = null → laedt beim naechsten Sync neu
  • images:refresh-missing (taeglich 05:00 UTC) → findet Profile ohne Bilder → laedt nach
  • OG-Images: Werden erst wieder generiert wenn ein lokales Profilbild existiert (Guard)

CloudflareR2Service

Location: app/Services/Cloudflare/CloudflareR2Service.php

Service für R2-Bucket-Metriken im Admin Dashboard. Nutzt Cloudflare GraphQL + REST APIs.

API-Methoden

MethodReturnBeschreibung
fetchBucketStorage(bucket)array|nullPer-Bucket Storage via GraphQL (Objects, Size, Metadata)
fetchOperationMetrics(bucket, days)array|nullOperations nach actionType via GraphQL mit Class A/B Mapping
fetchAccountMetrics()array|nullAccount-Level Storage via REST (Standard + Infrequent Access)
fetchBackupBucketStorage()array|nullBackup-Bucket Storage via GraphQL
fetchHourlyStorageData(bucket, days)array|nullPer-Stunde Storage via GraphQL (für Backfill)
calculateMonthlyCost(storage, ops)arrayKosten-Berechnung mit echten Class A/B Daten

Cache-Accessors (für Dashboard)

MethodReturnBeschreibung
getBucketSizeMb()float|nullBucket-Größe in MB (1h Cache)
getObjectCount()int|nullAnzahl Objekte (1h Cache)
getOperations()array|nullOperations-Daten mit Class A/B
getCostEstimate()array|nullKosten-Schätzung
getAnomalies()arrayErkannte Anomalien

Health & Config

MethodReturnBeschreibung
isR2Active()boolPrüft ob R2 konfiguriert ist
isHealthy()boolR2-Erreichbarkeit prüfen
getLatencyMs()floatR2-Latenz messen
hasApiCredentials()boolCloudflare API Token + Account ID vorhanden
getBackupStatus()arrayBackup-Bucket Health + letzter Sync

Anomalie-Erkennung (E2)

MethodReturnBeschreibung
detectAnomalies(bucket, storage, ops)arrayVergleich mit vorherigem Tag, Warnung bei >20% Abweichung

Prüft: Object Count, Storage-Größe, Total Requests. Bei Anomalien wird eine Admin-Notification gesendet.

Storage-Prognose (E5)

MethodReturnBeschreibung
getStorageForecast(bucket, months)array|nullLineare Regression auf 30 Tage DB-Snapshots

Gibt zurück: aktuell GB, prognostiziert GB, tägliches Wachstum, prognostizierte Kosten.

Billing-Vergleich (E4)

MethodReturnBeschreibung
getBillingComparison(bucket)array|nullMonatsvergleich (aktuell vs. Vormonat), Warnung bei >10% Abweichung

Class A/B Operation Mapping

Operations werden nach Cloudflare R2 Pricing Docs klassifiziert:

KlasseOperationsPreis
Class A (mutating)PutObject, CopyObject, ListObjects, DeleteObject etc.$4.50/1M
Class B (read-only)GetObject, HeadObject, HeadBucket etc.$0.36/1M

Admin Dashboard

Route: /admin/cloudflare-management Component: App\Livewire\Admin\CloudflareManagement\Index

Widgets

WidgetDatenquelleBeschreibung
KPI-BoxenCacheHealth, Latenz, Storage, Objects, Kosten, Bucket, CDN URL
Operations-BreakdownCache (GraphQL)Tabelle: actionType, Requests, Class A/B
Kosten-BreakdownCacheStorage, Class A, Class B, Egress, Total
Monatsvergleich (E4)DBKostenvergleich aktueller vs. Vormonat
Storage-Prognose (E5)DBLineare Projektion: 6 Monate, GB + Kosten
R2 Verlauf (Chart)DB (Hourly + Daily)Interaktiver ApexCharts-Chart mit Tabs: Storage, Operations, Kosten
Backup-Bucket (E3)Cache + GraphQLHealth, Sync-Status, Objects, Größe
Account StorageCache (REST)Standard + Infrequent Access über alle Buckets
Anomalie-Alert (E2)CacheWarnbanner bei >20% Abweichung
KonfigurationConfigENV-Werte (Driver, URL, Bucket, Token, Jurisdiction)

Aktualisieren-Button

Der Button triggert cloudflare:fetch-r2-metrics --force als detachierten OS-Prozess. Auto-Polling alle 5 Sekunden bis der Abruf abgeschlossen ist. Timeout: Nach 60 Sekunden ohne neuen Snapshot wird ein Fehler-Toast angezeigt und das Polling beendet.

Error-Reporting

Alle API-Fehler (GraphQL + REST) werden via report() an Flare/Nightwatch gemeldet. Das Command hat einen Top-level try/catch der auch unerwartete Exceptions abfängt und meldet. Logs landen in storage/logs/laravel.log (Log-Facade) und storage/logs/r2-metrics.log (Console-Output bei manueller Aktualisierung via exec).

R2 Verlauf-Chart (R2MetricsChart)

Component: App\Livewire\Admin\CloudflareManagement\R2MetricsChart

Interaktiver Multi-Tab Chart mit ApexCharts (CDN) und Alpine.js x-data + wire:ignore Pattern. Ersetzt die früheren statischen Storage-Trend und Request-Trend Tabellen.

Tabs

TabDatenChart-Typ
StorageStandard Storage (GB) + Infrequent Access (E9)Line
OperationsClass A + Class B Requests (Delta-Berechnung, E1)Stacked Bar
KostenGeschätzte Kosten ($/Monat)Line

Zeiträume

ZeitraumDatenquelleGranularität
1 Tagr2_metric_hourly_snapshotsStündlich
7 Tager2_metric_hourly_snapshotsStündlich
30 Tager2_metric_snapshotsTäglich
1 Jahrr2_metric_snapshotsTäglich

Features

  • E1 — Operations-Delta: Cloudflare-Operations sind kumulative 30d-Werte. Der Chart berechnet Deltas zwischen aufeinanderfolgenden Snapshots, um Operations pro Stunde/Tag darzustellen. Negative Deltas (durch 30d-Fenster-Verschiebung) werden auf 0 geclampt.
  • E2 — Kosten-Prognose: Für 30d und 1y Ansichten wird eine gestrichelte Prognoselinie via linearer Regression berechnet (30% der Datenpunkte in die Zukunft projiziert).
  • E9 — Infrequent Access: Im Storage-Tab wird eine zusätzliche IA-Serie angezeigt, wenn IA-Daten vorhanden sind (nur in stündlichen Ansichten).

DB-Tabelle: r2_metric_snapshots

Tägliche Snapshots für Langzeit-Trends (GraphQL hat nur 31 Tage Retention).

SpalteTypBeschreibung
dateDATETag des Snapshots
bucket_nameVARCHAR(100)z.B. "postbox-media", "postbox-backup"
object_countBIGINTAnzahl Objekte
payload_size_bytesBIGINTGesamtgröße der Objekte
metadata_size_bytesBIGINTGesamtgröße der Metadaten
class_a_requestsBIGINTClass A Requests (Tag)
class_b_requestsBIGINTClass B Requests (Tag)
total_requestsBIGINTGesamt-Requests (Tag)
operations_by_typeJSONBDetail nach actionType
estimated_cost_usdDECIMAL(8,4)Berechnete Tageskosten

Retention: 10 Jahre (via MassPrunable). ~146 KB/Jahr pro Bucket.


DB-Tabelle: r2_metric_hourly_snapshots

Stündliche Snapshots für hochauflösende Charts (1d/7d Ansichten). Werden bei jedem cloudflare:fetch-r2-metrics Lauf geschrieben.

SpalteTypBeschreibung
recorded_atTIMESTAMPTZStunde des Snapshots (startOfHour)
bucket_nameVARCHAR(100)z.B. "postbox-media", "postbox-backup"
object_countBIGINTAnzahl Objekte
payload_size_bytesBIGINTGesamtgröße der Objekte
metadata_size_bytesBIGINTGesamtgröße der Metadaten
upload_countBIGINTUpload-Zähler
ia_object_countBIGINTInfrequent Access Objekte (E9)
ia_payload_size_bytesBIGINTInfrequent Access Größe (E9)
class_a_requestsBIGINTClass A Requests (kumulativ 30d)
class_b_requestsBIGINTClass B Requests (kumulativ 30d)
total_requestsBIGINTGesamt-Requests (kumulativ 30d)
estimated_cost_usdDECIMAL(8,4)Berechnete Kosten zum Zeitpunkt

Unique Constraint: [recorded_at, bucket_name] — verhindert doppelte Einträge per Upsert.

Retention: 1 Jahr (via MassPrunable).

Pruning Schedule:

TabellePrune-ZeitpunktRetention
r2_metric_hourly_snapshotsTäglich 03:25 UTC1 Jahr
r2_metric_snapshotsTäglich 03:30 UTC10 Jahre

R2 Pricing (Stand 2025)

KomponentePreisFreibetrag
Storage$0.015/GB/Monat10 GB
Class A (PUT, POST, LIST)$4.50/1M Requests1M
Class B (GET, HEAD)$0.36/1M Requests10M
Egresskostenlosunbegrenzt

Vergleich: Lokal vs. R2

AspektLokal (Dev)R2 (Produktion)
ConfigPUBLIC_STORAGE_DRIVER=localPUBLIC_STORAGE_DRIVER=s3
Pfad-Basisstorage/app/public/R2 Bucket via S3-API
URL-BasisAPP_URL/storage/cdn.postbox.so/
Migrationstorage:migrate-to-r2
BackupManuellstorage:sync-r2-backup (wöchentlich)
Cache HeadersDefaultImmutable (max-age=31536000)
CDNKeinCloudflare (Custom Domain)
MonitoringFilesystemGraphQL/REST API + Admin Dashboard