Add wp media find-orphans subcommand - #252
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Add a non-destructive subcommand that finds orphaned media candidates by
comparing the media library, the uploads directory, and content usage.
Detectors (via `--type`, all run by default):
- filesystem: files on disk not in the media library. Restricted to known
media extensions (get_allowed_mime_types) and skips generated
subdirectories (elementor, gravity_forms, cache, wpcf7_uploads)
via the `wp_cli_media_find_orphans_ignore_paths` filter, so
page-builder noise is excluded.
- database: attachments whose file is missing from disk (both the
get_attached_file() and raw _wp_attached_file paths checked
to avoid `-scaled` false positives).
- thumbnails: generated thumbnails whose parent attachment is gone.
- usage: attachments unreferenced in content (conservative; scans all
registered post types; O(M+N) precomputed path->id lookup).
Options: --type, --format (table/json/csv/yaml/ids/count), --fields,
--include-thumbnails, --limit, --error-on-orphans (exit 1 when found).
Usage detection is extensible via the `wp_cli_media_find_orphans_used_ids`
filter so plugins can declare postmeta/ACF/page-builder references.
Adds a Behat feature covering all four types, JSON output, and the
error-on-orphans exit code. Registers the command in composer.json.
Implements wp-cli/ideas#216.
c0a9910 to
10a55e4
Compare
|
I've tried my best to get a 100% coverage on Codecov, but the missing lines would imply a lot of fixtures and heavy coding for a small result. Is it mandatory ? |
No, not mandatory at all. Thanks for keeping an eye on it though! |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The post_parent scan collected parent post IDs into $used_ids, which is a set of attachment IDs. This could misclassify attachments uploaded to a post (but not referenced in content) as unused, and could shield an unrelated attachment whose ID collided with a parent post ID. Select the attachment IDs themselves instead, and cover the post_parent-only case in the usage scenario.
|
Ready to test again :) |
📝 WalkthroughWalkthroughChangesThe PR adds Media orphan detection
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WPCLI
participant find_orphans
participant OrphanDetectors
participant OutputFormatter
WPCLI->>find_orphans: Provide scan options
find_orphans->>OrphanDetectors: Run selected orphan detectors
OrphanDetectors-->>find_orphans: Return orphan candidates
find_orphans->>OutputFormatter: Apply fields, format, and limit
OutputFormatter-->>WPCLI: Render results and status
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
features/media-find-orphans.feature (1)
137-144: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMake the negative ID assertions exact.
STDOUT should not containperforms a substring match over the whole table, including thefileandpathcolumns. If an attachment ID becomes a substring of another printed number, the scenario fails for the wrong reason. Assert on a single column instead, for examplewp media find-orphans --type=usage --field=attachment_id, and compare the exact ID list.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@features/media-find-orphans.feature` around lines 137 - 144, Update the negative assertions in the media find-orphans scenario to query only the attachment_id field and compare the exact returned ID list, rather than using whole-STDOUT substring checks. Preserve the expectation that FEATURED_ATTACHMENT_ID and ATTACHED_ATTACHMENT_ID are absent from the command’s exact attachment ID results.src/Media_Command.php (3)
2848-2856: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueApply the media-extension filter to the thumbnail scan.
detect_filesystem_orphans()restricts candidates to allowed media extensions on Line 2742. This scan does not. A non-media file whose name matches-WxH.ext, for examplesprite-12x12.css, is reported as a thumbnail orphan. Reuseget_allowed_media_extensions()here for consistent results between the two scans.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Media_Command.php` around lines 2848 - 2856, Update the thumbnail scan in detect_filesystem_orphans(), around the is_thumbnail_filename() check, to first require the file extension from get_allowed_media_extensions(). Continue scanning non-thumbnail and known declared-size files as before, while excluding matching thumbnail names with disallowed media extensions such as CSS.
2981-2991: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftFetch
post_contentin bulk instead of per post.
get_posts()on Line 2916 usesfields => 'ids', so the post rows are not primed.get_post_field()then issues one query per post. On a large site this is an N+1 over every published and draft post of every post type. SelectID, post_contentin batches with$wpdb, or prime the cache with_prime_post_caches()per chunk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Media_Command.php` around lines 2981 - 2991, Update the processing flow around the post ID query and foreach loop to avoid calling get_post_field() once per post. Fetch post IDs with their post_content in batches using $wpdb, or prime post caches with _prime_post_caches() per chunk, then read each post’s content from the bulk-loaded data while preserving the existing empty-content filtering and cache-clearing interval.
2724-2727: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffThe filesystem and thumbnail detectors repeat the same two full scans. Each detector independently rebuilds the library path index and walks the whole uploads tree. A default run, which executes both detectors, performs the attachment query and the recursive directory walk twice.
src/Media_Command.php#L2724-L2727: cache theget_known_library_paths()result in a private property and reuse it.src/Media_Command.php#L2837-L2837: reuse a single uploads walk for both detectors, or run both classifications inside one pass overiterate_upload_files().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Media_Command.php` around lines 2724 - 2727, In src/Media_Command.php lines 2724-2727, cache the result of get_known_library_paths() in a private property and reuse it across detector runs instead of rebuilding the index. In src/Media_Command.php line 2837, reuse one iterate_upload_files() traversal for both filesystem and thumbnail classifications, preserving both detectors’ existing classification behavior while eliminating the duplicate uploads walk.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Media_Command.php`:
- Around line 2636-2643: In src/Media_Command.php lines 2636-2643, cast the raw
fields value and format value to string before passing them to explode() and
Utils::format_items(). In src/Media_Command.php line 3054, replace the intval
callback with a closure that explicitly casts its argument to int. In
src/Media_Command.php lines 3181-3186, filter the
wp_cli_media_find_orphans_ignore_paths results to skip non-string entries rather
than casting them.
- Around line 3087-3093: Update the RecursiveIteratorIterator construction in
the upload directory scan to include the
RecursiveIteratorIterator::CATCH_GET_CHILD flag, allowing unreadable
subdirectories to be skipped while preserving the existing recursive leaf-only
traversal.
---
Nitpick comments:
In `@features/media-find-orphans.feature`:
- Around line 137-144: Update the negative assertions in the media find-orphans
scenario to query only the attachment_id field and compare the exact returned ID
list, rather than using whole-STDOUT substring checks. Preserve the expectation
that FEATURED_ATTACHMENT_ID and ATTACHED_ATTACHMENT_ID are absent from the
command’s exact attachment ID results.
In `@src/Media_Command.php`:
- Around line 2848-2856: Update the thumbnail scan in
detect_filesystem_orphans(), around the is_thumbnail_filename() check, to first
require the file extension from get_allowed_media_extensions(). Continue
scanning non-thumbnail and known declared-size files as before, while excluding
matching thumbnail names with disallowed media extensions such as CSS.
- Around line 2981-2991: Update the processing flow around the post ID query and
foreach loop to avoid calling get_post_field() once per post. Fetch post IDs
with their post_content in batches using $wpdb, or prime post caches with
_prime_post_caches() per chunk, then read each post’s content from the
bulk-loaded data while preserving the existing empty-content filtering and
cache-clearing interval.
- Around line 2724-2727: In src/Media_Command.php lines 2724-2727, cache the
result of get_known_library_paths() in a private property and reuse it across
detector runs instead of rebuilding the index. In src/Media_Command.php line
2837, reuse one iterate_upload_files() traversal for both filesystem and
thumbnail classifications, preserving both detectors’ existing classification
behavior while eliminating the duplicate uploads walk.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b9c03f74-81bb-4aa6-a896-e1a317e61125
📒 Files selected for processing (3)
composer.jsonfeatures/media-find-orphans.featuresrc/Media_Command.php
What it does
wp media find-orphansscans a WordPress site for "orphan" media — files and attachments that are out of sync between the filesystem and the media library — and reports them. It is read-only by design: it never deletes, moves, or modifies anything.It runs four independent detectors, selectable with
--type:wp-content/uploadsthat are not in the media library (File on disk not in media library)Attachment file missing from disk)-WxH) whose parent attachment no longer exists (Thumbnail parent attachment missing)Attachment appears unused in content); featured images are not flaggedWithout
--type, all detectors run. Output is a table by default and supports--format=table|json|csv|yaml|ids|count, plus--fields,--limit, and--include-thumbnails.The command always exits
0. Pass--error-on-orphansto exit1when any orphan is found — convenient for CI/cron checks.Extensibility
Two WordPress filters let themes/plugins teach the detectors about their own storage and references:
wp_cli_media_find_orphans_ignore_paths— upload subpaths to skip (defaults cover common generated dirs such as caches and form uploads), so plugin-generated assets aren't reported as filesystem orphans.wp_cli_media_find_orphans_used_ids— additional attachment IDs to treat as "used" (receives the scanned post IDs and known attachment IDs as context), so custom references (e.g. postmeta, custom fields) don't yield false positives in theusagedetector.Examples
Tests
Acceptance coverage lives in
features/media-find-orphans.feature— one scenario per capability, each following Arrange → Act → Assert on a fresh WP install (withuploads_use_yearmonth_foldersdisabled for deterministic paths):No orphan media foundand exits0.uploads/is detected.@require-wp-5.3) → an imported attachment whose file is thenrm'd is detected.-150x150file with no parent attachment is detected.--format=json→ output is valid JSON containing the expected fields (type,attachment_id,file,issue,path).--error-on-orphans→ returns a non-zero exit code when orphans exist.All 7 scenarios pass (72 steps) against the standard
wp-cli/wp-cli-testsBehat harness.Notes
composer.json.Summary by CodeRabbit
New Features
wp media find-orphanscommand to identify potential filesystem, database, thumbnail, and content-usage orphans.Tests