=== Defyn Media Offload ===
Contributors: defyndigital
Tags: media offload, s3, cdn, cloud storage, digitalocean spaces
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.1
Stable tag: 1.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Offload WordPress media to Amazon S3, DigitalOcean Spaces, Wasabi, Cloudflare R2 or MinIO, serve images from a CDN, and reclaim server disk space.

== Description ==

Defyn Media Offload moves your WordPress media library to S3-compatible object storage and serves every image, video and document from a fast content delivery network (CDN), then optionally deletes the local copies so your site fits within strict host disk and storage limits.

It works with Amazon S3, DigitalOcean Spaces, Cloudflare R2, Wasabi, Backblaze B2, Google Cloud Storage (in S3 mode), MinIO and any other S3-compatible object store. Offloading your uploads to cloud storage frees up server disk space, shrinks your backups, lowers bandwidth costs, and lets a global CDN deliver your images faster.

It was built for a large WooCommerce and multi-vendor marketplace on managed WordPress (where Jetpack's image CDN virtualises some thumbnail sizes), so it is hardened against the edge cases that break naive media offload plugins.

**Key features**

* Automatic offload on upload, after all thumbnail sizes are generated, including front-end (vendor) uploads.
* Serves product images, galleries, variation images, responsive `srcset`, REST API URLs and content-embedded images from the CDN.
* Resumable, cursor-based bulk migration for very large libraries (survives dropped SSH sessions). WP-CLI is the supported path; a browser-driven runner and Action Scheduler background runner are also provided.
* Optional, decoupled "remove local" pass so migration and deletion stay safe and reversible.
* Optional mirror-delete: removing an attachment removes its objects from the store.
* Handles Jetpack "virtual" thumbnail sizes (skip-and-log, never stalls).
* WooCommerce downloadable/protected files are stored privately and served with time-limited signed URLs, never made public-read.
* Sends a long-lived, configurable `Cache-Control` header with public objects for efficient CDN and browser caching.
* Optional hourly auto-retry (WP-Cron) that offloads any pending or previously failed uploads in the background.
* Keeps the WordPress image editor working after local copies are removed: originals are fetched back from the store on demand for crop, rotate and thumbnail regeneration, then re-offloaded.
* Storage dashboard showing how much is stored in object storage and how much local disk has been reclaimed.
* Works with the AWS SDK for PHP v3, namespace-isolated with php-scoper so it never conflicts with other plugins' bundled AWS SDK.

**Configuration**

For best security, define credentials as constants in `wp-config.php`:

`define( 'OSMO_KEY',      'your-access-key' );`
`define( 'OSMO_SECRET',   'your-secret-key' );`
`define( 'OSMO_BUCKET',   'your-bucket' );`
`define( 'OSMO_REGION',   'syd1' );`
`define( 'OSMO_ENDPOINT', 'https://syd1.digitaloceanspaces.com' );`
`define( 'OSMO_CDN',      'https://cdn.example.com' );`

Otherwise enter them on **Media → Media Offload**.

== Installation ==

1. Build the bundled AWS SDK once (developer step): run `composer install` then `bash build.sh` in the plugin directory. Release ZIPs already include the scoped `vendor/` directory.
2. Upload the plugin to `wp-content/plugins/` and activate it.
3. Go to **Media → Media Offload**, enter your credentials (or define the `OSMO_*` constants), and click **Test connection**.
4. Run the bulk migration: `wp osmo offload --all`, verify with `wp osmo verify`, then optionally `wp osmo remove-local --all`.

== Frequently Asked Questions ==

= Does it work with DigitalOcean Spaces? =
Yes. Set the region (e.g. `syd1`), the endpoint (`https://syd1.digitaloceanspaces.com`) and your CDN base. Any S3-compatible store works.

= Will it break Jetpack / WordPress.com image handling? =
Turn off Jetpack's Site Accelerator (image CDN) so real thumbnail files exist to offload. The plugin also detects Jetpack "virtual" sizes and skips them rather than stalling. Only this plugin rewrites image URLs.

= Is removing local files reversible? =
The objects remain in your store, and `wp osmo` / the admin can re-download them. Keep backups until you are confident.

= What about WooCommerce downloadable products? =
Protected download files are stored with a private ACL and are never exposed public-read. When "Private files via signed URLs" is enabled, direct links to those files are served as time-limited signed URLs (default 5 minutes, configurable); otherwise they continue to serve locally through WooCommerce.

= Can it keep caching headers for the CDN? =
Yes. Public objects are uploaded with a `Cache-Control` header. The max-age is configurable on the settings page (default one year) and applies to newly offloaded files.

= Can I edit or crop images after removing the local copies? =
Yes. When WordPress needs the source file for cropping, rotating or regenerating thumbnails and the local copy has been removed, the plugin downloads it back from the store automatically, then re-offloads the new sizes.

== External services ==

This plugin connects to one external service: the S3-compatible object storage
endpoint that **you** configure (for example DigitalOcean Spaces, Amazon S3,
Wasabi, or a self-hosted MinIO server). It is used to store and serve your media
files.

* What is sent, and when: when you upload media (or run a migration), the media
  files and their object keys (the uploads-relative path, e.g.
  `2026/06/image.jpg`) are uploaded to the bucket you configured, using the
  access key and secret you provide. URL rewriting and verification read object
  keys back from the same bucket.
* The plugin does not contact any other server. It does not collect analytics,
  telemetry, or usage data, and it never "phones home" to the author.
* You must supply your own credentials and accept the terms and privacy policy
  of whichever storage provider you choose. DigitalOcean Spaces:
  https://www.digitalocean.com/legal/terms-of-service-agreement /
  https://www.digitalocean.com/legal/privacy-policy . Amazon S3:
  https://aws.amazon.com/service-terms/ / https://aws.amazon.com/privacy/ .

== Third-party libraries ==

This plugin bundles the AWS SDK for PHP v3 (Apache License 2.0, GPL-compatible)
and its dependencies (Guzzle, PSR interfaces, JMESPath, Symfony polyfills). The
SDK is namespace-isolated under `OSMO\Vendor\` using php-scoper so it never
conflicts with an AWS SDK bundled by another plugin. This is namespacing, not
obfuscation, and the bundled PHP remains fully human-readable.

The full source code and the build tooling (composer.json, scoper.inc.php,
build.sh, and the post-scope patch helpers in bin/) are available so the bundled
`vendor/` directory can be reproduced from scratch with `bash build.sh`.

== Screenshots ==

1. Settings and live offload stats.
2. Bulk migration runner.
3. Media library offload column.

== Changelog ==

= 1.1.0 =
* New: WooCommerce/private files are now served with time-limited signed URLs (configurable lifetime), so they stay protected even after local copies are removed.
* New: configurable `Cache-Control` max-age header on public objects for CDN and browser caching.
* New: optional hourly auto-retry via WP-Cron that offloads pending or previously failed uploads.
* New: storage dashboard showing bytes stored in object storage and local disk reclaimed, in the settings page, Dashboard widget, and `wp osmo status`.
* Fix: the WordPress image editor (crop, rotate, scale, regenerate thumbnails) now works after local copies have been removed; the source file is fetched back from the store on demand and the new sizes are re-offloaded.

= 1.0.0 =
* Initial release.

== Upgrade Notice ==

= 1.1.0 =
Adds signed-URL delivery for private files, Cache-Control headers, an hourly auto-retry, storage stats, and fixes image editing after local copies are removed.

= 1.0.0 =
Initial release.
