Architecture (contributors)
Design notes for contributors.
Purpose
Move WordPress media (the contents of wp-content/uploads) to S3-compatible object storage while:
- Keeping 1:1 object keys that mirror uploads paths
- Leaving attachment posts /
_wp_attached_filevalid - Keeping public URLs on the site host by default:
https://example.com/wp-content/uploads/... - Sending new Media Library uploads to S3
- 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) └───────────────────┘
└─────────────────┘
| Component | Role |
|---|---|
Settings / Helpers | Constants / env / options; pure key + URL helpers |
S3_Client | AWS SDK wrapper with custom endpoint |
Uploads | New uploads + intermediate sizes → S3 |
URL_Filters | Same-host contract or public_base_url rewrite |
Serve | Stream (or redirect) from S3 when local missing |
Migrator + WP-CLI | Bulk migrate / verify / doctor |
Job_Runner | Admin background migrate (WP-Cron + manual batch) |
Media_UI | List column, metabox, bulk actions |
Content_Rewrite | Optional historic HTML rewrite (default off) |
Admin | Settings, presets, doctor, first-run, tabs |
Object key layout
Default: key = path relative to the uploads directory.
| Local path | Object key (prefix empty) |
|---|---|
.../wp-content/uploads/2020/03/foo.jpg | 2020/03/foo.jpg |
.../wp-content/uploads/2020/03/foo-150x150.jpg | 2020/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
- Default: site-origin upload URLs;
Servestreams when local file absent. - Optional:
public_base_urlrewrites attachment URLs for CDN/public hosting. - Optional: historic post content rewrite (toggle + dry-run; default off).
Local files
| Setting | Behavior |
|---|---|
delete_local=false (default) | Dual-presence until you migrate and prune |
delete_local=true | After 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
- Core writes file under
uploads/viawp_handle_upload - Plugin
PutObjectfor the original - After
wp_generate_attachment_metadata/wp_update_attachment_metadata, plugin uploads intermediate sizes +original_image - Core image editor paths fire the same metadata hooks
Bulk migration
- Admin:
Job_Runnerbatches files, progress UI, pause/resume/cancel, error policy - CLI:
wp s3 migratewith PHP engine (default concurrency 3) or optional s5cmd - Resume: skip when remote size matches local
- Errors: auth/config → halt; 429/5xx → backoff retry; bad local → skip+log
Non-goals (v1.0)
- Multisite
- Backup-only / mirror-only mode without live offload
- Third-party regenerate-thumbnail plugin integration
- First-party telemetry
- Automatic post content rewrite without explicit Tools action