Skip to content

Commit 10a55e4

Browse files
committed
feat: add wp media find-orphans subcommand
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.
1 parent eef589d commit 10a55e4

3 files changed

Lines changed: 1104 additions & 0 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"bundled": true,
3737
"commands": [
3838
"media",
39+
"media find-orphans",
3940
"media fix-orientation",
4041
"media import",
4142
"media prune",
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
Feature: Find orphan WordPress media
2+
3+
Background:
4+
Given a WP install
5+
And I run `wp option update uploads_use_yearmonth_folders 0`
6+
7+
Scenario: Report no orphans when the library is consistent
8+
Given download:
9+
| path | url |
10+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
11+
12+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="A clean attachment" --porcelain`
13+
Then save STDOUT as {ATTACHMENT_ID}
14+
And the wp-content/uploads/canola.jpg file should exist
15+
16+
When I run `wp media find-orphans --type=database`
17+
Then STDOUT should contain:
18+
"""
19+
Success: No orphan media found.
20+
"""
21+
And the return code should be 0
22+
23+
Scenario: Detect a file on disk that is not in the media library
24+
Given download:
25+
| path | url |
26+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
27+
And a wp-content/uploads/stray-image.jpg file:
28+
"""
29+
not a real image, just a stray file on disk
30+
"""
31+
32+
When I run `wp media find-orphans --type=filesystem`
33+
Then STDOUT should contain:
34+
"""
35+
filesystem
36+
"""
37+
And STDOUT should contain:
38+
"""
39+
stray-image.jpg
40+
"""
41+
And STDOUT should contain:
42+
"""
43+
File on disk not in media library
44+
"""
45+
And the return code should be 0
46+
47+
@require-wp-5.3
48+
Scenario: Detect an attachment whose file is missing from disk
49+
Given download:
50+
| path | url |
51+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
52+
53+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="My medium attachment" --porcelain`
54+
Then save STDOUT as {ATTACHMENT_ID}
55+
And the wp-content/uploads/canola.jpg file should exist
56+
57+
# Remove the original file on disk while keeping the database record.
58+
When I run `rm wp-content/uploads/canola.jpg`
59+
Then the wp-content/uploads/canola.jpg file should not exist
60+
61+
When I run `wp media find-orphans --type=database`
62+
Then STDOUT should contain:
63+
"""
64+
database
65+
"""
66+
And STDOUT should contain:
67+
"""
68+
{ATTACHMENT_ID}
69+
"""
70+
And STDOUT should contain:
71+
"""
72+
canola.jpg
73+
"""
74+
And STDOUT should contain:
75+
"""
76+
Attachment file missing from disk
77+
"""
78+
And the return code should be 0
79+
80+
Scenario: Detect a thumbnail whose parent attachment is gone
81+
# A thumbnail-named file on disk whose parent original/attachment does not
82+
# exist. (Note: `wp post delete --force` also removes the thumbnail files,
83+
# so we stage the orphan thumbnail directly.)
84+
Given a wp-content/uploads/orphan-image-150x150.jpg file:
85+
"""
86+
not a real image, just a stray thumbnail on disk
87+
"""
88+
89+
When I run `wp media find-orphans --type=thumbnails`
90+
Then STDOUT should contain:
91+
"""
92+
thumbnails
93+
"""
94+
And STDOUT should contain:
95+
"""
96+
orphan-image-150x150.jpg
97+
"""
98+
And STDOUT should contain:
99+
"""
100+
Thumbnail parent attachment missing
101+
"""
102+
And the return code should be 0
103+
104+
Scenario: Detect an unreferenced attachment but not a featured image
105+
Given download:
106+
| path | url |
107+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
108+
| {CACHE_DIR}/large-image.jpg | http://wp-cli.github.io/behat-data/large-image.jpg |
109+
110+
When I run `wp post create --post_type=post --post_title="A post" --post_status=publish --porcelain`
111+
Then save STDOUT as {POST_ID}
112+
113+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="Unused attachment" --porcelain`
114+
Then save STDOUT as {UNUSED_ATTACHMENT_ID}
115+
116+
When I run `wp media import {CACHE_DIR}/large-image.jpg --post_id={POST_ID} --featured_image --title="Featured attachment" --porcelain`
117+
Then save STDOUT as {FEATURED_ATTACHMENT_ID}
118+
119+
When I run `wp media find-orphans --type=usage`
120+
Then STDOUT should contain:
121+
"""
122+
usage
123+
"""
124+
And STDOUT should contain:
125+
"""
126+
{UNUSED_ATTACHMENT_ID}
127+
"""
128+
And STDOUT should contain:
129+
"""
130+
Attachment appears unused in content
131+
"""
132+
And STDOUT should not contain:
133+
"""
134+
{FEATURED_ATTACHMENT_ID}
135+
"""
136+
And the return code should be 0
137+
138+
Scenario: Output orphans as valid JSON
139+
Given download:
140+
| path | url |
141+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
142+
143+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="My medium attachment" --porcelain`
144+
Then save STDOUT as {ATTACHMENT_ID}
145+
And the wp-content/uploads/canola.jpg file should exist
146+
147+
# Remove the original file on disk to create a deterministic database orphan.
148+
When I run `rm wp-content/uploads/canola.jpg`
149+
Then the wp-content/uploads/canola.jpg file should not exist
150+
151+
When I run `wp media find-orphans --type=database --format=json`
152+
Then STDOUT should be JSON containing:
153+
"""
154+
[
155+
{
156+
"type": "database",
157+
"attachment_id": "{ATTACHMENT_ID}",
158+
"file": "canola.jpg",
159+
"issue": "Attachment file missing from disk",
160+
"path": "canola.jpg"
161+
}
162+
]
163+
"""
164+
And the return code should be 0
165+
166+
Scenario: Return a non-zero exit code with --error-on-orphans
167+
Given a wp-content/uploads/stray-image.jpg file:
168+
"""
169+
not a real image, just a stray file on disk
170+
"""
171+
172+
When I try `wp media find-orphans --type=filesystem --error-on-orphans`
173+
Then STDOUT should contain:
174+
"""
175+
stray-image.jpg
176+
"""
177+
And the return code should be 1
178+
179+
Scenario: Run every detector when no type is given
180+
Given download:
181+
| path | url |
182+
| {CACHE_DIR}/canola.jpg | http://wp-cli.github.io/behat-data/canola.jpg |
183+
184+
When I run `wp media import {CACHE_DIR}/canola.jpg --title="A clean attachment" --porcelain`
185+
Then save STDOUT as {ATTACHMENT_ID}
186+
187+
Given a wp-content/uploads/stray-image.jpg file:
188+
"""
189+
not a real image, just a stray file on disk
190+
"""
191+
192+
# No --type runs filesystem + database + thumbnails + usage together.
193+
When I run `wp media find-orphans`
194+
Then STDOUT should contain:
195+
"""
196+
filesystem
197+
"""
198+
And STDOUT should contain:
199+
"""
200+
stray-image.jpg
201+
"""
202+
And STDOUT should contain:
203+
"""
204+
usage
205+
"""
206+
And the return code should be 0
207+
208+
Scenario: Reject an invalid --type value
209+
When I try `wp media find-orphans --type=bogus`
210+
Then STDERR should contain:
211+
"""
212+
Invalid value specified for 'type'
213+
"""
214+
And the return code should be 1
215+
216+
Scenario: Reject an invalid --limit value
217+
When I try `wp media find-orphans --limit=-5`
218+
Then STDERR should contain:
219+
"""
220+
The --limit value must be an integer
221+
"""
222+
And the return code should be 1
223+
224+
Scenario: Cap the number of results with --limit
225+
Given a wp-content/uploads/stray-one.jpg file:
226+
"""
227+
stray file one
228+
"""
229+
And a wp-content/uploads/stray-two.jpg file:
230+
"""
231+
stray file two
232+
"""
233+
234+
When I run `wp media find-orphans --type=filesystem --format=count`
235+
Then STDOUT should be:
236+
"""
237+
2
238+
"""
239+
240+
When I run `wp media find-orphans --type=filesystem --limit=1 --format=count`
241+
Then STDOUT should be:
242+
"""
243+
1
244+
"""
245+
And the return code should be 0
246+
247+
Scenario: Restrict output columns with --fields
248+
Given a wp-content/uploads/stray-image.jpg file:
249+
"""
250+
not a real image, just a stray file on disk
251+
"""
252+
253+
When I run `wp media find-orphans --type=filesystem --fields=type,file --format=csv`
254+
Then STDOUT should contain:
255+
"""
256+
type,file
257+
"""
258+
And STDOUT should contain:
259+
"""
260+
filesystem,stray-image.jpg
261+
"""
262+
And the return code should be 0
263+
264+
Scenario: Include orphan thumbnails in the filesystem scan with --include-thumbnails
265+
Given a wp-content/uploads/lonely-image-400x300.jpg file:
266+
"""
267+
not a real image, just a stray thumbnail-named file
268+
"""
269+
270+
# Thumbnail-named files are skipped by the filesystem scan by default.
271+
When I run `wp media find-orphans --type=filesystem`
272+
Then STDOUT should not contain:
273+
"""
274+
lonely-image-400x300.jpg
275+
"""
276+
277+
# With the flag, the same file is reported as a filesystem orphan.
278+
When I run `wp media find-orphans --type=filesystem --include-thumbnails`
279+
Then STDOUT should contain:
280+
"""
281+
lonely-image-400x300.jpg
282+
"""
283+
And STDOUT should contain:
284+
"""
285+
File on disk not in media library
286+
"""
287+
And the return code should be 0
288+
289+
Scenario: Skip generated subdirectories during the filesystem scan
290+
Given a wp-content/uploads/cache/generated-image.jpg file:
291+
"""
292+
a plugin-generated cache file that must be ignored
293+
"""
294+
295+
When I run `wp media find-orphans --type=filesystem`
296+
Then STDOUT should contain:
297+
"""
298+
Success: No orphan media found.
299+
"""
300+
And STDOUT should not contain:
301+
"""
302+
generated-image.jpg
303+
"""
304+
And the return code should be 0

0 commit comments

Comments
 (0)