Architecture (contributors)

Design notes for contributors.

Purpose

Move WordPress media (the contents of wp-content/uploads) to S3-compatible object storage while:

  1. Keeping 1:1 object keys that mirror uploads paths
  2. Leaving attachment posts / _wp_attached_file valid
  3. Keeping public URLs on the site host by default: https://example.com/wp-content/uploads/...
  4. Sending new Media Library uploads to S3
  5. Staying provider-agnostic (AWS S3, Linode Object Storage, MinIO, Cloudflare R2, DigitalOcean Spaces, etc.)

Architecture

┌─────────────────┐     hooks      ┌──────────────┐     PutObject     ┌─────────────────┐
│ WordPress       │ ─────────────► │ Uploads      │ ─────────────────► │ S3-compatible   │
│ Media Library   │                │ Migrator     │                    │ bucket          │
└────────┬────────┘                └──────────────┘                    └────────▲────────┘
         │ URL: same-host or public_base_url                                     │
         ▼                                                                       │ GetObject
┌─────────────────┐     missing local file                          ┌───────────┴───────┐
│ Browser / CDN   │ ───────────────────────────────────────────────►│ Serve (PHP stream)│
│                 │   (or direct CDN when public_base_url set)      └───────────────────┘
└─────────────────┘
ComponentRole
Settings / HelpersConstants / env / options; pure key + URL helpers
S3_ClientAWS SDK wrapper with custom endpoint
UploadsNew uploads + intermediate sizes → S3
URL_FiltersSame-host contract or public_base_url rewrite
ServeStream (or redirect) from S3 when local missing
Migrator + WP-CLIBulk migrate / verify / doctor
Job_RunnerAdmin background migrate (WP-Cron + manual batch)
Media_UIList column, metabox, bulk actions
Content_RewriteOptional historic HTML rewrite (default off)
AdminSettings, presets, doctor, first-run, tabs

Object key layout

Default: key = path relative to the uploads directory.

Local pathObject key (prefix empty)
.../wp-content/uploads/2020/03/foo.jpg2020/03/foo.jpg
.../wp-content/uploads/2020/03/foo-150x150.jpg2020/03/foo-150x150.jpg

Optional WP_TO_S3_PREFIX (e.g. media) prepends a single path segment.

WordPress metadata stores 2020/03/foo.jpg in _wp_attached_file as usual — no database rewrite for attachment meta.

URL contract

Local files

SettingBehavior
delete_local=false (default)Dual-presence until you migrate and prune
delete_local=trueAfter successful put, unlink local; Serve streams later

Delete-local-later is also available per-attachment and via Media bulk actions when remote size matches.

New uploads + core derivatives

  1. Core writes file under uploads/ via wp_handle_upload
  2. Plugin PutObject for the original
  3. After wp_generate_attachment_metadata / wp_update_attachment_metadata, plugin uploads intermediate sizes + original_image
  4. Core image editor paths fire the same metadata hooks

Bulk migration

Non-goals (v1.0)