Skip to content

Object Storage (MinIO / Wasabi / S3)

AZSuite stores most user-uploaded files (photos, PDFs, scans, flight tracks, engine-monitor dumps) in S3-compatible object storage rather than the filesystem. This page explains how the storage layer works, how to add or swap providers, and what gets stored where.

Supported providers

The ObjectStorageManager.php class talks to any S3-compatible API. Tested in production with:

  • MinIO — self-hosted, on-premises
  • Wasabi — hot-cloud S3, no egress fees
  • AWS S3 — the original
  • Backblaze B2 — works but not extensively tested

The class makes its own AWS Signature V4 requests (no SDK dependency), which keeps the deployment lightweight.

Per-purpose configuration

Storage is configured per purpose in the object_storage_configs table. Each row defines:

  • config_id — primary key
  • config_name — human-readable label
  • storage_typeminio / wasabi / s3_compatible
  • endpoint_urlhttps://s3.us-central-1.wasabisys.com, http://192.168.1.35:9000, etc.
  • access_key / secret_key — encrypted in the database
  • bucket_name — the bucket to write to
  • bucket_purpose — which AZSuite feature uses it (see below)
  • region — for SigV4 signing (us-east-1, us-central-1, etc.)
  • use_ssl / use_path_style — flags
  • is_active — only one row per purpose should be active at a time
  • is_validated — set after the connection-test passes

Purposes:

Purpose What's stored
logbook_photos Scan/OCR uploads, logbook page images, signed entries
aircraft_documents Aircraft manuals, weight & balance reports, registration
aircraft_photos Aircraft photo gallery
aircraft_monitor Engine-monitor dumps awaiting parse
engine_data Parsed engine-monitor data archive
adsb_tracks Flight track files (KML/GPX/CSV)
ocr_cropped Cropped regions from scan_define for OCR
panel_designs Panel designer projects
experimental Experimental builder media
work_event_archive Work order finished-job archives
custom Anything else / future use

How a feature picks the right bucket

Code calls getConfigByPurpose('logbook_photos') and gets back the active config row. The query is:

SELECT * FROM object_storage_configs
WHERE bucket_purpose = ? AND is_active = 1
LIMIT 1

So only one config can be active per purpose at a time. This makes provider migrations a one-row UPDATE.

Managing configs (admin UI)

Admins can view, edit, and add storage configs through the AZSuite admin UI (typically under settings). The form lets you:

  • Add a new config row (filled with defaults)
  • Edit endpoint, region, bucket name, keys
  • Test the connection — issues a HEAD bucket against the endpoint with signed credentials, returns success/fail
  • Toggle is_active (with care — see provider migration below)

Provider migration

The typical migration story: replace MinIO with Wasabi for one purpose while keeping production traffic flowing.

The migration tool lives at scripts/migrate_bucket.php:

php scripts/migrate_bucket.php --from=3 --to=11

What it does:

  1. Lists every object in the source config's bucket (paginated)
  2. Downloads each to a tempfile
  3. Uploads to the destination config's bucket at the same key
  4. Verifies the destination's reported size matches the source
  5. Logs progress + per-object failures to /tmp/migrate_bucket_<ts>.log

Flags:

  • --prefix=path/ — only migrate keys with that prefix (good for staged migrations)
  • --dry-run — list what would be copied
  • --skip-existing — resume after partial run; skips destination keys that already exist with matching size
  • --limit=N — stop after N objects (testing)
  • --verbose — log every object instead of every 25th

Source is never modified — non-destructive copy only.

Once migration completes, the cutover is two SQL statements in a transaction:

START TRANSACTION;
UPDATE object_storage_configs SET is_active = 0 WHERE config_id = 3;   -- old
UPDATE object_storage_configs SET is_active = 1 WHERE config_id = 11;  -- new
COMMIT;

After the cutover, the old config can stay around in is_active=0 state for historical reference (or be deleted entirely).

Key encoding

Object keys can contain spaces, parentheses, plus-signs, etc. — common with human-named PDFs like "Beechcraft-SB 2147.pdf". The storage layer URL-encodes each path segment when building the request URL and the SigV4 canonical URI (both must use the same encoded form, or signature verification fails).

This was a real bug fixed during the Wasabi migration — early implementations didn't URL-encode, which broke any key with a space.

Security

  • Secret keys are encrypted at rest in the database (custom encryption with a key derived from app config)
  • Validation/connection tests don't log the secret
  • The useSSL=true flag should always be set in production; only disable for local MinIO over HTTP for dev
  • For Wasabi (real public CA certs), CURLOPT_SSL_VERIFYPEER should be on — there's a planned audit fix to make this conditional per provider

Performance characteristics

Operation MinIO (LAN) Wasabi (cloud)
Single PUT, 1 MB ~50 ms ~300 ms
Single GET, 1 MB ~30 ms ~200 ms
LIST 1000 objects ~50 ms ~500 ms
DELETE ~30 ms ~100 ms

LAN MinIO will always be faster than cloud Wasabi for individual operations — it's the latency of the WAN round-trip. Both are fine for AZSuite's scale.

Costs

MinIO is free (you pay for the hardware / VPS). Wasabi is ~$6/TB/month flat with no egress fees and no per-request fees. AWS S3 is variable depending on region and storage class.

Migration recommendations

If you're starting fresh, Wasabi is the easiest choice — no infrastructure to maintain, predictable pricing, full S3 compatibility.

MinIO makes sense when:

  • You have low-bandwidth uplinks (LAN access is faster)
  • You need data sovereignty (everything stays on your premises)
  • You already have NAS / server hardware you want to use

A common hybrid: MinIO for the frequently-accessed reference library (read-heavy, on the LAN) + Wasabi for user-generated content (need cloud durability + multi-tenant access).