Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

🗑 Programowe czyszczenie biblioteki mediów WordPress: skrypty PHP do usuwania niepotrzebnych plików

🗑 Programowe czyszczenie biblioteki mediów WordPress: skrypty PHP do usuwania niepotrzebnych plików

Biblioteka multimediów WordPress jest urządzona jak strych: usunęli Państwo wpis, obrazy zostały. Zmienili Państwo motyw, stare rozmiary obrazów zalegają jako martwy balast. Przenieśli Państwo witrynę, połowa miniatur zwraca 404.

Ręczne czyszczenie przez panel administracyjny na witrynie z kilkoma tysiącami plików to zajęcie dla medytujących. Istnieje jednak szybszy sposób: pięć funkcji PHP, które znajdują i usuwają śmieci w jednym przebiegu. Bez wtyczek, z czytelnym kodem i kontrolą nad każdym usuwanym plikiem.

Przed uruchomieniem proszę wykonać pełną kopię zapasową. Funkcje usuwają pliki bezpowrotnie: nie mają ani kosza, ani opcji „cofnij". W razie wątpliwości proszę uruchomić je na kopii stagingowej.

💡 Szybki przegląd:

  • Usuwanie niepodpiętych załączników, plików pozostałych po usunięciu wpisów
  • Czyszczenie plików multimedialnych konkretnego, dowolnego typu wpisu (CPT)
  • Oczyszczanie biblioteki multimediów z uszkodzonych odnośników, załączników 404 bez pliku na serwerze
  • Wyszukiwanie i usuwanie plików w wp-content/uploads, które nie są zarejestrowane jako załączniki WordPress
  • Scenariusz dla witryn, gdzie obrazy są przechowywane w polach niestandardowych (ACF, Meta Box), a nie jako załączniki

O czym warto wiedzieć przed uruchomieniem

Poniższy kod usuwa pliki fizycznie, z dysku i z bazy danych. Trzy kwestie, które uratują witrynę.

Po pierwsze: obrazy na stronach archiwów tagów lub w opisach SEO często nie są przypisane do żadnego wpisu. Wiszą jako „sieroty", ale są witrynie potrzebne. Jeśli mają Państwo takie obrazy, proszę wykluczyć je z zakresu działania funkcji lub dopracować warunki.

Po drugie: WordPress tworzy kilka rozmiarów każdego obrazu. Miniatury dziedziczą post_parent oryginału, dlatego funkcja delete_unattached_attachments() ich nie rusza, filtruje ściśle według post_parent = 0. Problem pojawia się tylko wtedy, gdy sam oryginał utracił powiązanie z wpisem.

Po trzecie: jeśli do usuwanego pliku prowadzi odnośnik w treści wpisu, po czyszczeniu stanie się on uszkodzony. Przed uruchomieniem proszę przeskanować witrynę narzędziem Screaming Frog lub podobnym i sporządzić mapę odnośników.

1. Usuwanie niepodpiętych załączników

Najczęstszy scenariusz: usunęli Państwo wpis, załączniki pozostały w bazie z post_type = 'attachment' i post_parent = 0. Zajmują miejsce na dysku i w kopiach zapasowych.

Poniższa funkcja znajduje wszystkie takie wpisy i je usuwa. Proszę umieścić ją w functions.php motywu potomnego lub za pomocą wtyczki do snippetów, takiej jak WPCode. Sama z siebie nie uruchomi się, to definicja, która wymaga wywołania.

1function delete_unattached_attachments() {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 'fields' => 'ids',
6 'post_parent' => 0,
7 ) );
8
9 if ( $attachments ) {
10 foreach ( $attachments as $attachment_id ) {
11 $attachment_path = get_attached_file( $attachment_id );
12 wp_delete_attachment( $attachment_id, true );
13 unlink( $attachment_path );
14 }
15 }
16}

get_posts() wybiera wszystkie załączniki bez wpisu nadrzędnego. wp_delete_attachment() z parametrem true usuwa zarówno wpis w bazie, jak i plik wraz z miniaturami. Dodatkowy unlink() to zabezpieczenie: jeśli plik z jakiegoś powodu pozostał na dysku, jest usuwany siłowo.

Proszę zwrócić uwagę: wyróżnione obrazy (featured images) również mają post_parent = 0 w niektórych konfiguracjach. Przed uruchomieniem produkcyjnym proszę zastąpić wp_delete_attachment przez echo $attachment_id . '<br>', zobaczą Państwo listę ID, które zostaną usunięte. Po upewnieniu się, że wszystko jest w porządku, proszę przywrócić wariant produkcyjny.

Po jednorazowym uruchomieniu proszę usunąć funkcję z functions.php. Nie ma potrzeby trzymać jej przy każdym init.

2. Usuwanie załączników konkretnego CPT

Dawny sklep internetowy na WooCommerce, stara sekcja portfolio, usunięty niestandardowy typ wpisu, wszystkie ich obrazy nadal zalegają na serwerze. Poniższa funkcja czyści załączniki przypisane do wpisów wskazanego typu.

1function delete_cpt_attachments( $cpt = 'card' ) {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 ) );
6
7 if ( $attachments ) {
8 foreach ( $attachments as $attachment ) {
9 $parent_id = $attachment->post_parent;
10
11 if ( $cpt === get_post_type( $parent_id ) ) {
12 $attachment_path = get_attached_file( $attachment->ID );
13 wp_delete_attachment( $attachment->ID, true );
14 unlink( $attachment_path );
15 }
16 }
17 }
18}

Proszę zastąpić 'card' slugiem Państwa CPT. Dla produktów WooCommerce będzie to 'product'. Jeśli CPT został już usunięty, get_post_type() zwróci false, załączniki tego typu nie zostaną objęte działaniem funkcji. W przypadku usuniętych CPT logikę trzeba skorygować: sprawdzać nie typ elementu nadrzędnego, a przynależność do taksonomii lub pola meta.

Na dużych bazach danych proszę zachować ostrożność: 'numberposts' => -1 bez 'fields' => 'ids' ładuje pełne obiekty WP_Post. Przy ponad 10 000 załączników może to natrafić na limit memory_limit. Dla wolumenów produkcyjnych proszę dodać 'fields' => 'ids' i pobierać tylko ID, get_post_type() zadziała również po ID elementu nadrzędnego.

3. Oczyszczanie biblioteki multimediów z załączników 404

Uszkodzone miniatury w bibliotece multimediów to symptom tego, że plik na dysku został usunięty (ręcznie, przez awarię hostingu lub wadliwą wtyczkę), a wpis w bazie pozostał. WordPress pokazuje szary prostokąt, ale po kliknięciu zwraca 404.

Funkcja odpytuje każdy URL załącznika i usuwa te, które zwracają 404.

1function delete_404_attachments() {
2 $attachments = get_posts( array(
3 'post_type' => 'attachment',
4 'numberposts' => -1,
5 'fields' => 'ids',
6 ) );
7
8 if ( $attachments ) {
9 foreach ( $attachments as $attachment_id ) {
10 $file_url = wp_get_attachment_url( $attachment_id );
11 $file_headers = @get_headers( $file_url );
12
13 if ( $file_headers && strpos( $file_headers[0], '404' ) !== false ) {
14 wp_delete_attachment( $attachment_id, true );
15 }
16 }
17 }
18}

Ważne: funkcja jest zasobożerna. Każde wywołanie get_headers() to żądanie HTTP do Państwa własnego serwera. Przy tysiącach załączników wykonują Państwo tysiąc żądań HTTP w jednym przebiegu. Rezultat: długi czas wykonania, obciążenie serwera, niektóre hostingi przerywają proces z powodu timeoutu.

W przypadku dużych bibliotek multimediów należy podzielić je na porcje za pomocą 'offset' i 'numberposts' lub uruchamiać przez WP-CLI z limitem na partię. Jeśli strona działa za CDN lub proxy, proszę zastąpić sprawdzanie funkcją wp_remote_head() z timeoutem, ponieważ get_headers() nie zawsze poprawnie obsługuje przekierowania i nie wspiera uwierzytelniania.

4. Odwrotna weryfikacja: pliki w uploads bez wpisu w bazie danych

Poprzednie trzy funkcje czyszczą bazę danych, usuwają wpisy załączników. Jednak w wp-content/uploads mogą znajdować się pliki, które w ogóle nie są zarejestrowane jako załączniki: wgrane przez FTP, pozostawione przez wtyczki, wygenerowane przez cache.

Ta funkcja działa w odwrotnym kierunku: nie od bazy do plików, lecz od plików do bazy. Rekursywnie skanuje wp-content/uploads i dla każdego pliku sprawdza za pomocą attachment_url_to_postid(), czy jest on załącznikiem. Jeśli nie, usuwa go.

1function clean_uploads_from_nonattachments() {
2 $uploads_dir = wp_upload_dir();
3 $search = $uploads_dir['basedir'];
4 $replace = $uploads_dir['baseurl'];
5 $root = $uploads_dir['basedir'];
6
7 $iter = new RecursiveIteratorIterator(
8 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ),
9 RecursiveIteratorIterator::SELF_FIRST,
10 RecursiveIteratorIterator::CATCH_GET_CHILD
11 );
12
13 foreach ( $iter as $fileinfo ) {
14 if ( $fileinfo->isFile() ) {
15 $image_path = $fileinfo->getPathname();
16 $image_url = str_replace( $search, $replace, $image_path );
17 $attachment_id = attachment_url_to_postid( $image_url );
18
19 if ( ! $attachment_id ) {
20 unlink( $image_path );
21 }
22 }
23 }
24}

Na serwerze testowym z 1 GB przesłanych plików funkcja wykonała się w 15 sekund i zwolniła 700 MB, pozostawiając 300 MB faktycznie używanych plików. W przypadku folderów większych niż 5 GB proszę podzielić skanowanie według lat: zastąpić $root przez $uploads_dir['basedir'] . '/2025/', następnie '/2024/' i tak dalej.

Najpierw proszę uruchomić wariant bez usuwania, zastępując unlink( $image_path ) przez echo $image_path . PHP_EOL. Zobaczą Państwo pełną listę plików, które funkcja uznaje za śmieci. Po sprawdzeniu wzrokowym można przywrócić unlink().

5. Scenariusz z polami niestandardowymi: gdy obrazy nie są załącznikami

Najtrudniejszy przypadek: strona, na której obrazy nie są przechowywane jako załączniki WordPress, lecz jako adresy URL w polach niestandardowych (ACF, Meta Box, własne pola motywu). Typowy przykład to księgarnia internetowa: okładka książki w polu bookcover, zdjęcie autora w bookauthor_picture, obrazek z listą w book_list_pictrue.

W takiej architekturze dla wszystkich plików w uploads funkcja attachment_url_to_postid() zwróci 0. Poprzednia funkcja usunęłaby absolutnie wszystko, włącznie z faktycznie używanymi obrazami. Potrzebne jest inne podejście.

5.1. Tworzymy białą listę

Najpierw zbieramy adresy URL wszystkich obrazów ze wszystkich potrzebnych pól niestandardowych. W poniższym przykładzie trzy CPT i trzy pola:

1$all_good_pictures = array();
2
3// Обложки книг (CPT 'post', поле 'bookcover')
4$posts = get_posts( array(
5 'post_type' => 'post',
6 'posts_per_page' => -1,
7 'post_status' => 'any',
8 'fields' => 'ids',
9) );
10foreach ( $posts as $post_id ) {
11 $cover = get_field( 'bookcover', $post_id );
12 if ( $cover ) {
13 $all_good_pictures[] = $cover;
14 }
15}
16
17// Изображения списков (CPT 'book_list', поле 'book_list_pictrue')
18$lists = get_posts( array(
19 'post_type' => 'book_list',
20 'posts_per_page' => -1,
21 'post_status' => 'any',
22 'fields' => 'ids',
23) );
24foreach ( $lists as $list_id ) {
25 $pic = get_field( 'book_list_pictrue', $list_id );
26 if ( $pic ) {
27 $all_good_pictures[] = $pic;
28 }
29}
30
31// Фото авторов (CPT 'bookauthor', поле 'bookauthor_picture')
32$authors = get_posts( array(
33 'post_type' => 'bookauthor',
34 'posts_per_page' => -1,
35 'post_status' => 'any',
36 'fields' => 'ids',
37) );
38foreach ( $authors as $author_id ) {
39 $pic = get_field( 'bookauthor_picture', $author_id );
40 if ( $pic ) {
41 $all_good_pictures[] = $pic;
42 }
43}
44
45$all_good_pictures = array_filter( $all_good_pictures );

W rzeczywistym projekcie, księgarni internetowej, to podejście pozwoliło wykryć większość niepotrzebnych plików i zwolnić znaczną część przestrzeni dyskowej.

5.2. Usuwamy wszystko, czego nie ma na białej liście

Teraz przechodzimy przez wp-content/uploads i usuwamy każdy plik, którego nie ma w $all_good_pictures:

1$uploads_dir = wp_upload_dir();
2$search = $uploads_dir['basedir'];
3$replace = $uploads_dir['baseurl'];
4$root = $uploads_dir['basedir'];
5
6$iter = new RecursiveIteratorIterator(
7 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ),
8 RecursiveIteratorIterator::SELF_FIRST,
9 RecursiveIteratorIterator::CATCH_GET_CHILD
10);
11
12foreach ( $iter as $fileinfo ) {
13 if ( $fileinfo->isFile() ) {
14 $image_path = $fileinfo->getPathname();
15 $image_url = str_replace( $search, $replace, $image_path );
16
17 if ( ! in_array( $image_url, $all_good_pictures, true ) ) {
18 unlink( $image_path );
19 }
20 }
21}

Metoda in_array() ze ścisłym porównaniem na tablicy liczącej ponad 10 000 elementów nie jest najszybsza. W przypadku wolumenów produkcyjnych proszę zastąpić zwykłą tablicę tablicą asocjacyjną: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) i sprawdzać za pomocą isset(). Różnica przy 40 000 elementów to spadek czasu wykonania z kilkunastu sekund do ułamków sekundy.

Jak uruchomić te funkcje

Wszystkie powyższe snippety to definicje funkcji. Nie robią one nic, dopóki ich Pan/Pani nie wywoła. Trzy bezpieczne sposoby uruchomienia:

Sposób

Kiedy stosować

Wycofanie

WP-CLI wp eval-file

Jednorazowe czyszczenie, dostęp do konsoli

Brak, tylko kopia zapasowa

Hak admin_init + parametr URL

Brak konsoli, potrzeba uruchomienia z panelu administracyjnego

Brak, tylko kopia zapasowa

Wtyczka do snippetów (WPCode)

Wygodne przechowywanie i włączanie/wyłączanie

Wyłączono snippet, funkcja nieaktywna

Przykład jednorazowego uruchomienia przez panel administracyjny:

1add_action( 'admin_init', 'run_cleanup_once' );
2function run_cleanup_once() {
3 if ( isset( $_GET['cleanup'] ) && 'confirmed' === $_GET['cleanup'] ) {
4 delete_unattached_attachments();
5 }
6}

Proszę przejść pod https://вашсайт.com/wp-admin/?cleanup=confirmed, funkcja wykona się jednorazowo. Po wykonaniu proszę usunąć snippet.

Dla WP-CLI, zalecanego sposobu dla środowiska produkcyjnego, proszę zapisać kod funkcji w pliku tymczasowym i wykonać:

1wp eval-file cleanup.php

Przed czyszczeniem warto zobaczyć proces wizualnie. Na poniższym filmie znajduje się szczegółowa analiza czyszczenia biblioteki mediów WordPress metodami ręcznymi i automatycznymi.

⁉️🤔 Często zadawane pytania

Czy można odzyskać pliki po usunięciu?

Nie. Funkcje używają wp_delete_attachment() z true oraz unlink(), pliki są usuwane fizycznie, z pominięciem kosza. Jedynym zabezpieczeniem jest pełna kopia zapasowa plików i bazy danych przed uruchomieniem. Proszę sprawdzić, czy dostawca hostingu oferuje automatyczne codzienne kopie zapasowe, w Kinsta, WP Engine i SiteGround są one domyślnie włączone. Daje to dodatkowy punkt przywracania, oprócz Pana/Pani ręcznej kopii zapasowej.

Dlaczego funkcja nie zadziałała, pliki pozostały na miejscu?

Najczęstsza przyczyna: dodał/a Pan/Pani definicję funkcji w functions.php, ale jej nie wywołał/a. Blok function ... { } to tylko instrukcja. Aby kod się wykonał, funkcję należy podpiąć do haka przez add_action() lub uruchomić ręcznie przez WP-CLI. W sekcji „Jak uruchomić" są trzy sposoby, proszę wybrać odpowiedni do swojego poziomu dostępu do serwera.

Czy funkcja usunie niepodpięte załączniki-miniaturki używanych obrazów?

Nie. Miniatury (thumbnail, medium, large) mają ten sam post_parent co oryginalny załącznik. Funkcja filtruje ściśle według post_parent = 0, tylko wpisy, które w ogóle nie mają postu nadrzędnego. Rozmiary oryginałów dziedziczą post_parent i nie trafiają do wyników. Problem pojawia się tylko wtedy, gdy sam oryginał utracił powiązanie, wówczas funkcja usunie go wraz ze wszystkimi rozmiarami.

Co zrobić, jeśli część obrazów jest w polach niestandardowych, a część jako zwykłe załączniki?

Proszę połączyć podejścia z sekcji 4 i 5. Najpierw proszę zebrać białą listę z pól niestandardowych (sekcja 5.1). Następnie podczas skanowania katalogu uploads (sekcja 4) dla każdego pliku proszę sprawdzać oba warunki: czy plik jest załącznikiem WordPress przez attachment_url_to_postid() ORAZ czy znajduje się na białej liście. Plik jest usuwany tylko wtedy, gdy nie jest spełniony żaden z warunków: if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.

Na ile jest to bezpieczne dla strony WooCommerce?

WooCommerce przechowuje obrazy produktów jako standardowe załączniki WordPress, są one podpięte do postu typu product. Funkcja usuwania niepodpiętych załączników (sekcja 1) ich nie naruszy. Natomiast funkcja dla konkretnego CPT (sekcja 2) tak, jeśli przekaże Pan/Pani 'product'. Dla WooCommerce najbezpieczniej jest użyć metody z sekcji 5 (biała lista): operuje ona na tym, co jest faktycznie używane, a nie na tym, co jest podpięte. Przed uruchomieniem proszę wykonać eksport ID wszystkich załączników produktowych w celu weryfikacji.

Co wdrożyć w swoim projekcie: podsumowanie

Wybór metody zależy od architektury strony:

  • Standardowy blog lub strona informacyjna, wystarczą funkcje z sekcji 1 (niepodpięte załączniki) i sekcji 3 (załączniki 404). Proszę uruchomić raz na pół roku, biblioteka mediów będzie w porządku.
  • Strona ze starymi CPT (portfolio, katalog, tablica ogłoszeń), proszę dodać sekcję 2. Punktowo wyczyści Pan/Pani śmieci po usuniętych lub porzuconych typach wpisów.
  • Projekt na ACF/Meta Box z polami niestandardowymi dla obrazów, Pana/Pani wariant to sekcja 5. Proszę zebrać białą listę, resztę usunąć. Raz skonfigurować, potem tylko powtarzać w razie potrzeby.
  • Wszystko razem i nie wiadomo co, proszę zacząć od rekurencyjnego skanowania katalogu uploads (sekcja 4). Proszę zobaczyć, ile śmieci zalega na dysku. Następnie punktowo zastosować sekcje 1-3 i 5 w zależności od sytuacji.

Żaden z tych skryptów nie zastąpi regularnej higieny strony. Ale pisząc raz potrzebną funkcję i zapisując ją w dokumentacji projektu, zaoszczędzi Pan/Pani godziny ręcznej pracy podczas następnej rewizji.

I tak, kopię zapasową już Pan/Pani wykonał/a.

Czyszczenie biblioteki multimediów WordPress ze śmieciowych plików