== Changelog ==

Older releases of OmniOffloader. The latest are in readme.txt; the full list is at https://omnioffloader.vercel.app/changelog/

= 0.1.5 =

**Fixes**

* Media that is only in the cloud (after Smart Local Cleanup or Full Cloud Migration) now shows while you edit a post, in both the block editor and the classic editor, including video and audio.
* Saving a post keeps your site's own media addresses in the content, so it keeps working after a Bring Back or a CDN domain change.

= 0.1.4 =

**Serve Media from Cloud**

* New setting in **Settings → Retention Policy**: with **Retain Local Files**, turn off **Serve Media from Cloud** to keep offloading media while your site still loads it from your own server, for example while you set up your CDN. Turn it on and every offloaded file switches to your cloud storage or CDN at once, with no re-upload.
* Smart Local Cleanup and Full Cloud Migration delete local files, so with those policies media always loads from the cloud. Files an earlier policy already removed keep loading from the cloud until they are downloaded again, so turning serving off never breaks an image.

**Move media to the current folder**

* You can change the Path Prefix while media is offloaded. Settings explains that existing media stays in its folder and keeps working, and **Save changes** asks you to confirm. Once saved, Settings shows how many attachments are outside the current folder and how to move them: `wp omnioffloader move --all` (recommended, with copy buttons and a background version for large libraries) or **Move media in Tools**.
* New `wp omnioffloader move`: moves media offloaded under an earlier Path Prefix into the current folder. Within the same bucket your storage provider copies the files itself, so nothing is downloaded and your server's files are not touched; old copies are deleted once the new ones are verified. Files that can't be copied that way (over 5 GB, or in another bucket) go through the server. Supports `--dry-run`, `--ids`, `--limit`, `--keep-old` and `--concurrency` (up to 64 files at once).
* New **Tools → Move Media to the Current Folder**: the same move from the admin screen, with a progress bar and Cancel. Shown only when some media is outside the current folder.
* The OmniOffloader box on an attachment's edit screen shows its bucket **Folder**, marked *Not in the current folder* when needed. `wp omnioffloader status` shows the current folder and how many offloaded attachments are in other folders.

**WP-CLI: faster offloads**

* New `wp omnioffloader offload --concurrency=<1–64>`: upload up to 64 files at the same time for that run, instead of the Transfer Speed setting (at most 10, which the admin screens keep). For a VPS or dedicated server offloading a large library.
* `restore` and `retention` take `--concurrency` too. A stricter retention policy now checks a group of attachments in cloud storage at once (Transfer Speed on the Tools screen, `--concurrency` in WP-CLI), so it is much faster on large libraries; every file is still verified by size before deletion.
* **Tools → Apply Retention Policy** links to its WP-CLI command, like Offload and Bring Back.

**WP-CLI: stop, status and long runs**

* Long runs started with `nohup wp omnioffloader … > file.log 2>&1 &` keep going after you close the terminal and write a progress line every 25 attachments to the log. A run without a terminal that would ask for confirmation stops with a message asking for `--yes`, instead of waiting for an answer.
* New `wp omnioffloader stop` ends the running job (offload, Bring Back, retention or move) from any terminal. A WP-CLI run finishes the file it is transferring first; a job started from the admin screen is cancelled.
* `wp omnioffloader status` starts with what is running now: progress, start time, speed, time left and expected finish, files transferred at once, log file and how to stop it. When nothing runs, it shows how the last WP-CLI job ended and the command to offload media that is still local.
* Long runs write to the database far less often: locks are renewed every 30 seconds and progress is saved at most every 5 seconds, instead of several writes per attachment.
* A lock left behind by a killed run (for example after a server restart) is cleared by the next command on the same server, instead of blocking it for 10 minutes.
* New documentation page [Long runs and log files](https://omnioffloader.vercel.app/wp-cli/long-runs/): start a big job with a log file, watch it, work out the speed and time left, and what to do when it ends.

**Admin screens**

* The WP-CLI reference on the Tools screen covers the new `move` and `stop` commands and how to keep a job running with `nohup`.
* The Offload and Bring Back pages have a **Run this job from WP-CLI** panel whose **View command** button opens the command in the WP-CLI reference on the Tools screen (recommended for libraries of 5,000+ items).
* Every OmniOffloader screen links to the documentation: a **Documentation** link in the page header, **Learn more** on each section and setting, and a setup guide for the selected storage provider.
* While a WP-CLI job runs, the Offload, Bring Back and Tools screens show which one, with its progress, speed, time left and how to follow or stop it, and their Start buttons wait until it finishes. They check when you open them or come back to the tab, and poll only while a WP-CLI job runs.
* Each page keeps its scroll position when you switch pages and come back, and links to a section open the page right at it.
* Confirmations (Bring Back that deletes cloud copies, a bucket folder change, moving media, Safe mode's live-site and folder buttons) use the plugin's own dialog with clear button labels, instead of the browser's OK / Cancel box.
* Dashboard: the status no longer says "Connected & Active" just because credentials are saved. It shows **Active**, or **Safe mode** on a copy of the site, and says when your site still loads media from the server (Serve Media from Cloud off).
* Accessibility: screen readers hear when a bulk job starts, the result of Test Connection, and why a Start button is disabled (now also shown as text under the button); after switching pages, focus moves to the new page's title.

**Fixes**

* Switching to another storage provider no longer breaks media you already offloaded: it keeps loading from the provider it was offloaded to (through its saved settings), Mirror Delete deletes it there, and Move Media brings it over to the new provider.
* The bucket, endpoint and region can no longer be changed while media is offloaded there, which used to break those URLs and Bring Back. Settings explains the steps: Bring Back, change the bucket, offload again. A region may only contain letters, numbers and dashes.
* Sharing a bucket: every folder this site has used, including folders from before 0.1.4, stays protected, and each upload or delete is checked against the folder it really goes to. If another site takes over an old folder, this site stops writing to it and `wp omnioffloader status` warns about it.
* Multisite: a site administrator can no longer point a provider at a private or local endpoint address (only a network administrator can), so a sub-site can't make the server send requests into its own network.
* Full Cloud Migration and Smart Local Cleanup: a new upload no longer reuses a file name that another attachment still has in your cloud storage (the original of a scaled image, a thumbnail size, a PDF preview or an edit backup), which could overwrite that attachment's files.
* Bring Back with "Delete from Cloud After Restore" no longer deletes cloud files it did not download, such as the original WordPress keeps after you edit an image. Only files that are now on your server are removed from the cloud.
* Bulk Bring Back with **Delete from Cloud After Restore** now asks for confirmation before it starts, as WP-CLI's `--delete-cloud` does.
* Bring Back (and loosening the retention policy) no longer fails for an image whose original was already gone before it was offloaded, for example removed by an image optimizer.
* Safe mode: **This is the live site** now asks for confirmation before turning Safe mode off, since clicking it on a staging copy lets that copy write to the live site's cloud storage. If it fails, the real reason is shown.
* Cover and Media & Text blocks no longer rewrite links to other files from the same upload folder (another image or a PDF inside the block) to the cloud. Only the block's own media is rewritten.
* Bulk jobs: a run whose last worker was stopped by the server no longer shows "running" forever; it finishes on its own once it has been quiet for a minute and the admin screen checks on it.
* Bulk jobs: cancelling and immediately starting again no longer lets the cancelled run carry on inside the new one.
* Background upload of new media: a large file is no longer uploaded twice at the same time, and an upload the server keeps stopping (time or memory limit) is marked failed after three tries instead of being retried forever.
* WP-CLI: closing the SSH window during `wp omnioffloader offload`, `restore` or `retention` now releases the job lock right away, as Ctrl+C does. Before, the next run was refused for up to 10 minutes.
* WP-CLI: media the run was transferring when it was interrupted (Ctrl+C, a closed window, a killed process) no longer shows "Offloading..." or "Restoring..." in the Media Library forever. It goes back to its real status and the next run picks it up.
* WP-CLI: a run started with `--force` next to another one no longer releases the other run's lock when it ends, and stopping after the failure limit says whether the connection or the attachments are at fault.
* WP-CLI: `wp omnioffloader retention` now stops after repeated failures when the storage provider can't be reached, and stops at once when the server runs out of disk space, like `restore`.
* Media Library: when Offload or Bring Back fails, the reason is shown as a notice (for example Safe mode, or another job running) instead of the page silently reloading; a failed bulk action shows it too.
* Attachments whose file isn't on the server (a copy of the site without its uploads, or files deleted outside WordPress) are now marked **File missing** instead of failing on every run: they are skipped before anything is uploaded, left out of **Local (Ready to Offload)** and later runs, and counted on the Offload page and in `wp omnioffloader status`. Once the file is back, the next offload picks it up by itself.
* Media Library: an item stuck on "Queued for background offload" or "Offloading..." (for example when thumbnail generation failed, or the background queue was cleared) goes back to its real status by itself once no job is running.
* Media Library: picking Offload or Bring Back for items that have nothing to do (already offloaded, or already local) while a bulk job of the same kind runs no longer stops that job.
* Media Library: a single Offload or Bring Back still in progress keeps its badge while another tab is open.
* Media Library: status checks slow down while nothing changes (from every 1.8 up to every 6 seconds) and keep going after a failed check instead of freezing until the tab is focused again. The status shows the provider's name (for example "Cloudflare R2") instead of its internal key.
* Uninstalling with `wp plugin uninstall --deactivate` no longer shows a PHP warning ("Constant OMNIOFFLOADER_DIR already defined").
* Settings: a save error stays visible until you change something or save again, and a save that stored the provider details but not the other settings says so. The Path Prefix field shows the folder name as it will be saved ("My Media" becomes "My-Media").

= 0.1.3 =

**Documentation**

* New documentation site at [omnioffloader.vercel.app](https://omnioffloader.vercel.app/): setup guides for every storage provider, every feature, the WP-CLI commands and the developer hooks. A **Documentation** link on the Plugins screen opens it.

**Apply Retention Policy to Existing Media**

* New Tools screen: apply the saved retention policy to media that was offloaded under an earlier policy. Switching from Retain Local Files to Smart Local Cleanup or Full Cloud Migration now frees space for existing media too, not just new uploads.
* Every local file is checked in cloud storage by size before it is deleted. Files missing from the bucket (for example thumbnails regenerated after the offload) are uploaded first; a file whose cloud copy differs is uploaded again, and kept and logged only if that fails. An attachment only counts as updated once every file was handled, so failures are retried on the next run.
* Moving to a looser policy downloads local copies again while the attachment stays offloaded.
* Preflight shows how many attachments need updating and roughly how much space will be freed or downloaded. Full Cloud Migration asks for confirmation before originals are removed.
* Runs in the background with the same progress bar, activity log, batch sizes and cancel button as Bulk Offload and Bring Back.
* Settings shows a notice after saving a new policy when existing media still follows an older one.
* New WP-CLI command: `wp omnioffloader retention [--policy=<0|1|2>] [--ids=<ids>] [--limit=<number>] [--dry-run] [--yes] [--force]`. `wp omnioffloader status` now reports media that follows an older policy.
* New filter `omnioffloader_should_apply_retention` to keep specific attachments' local files.
* WP-CLI reference on the Tools screen: every command with its options, copy buttons and a link to the matching admin screen.
* Offload, Bring Back and retention jobs can no longer run at the same time. This applies whether a job was started from the admin screens, the Media Library or WP-CLI.

**Faster Offloading**

* Offloading media that is not in the cloud yet is now faster. New uploads were already offloaded instantly in the background; the big win is for your existing library — Bulk Offload, Media Library bulk actions and `wp omnioffloader offload`. An attachment's files (original and every generated size) now upload side by side instead of one after another, and at any speed above Low a few attachments share one batch, with their main files confirmed in the bucket together. Everything still runs inside one PHP process, and each attachment is only marked offloaded once every one of its files uploaded and its main file is confirmed in the bucket.
* New Transfer Speed setting: Low (one file at a time, lowest resource use), Balanced (3 files, default), Fast (6 files, for VPS and dedicated servers) or Custom (any number from 1 to 10). WP-CLI offloads use the same setting. Large multipart uploads follow the same limit. Developers can override it with the `omnioffloader_transfer_concurrency` filter.
* Bring Back is faster: files that are still on your server are checked against the cloud with one request per attachment instead of one per file, and missing files download side by side at the same Transfer Speed. Each file is still written to a temporary file and moved into place only when complete.
* Background passes run longer where the host allows it (up to 40 seconds, based on `max_execution_time`), so WordPress reloads less often during a bulk job.
* Uploads no longer add the optional CRC32 checksum that recent AWS SDK versions send by default. That saves one read of every file, and several S3-compatible services reject the header.

**Security**

* Offloading and bringing back media, from the Media Library links, bulk actions or REST, is now limited to administrators, like the plugin's settings and bulk screens. Before, any user who could upload files could offload or bring back anyone's media and delete its cloud copies. Other users keep their normal WordPress rights: they upload and delete their own media, and with Mirror Delete on, deleting also removes the cloud copy.
* Bring Back from the Media Library screen now follows the "delete from cloud after restore" setting.
* Local files are only deleted inside the uploads folder, and restores refuse paths containing `..` before creating any directory. File paths from attachment metadata that point outside the uploads folder are ignored for upload, cleanup and restore.
* The Media Library status check only returns media the user can edit, and only shows the bucket name to administrators. Only administrators' status checks process pending background uploads.
* Error messages no longer include absolute server paths.
* Media Library status tooltips are escaped.
* REST parameters are validated against their schema (invalid limits, page sizes or actions now return 400), and the Media Library bulk endpoint accepts at most 1,000 IDs.
* "Remove all data on uninstall" keeps the offload records of media that exists only in the cloud, so it can still be recovered after reinstalling.

**Performance**

* Front end: media blocks are the only blocks whose IDs are looked up. A navigation menu no longer costs a database query per item.
* Front end: all attachments used by image, gallery, video, audio, file, cover and media-text blocks are loaded in one batch before the blocks render, so a page costs no extra queries per image (a 6-image gallery went from 11 queries to 6, the same as without the plugin).
* Front end: video and audio links in content are resolved with one query per post instead of one full scan per link, and cached for the rest of the page even when the content is rendered again (for example by an SEO plugin).
* The Bring Back and Tools screens reuse the cached preflight instead of rescanning the whole library every 6 seconds during a run.
* The Media Library status script no longer polls the server on every editor load or window focus when there is nothing to update.
* Each Media Library status check reads the bulk job state once (2 queries when idle instead of 6–8).
* `wp omnioffloader retention --dry-run` and the offload/restore dry runs load attachments 200 at a time instead of one by one.
* Uploading a file whose name is already taken checks all existing names with one query instead of one per "-1", "-2"… suffix.
* Clearing the stats cache costs half as many queries, and background uploads clear it once per batch instead of once per file.
* The post-activation redirect check only runs on the Plugins screen instead of on every admin request.
* Bulk jobs write their progress once per batch instead of two or three times per attachment, which cuts database writes sharply on large libraries.
* The Bring Back preflight reads only the attachment metadata it needs, 500 attachments per query, and keeps its estimate for an hour (it is refreshed whenever media is offloaded, brought back or deleted). Free disk space is always current.
* The Media Library bulk action loads the selected attachments in a few queries instead of several per item.
* On libraries of 5,000 items or more, the Bulk Offload and Bring Back screens suggest the matching WP-CLI command.

**Fixes**

* Bulk jobs: two background workers could pick up the same batch at the same moment and process items twice. A database lock now lets only one worker run a queue at a time.
* Multisite: each site now has its own queues. Before, all sites in a network shared one queue.
* Only administrators' Media Library views keep bulk jobs moving; other users' views only read their status. Idle status responses no longer repeat the last run's numbers.
* WP-CLI: locks are renewed after every file and released on Ctrl-C. `--limit` must be a positive number (a typo no longer means "the whole library"), `--ids` and `--all` can't be combined, and skipped IDs are reported. `restore` stops after 10 failures in a row, `status` exits with an error code when the bucket is unreachable, and `retention --dry-run` supports `--format=json|csv`.
* REST: settings and credentials are validated against their expected shape, the error list supports paging, and responses always use consistent types.
* Accessibility: card choices support arrow keys, progress bars are labelled, and screen readers hear when a run starts or finishes. The admin screen now loads right-to-left styles for RTL languages and respects reduced motion.
* The Dashboard only shows saved settings, the Settings page warns before you leave with unsaved changes, and error messages stay until dismissed.
* Attachment offload records can no longer be edited as custom fields.
* A missing `vendor` folder shows an admin notice instead of a fatal error.
* Bulk jobs no longer stop for good on a stretch of broken attachments. Before, 10 failures in a row, such as files missing from the server, stopped the run. Because those attachments stayed first in line, every restart stopped on them again. Now the connection to the storage provider is checked first. If it answers, the run carries on and the failures stay in the error log. It still stops after 100 failures in a row. Attachments that failed before now wait until the end of the next run. The same applies to `wp omnioffloader offload` and `restore`.
* When the storage provider can't be reached, the stop message now includes the provider's error.
* Cancelling a bulk job now stops the background worker right away. Before, the worker kept going until the end of its pass (up to 40 seconds, and longer with Redis or Memcached). Because the job already showed as cancelled, another job could start while it was still running.
* Media Library items no longer stay stuck on "Queued" or "Offloading…" after a job was cancelled or stopped, or after the server killed the worker mid-file.
* Bring Back stops with a clear "out of disk space" message before the disk fills up, instead of failing downloads and blaming the connection. The attachment stays in the cloud, and the next run picks it up again.
* Bring Back no longer counts an attachment as failed when it was already brought back while it waited in the queue.
* Attachments excluded by the `omnioffloader_should_offload_attachment` filter now show as "Skipped" in the activity log and WP-CLI summary instead of as failures, and no longer keep a "Queued" badge.
* A very long single transfer (a multi-GB file on a slow connection) no longer lets the job lock expire mid-file. The lock is renewed while bytes are moving, so another job can't start alongside it. New action `omnioffloader_transfer_heartbeat` fires every 30 seconds during uploads and downloads.
* Partial downloads left behind when the server stopped a Bring Back mid-file are now cleaned up.
* The Bring Back download estimate now counts only the files that are actually missing from the server. Before, it also counted files your retention policy kept on the server, so a site using Retain Local Files saw its whole library as the download size and could get a false "not enough disk space" warning. Attachments uploaded before WordPress 6.0, which have no recorded file size, are now reported separately instead of silently counting as zero. A note also explains that free disk space is what the server reports, and a hosting plan's storage quota may be lower.
* Shared hosting with a storage quota (cPanel, CloudLinux): PHP can only see the whole server disk, not your plan's quota. Bring Back and retention updates now notice when the server refuses to save a file ("Disk quota exceeded", "No space left on device") and stop right away with a clear message. Before, they kept failing until the failure limit and blamed the attachments. Nothing is lost: attachments that were not brought back stay in the cloud. New filter `omnioffloader_free_disk_space` lets a host or developer supply the real space left in the quota.
* Mirror Delete is now on by default for new installs, so an attachment deleted in WordPress no longer stays publicly reachable, and billed, in the bucket. Existing sites keep their current setting. It never runs on a staging or development copy (Safe mode).
* New: Safe mode for staging, development and local copies. A copy of a live site (made with a backup or migration plugin, a host's staging tool, or on a local machine) is recognised automatically, and nothing is uploaded to or deleted from the live site's cloud storage from it. Before, a copy could overwrite files in the live bucket with its own uploads and, with Mirror Delete on, delete files the live site still served. Media already in the cloud keeps loading and new uploads stay local. Bring Back is hidden and turned off on a copy unless you allow it in Settings ("Allow Bring Back on a copy"), and then it only downloads, never deleting from the cloud. Detection uses the site address, `WP_ENVIRONMENT_TYPE` and local addresses; a banner explains why, with a "This is the live site" button for a real domain move. Control it with the new Settings option "Protect the live site when this is a copy" or the `OMNIOFFLOADER_SAFE_MODE` constant. `wp omnioffloader status` shows the mode; `wp omnioffloader offload` and `restore` refuse to run in Safe mode (`restore` works when Bring Back is allowed, but never with `--delete-cloud`).
* cPanel hosting: the Bring Back screen now shows the space left in your hosting plan (for example "9.3 GB of 10 GB"), read from cPanel's own quota record, together with when cPanel last updated it. Before, it showed the whole server disk (often terabytes). No shell commands are used. The disk check during Bring Back uses the quota too, as long as cPanel updated it within the last hour.
* The retention policy update now checks the storage connection before stopping on repeated failures, like Bulk Offload and Bring Back.
* The Dashboard's local media count now matches what Bulk Offload processes (trashed attachments and entries without a file are not counted), so it reaches 0 after a full run.
* Provider errors are only written to the PHP error log when `WP_DEBUG` is on (filter `omnioffloader_log_provider_errors`); they are always recorded on the attachment.
* Retention: when a cloud copy differs in size from the server file (for example after an image optimizer shrank the thumbnails), the server file is uploaded again and verified, instead of the run stopping with a misleading connection error.
* Retention: attachments excluded with the `omnioffloader_should_apply_retention` filter no longer show as "needs updating" forever.
* Progress polling no longer runs two timers at once, keeps polling after you leave the page, or restarts after Cancel.
* Deactivating the plugin now also clears the background upload queue and its scheduled event.
* Counts use proper singular/plural translations, and the activity log's "Attachment #" label is translatable.
* New filter `omnioffloader_media_block_names` to register third-party media blocks.
* The `omnioffloader_after_offload` action fires again after an attachment is offloaded and verified.
* New: separate sites can safely share one bucket and one CDN address, each in its own Path Prefix folder. Each site marks its folder with a small `.omnioffloader-site.json` file. Before, two sites using the same folder overwrote each other's files, and Mirror Delete on one site deleted files the other still served. Settings now refuses a folder another site uses. A site that finds another site's marker in its folder switches to Safe mode and stops uploading and deleting. It shows a banner with **Choose a Path Prefix** and **This folder belongs to this site**. Bulk Offload and retention runs stop with a clear message instead of failing item by item. Test Connection warns about a taken folder, `wp omnioffloader status` reports it, and a confirmed domain move ("This is the live site") takes the folder's claim along. The folder is checked on the first upload or delete after an update or settings change, then once a day. Developers can turn the check off with the `omnioffloader_check_folder_claim` filter.
* Safe mode and the folder check identify a site by its saved Site Address, so multilingual sites with a different domain per language (WPML, Polylang) are never mistaken for a copy of themselves or for another site.

= 0.1.2 =

* Bulk Offload and Bring Back can process a custom number of attachments (presets 5, 10, 25, 50, 100 or All); the Start buttons show the count, and Bring Back estimates the disk space for that batch.
* New `--limit=<number>` for `wp omnioffloader restore`, matching `offload`.
* Bulk progress stays live under LiteSpeed Cache, Breeze, browser and proxy caches, and Redis or Memcached object caches.
* Fixed the counts, the Start Bring Back button and the "All Media Offloaded" state not refreshing when a run finished.

= 0.1.1 =

* New providers: Amazon S3, DigitalOcean Spaces, Backblaze B2, Wasabi and any S3-compatible storage (MinIO and others).
* Media Library: a status column, live badges in the grid view, an OmniOffloader box on the edit screen, row actions and bulk actions.
* New uploads are offloaded in the background; files above 64 MB upload in parts (files over 5 GB supported).
* URL rewriting for Video, Audio, File, Cover and Media & Text blocks, Classic Editor media tags and audio/video shortcodes.
* New `--limit=<number>` for `wp omnioffloader offload`.
* Bulk jobs are much more reliable: no endless retries of one file, no duplicate workers, a stop after 10 failures in a row, Cancel works where loopback requests are blocked, connection timeouts on every request, and no queue rows left behind on deactivation.
* Faster Media Library status updates (smaller responses, paused in background tabs) and fewer database writes during bulk runs.

= 0.1.0 =

* Initial release of OmniOffloader.
* Multi-cloud offload engine with Cloudflare R2 provider support.
* Modern React-based settings and control dashboard.
* Background bulk offload and bulk restore with real-time progress indicators.
* URL rewriting for post content, attachment URLs, and responsive srcset attributes.
* Media library filters and individual attachment action controls.
* Complete WP-CLI commands: `wp omnioffloader offload`, `wp omnioffloader restore`, and `wp omnioffloader status`.
