Skip to content

Tutto per WordPress, lo sviluppo web — e non solo

🗑 Pulizia programmatica della libreria media di WordPress: script PHP contro i file spazzatura

🗑 Pulizia programmatica della libreria media di WordPress: script PHP contro i file spazzatura

Libreria media di WordPress funziona come una soffitta: cancelli un post, le immagini restano. Cambi tema, le vecchie dimensioni delle immagini restano lì come peso morto. Migri il sito, metà delle miniature restituisce 404.

La pulizia manuale dal pannello di amministrazione su un sito con un paio di migliaia di file è un esercizio di meditazione. Ma c'è un modo più veloce: cinque funzioni PHP che trovano e rimuovono la spazzatura in un unico passaggio. Nessun plugin, con codice chiaro e controllo su ogni file eliminato.

Prima di eseguire, backup completo. Queste funzioni eliminano i file in modo permanente: nessun cestino, nessun annullamento. In caso di dubbio, eseguire prima su una copia di staging.

💡 Panoramica rapida:

  • Eliminazione degli allegati non associati, file lasciati dopo la cancellazione dei post
  • Pulizia dei file media da un tipo di post personalizzato (CPT) specifico
  • Pulizia della libreria media dai link non funzionanti, allegati 404 senza un file sul server
  • Ricerca ed eliminazione dei file in wp-content/uploads non registrati come allegati di WordPress
  • Scenario per siti in cui le immagini sono archiviate in campi personalizzati (ACF, Meta Box), non come allegati

Cosa devi sapere prima di eseguire

Il codice seguente elimina i file fisicamente, dal disco e dal database. Tre cose che salveranno il tuo sito.

Primo: le immagini nelle pagine di archivio tag o nelle descrizioni SEO spesso non sono collegate a nessun post. Rimangono come "orfane", ma il sito ne ha bisogno. Se hai immagini di questo tipo, escludile dall'ambito di queste funzioni o modifica le condizioni.

Secondo: WordPress crea diverse dimensioni per ogni immagine. Le miniature ereditano il post_parent dell'originale, quindi la funzione delete_unattached_attachments() non le tocca, filtra rigorosamente per post_parent = 0. Il problema sorge solo se l'originale stesso ha perso il suo collegamento al post.

Terzo: se un link al file eliminato esiste nel contenuto di un post, si romperà dopo la pulizia. Prima di eseguire, scansiona il sito con Screaming Frog o strumenti simili e mappa i link.

1. Eliminare gli allegati non associati

Scenario più comune: hai cancellato un post, gli allegati sono rimasti nel database con post_type = 'attachment' e post_parent = 0. Occupano spazio su disco e nei backup.

La funzione seguente trova tutti questi record e li elimina. Inseriscila nel functions.php di un tema child o tramite un plugin di snippet come WPCode. Non si esegue da sola, è una definizione che necessita di una chiamata.

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() seleziona tutti gli allegati senza un post padre. wp_delete_attachment() con il parametro true cancella sia il record del database che il file con le miniature. L'ulteriore unlink() è un'assicurazione: se il file è in qualche modo rimasto sul disco, viene eliminato forzatamente.

Nota: le immagini in evidenza hanno anch'esse post_parent = 0 in alcune configurazioni. Prima dell'esecuzione in produzione, sostituisci wp_delete_attachment con echo $attachment_id . '<br>', vedrai l'elenco degli ID che verranno eliminati. Una volta confermato che tutto è corretto, ripristina la versione di produzione.

Dopo una singola esecuzione, rimuovi la funzione da functions.php. Non è necessario mantenerla su ogni init.

2. Eliminare gli allegati di un CPT specifico

Ex negozio WooCommerce, vecchia sezione portfolio, tipo di post personalizzato eliminato, tutte le loro immagini continuano a risiedere sul server. La funzione seguente pulisce gli allegati collegati ai post di un tipo specificato.

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}

Sostituisci 'card' con lo slug del tuo CPT. Per i prodotti WooCommerce, 'product'. Se il CPT è già stato eliminato, get_post_type() restituirà false, gli allegati di quel tipo non saranno interessati. Per i CPT eliminati, la logica necessita di un aggiustamento: verificare non il tipo del padre, ma l'appartenenza a una tassonomia o a un meta campo.

Su database di grandi dimensioni, fai attenzione: 'numberposts' => -1 senza 'fields' => 'ids' carica oggetti WP_Post completi. Su oltre 10.000 allegati questo può raggiungere il memory_limit. Per volumi di produzione, aggiungi 'fields' => 'ids' e ottieni solo gli ID, get_post_type() funzionerà anche con gli ID padre.

3. Pulire la libreria media dagli allegati 404

Miniature rotte nella libreria media sono un sintomo che il file su disco è stato eliminato (manualmente, per crash dell'hosting o plugin difettoso), ma il record del database è rimasto. WordPress mostra un rettangolo grigio, ma al click, 404.

La funzione interroga l'URL di ogni allegato ed elimina quelli che restituiscono 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}

Importante: questa funzione è dispendiosa in termini di risorse. Ogni chiamata get_headers() è una richiesta HTTP al tuo stesso server. Su mille allegati effettui mille richieste HTTP in un solo passaggio. Risultato: lentezza, carico del server, alcuni provider di hosting terminano il processo per timeout.

Per librerie media di grandi dimensioni, suddividi in blocchi con 'offset' e 'numberposts' oppure esegui tramite WP-CLI con un limite batch. Se il sito è dietro CDN o proxy, sostituisci il controllo con wp_remote_head() con un timeout, get_headers() non sempre gestisce correttamente i redirect e non supporta l'autenticazione.

4. Verifica inversa: file negli upload senza un record nel database

Le tre funzioni precedenti puliscono il database, eliminano i record degli allegati. Ma wp-content/uploads potrebbe contenere file che non sono affatto registrati come allegati: caricati via FTP, lasciati da plugin, generati dalla cache.

Questa funzione procede in modo opposto: non dal database ai file, ma dai file al database. Scansiona ricorsivamente wp-content/uploads e per ogni file verifica tramite attachment_url_to_postid() se è un allegato. In caso contrario, lo elimina.

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}

Su un server di test con 1 GB di upload, la funzione è stata eseguita in 15 secondi e ha liberato 700 MB, lasciando 300 MB di file effettivamente utilizzati. Per cartelle superiori a 5 GB, suddividi la scansione per anni: sostituisci $root con $uploads_dir['basedir'] . '/2025/', poi '/2024/' e così via.

Prima esegui la versione senza eliminazione, sostituisci unlink( $image_path ) con echo $image_path . PHP_EOL. Vedrai l'elenco completo dei file che la funzione considera spazzatura. Controlla visivamente, poi ripristina unlink().

5. Scenario con campi personalizzati: quando le immagini non sono allegati

Il caso più complesso: un sito in cui le immagini non sono archiviate come allegati di WordPress, ma come URL in campi personalizzati (ACF, Meta Box, campi tema personalizzati). Esempio tipico, un negozio di libri: copertina del libro nel campo bookcover, foto dell'autore in bookauthor_picture, immagine di elenco in book_list_pictrue.

In questa architettura, per tutti i file negli upload attachment_url_to_postid() restituirà 0. La funzione precedente eliminerà tutto, incluse le immagini effettivamente utilizzate. È necessario un approccio diverso.

5.1. Costruire una whitelist

Per prima cosa raccogli gli URL di tutte le immagini da tutti i campi personalizzati necessari. Nell'esempio seguente, tre CPT e tre campi:

1$all_good_pictures = array();
2
3// Book covers (CPT 'post', field '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// List images (CPT 'book_list', field '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// Author photos (CPT 'bookauthor', field '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 );

Su un progetto reale, un negozio di libri, questo approccio ha permesso di calcolare la maggior parte dei file spazzatura e liberare una porzione significativa di spazio su disco.

5.2. Eliminare tutto ciò che non è nella whitelist

Ora percorri wp-content/uploads ed elimina ogni file non presente in $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}

Il metodo in_array() con confronto stretto su un array di oltre 10.000 elementi non è il più veloce. Per volumi di produzione, sostituisci l'array normale con uno associativo: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) e verifica tramite isset(). La differenza su 40.000 elementi, da decine di secondi a frazioni di secondo.

Come eseguire queste funzioni

Tutti gli snippet sopra sono definizioni di funzioni. Non fanno nulla finché non le chiami. Tre modi sicuri per eseguirle:

Metodo

Quando usarlo

Rollback

WP-CLI wp eval-file

Pulizia una tantum, accesso alla console

No, solo backup

Hook admin_init + parametro URL

Nessuna console, necessità di eseguire da admin

No, solo backup

Plugin di snippet (WPCode)

Archiviazione comoda e abilitazione/disabilitazione

Disabilita snippet, funzione inattiva

Esempio di esecuzione una tantum tramite admin:

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}

Visita https://iltuosito.com/wp-admin/?cleanup=confirmed, la funzione viene eseguita una volta. Dopo l'esecuzione, rimuovi lo snippet.

Per WP-CLI, il metodo consigliato per la produzione, salva il codice della funzione in un file temporaneo ed esegui:

1wp eval-file cleanup.php

Prima della pulizia, è utile vedere il processo visivamente. Nel video qui sotto, un'analisi passo passo della pulizia della libreria media di WordPress con metodi manuali e automatici.

⁉️🤔 Domande frequenti

I file possono essere ripristinati dopo l'eliminazione?

No. Le funzioni usano wp_delete_attachment() con true e unlink(), i file vengono eliminati fisicamente, bypassando il cestino. L'unica assicurazione: backup completo di file e database prima dell'esecuzione. Verifica se il tuo provider di hosting dispone di backup giornalieri automatici, su Kinsta, WP Engine e SiteGround sono abilitati per impostazione predefinita. Questo fornisce un punto di ripristino aggiuntivo oltre al tuo backup manuale.

Perché la funzione non ha funzionato, i file sono rimasti al loro posto?

Motivo più comune: hai aggiunto la definizione della funzione a functions.php, ma non l'hai chiamata. Un blocco function ... { } è solo un'istruzione. Perché il codice venga eseguito, la funzione deve essere agganciata a un hook tramite add_action() o eseguita manualmente tramite WP-CLI. Nella sezione "Come eseguire", tre metodi, scegli in base al tuo livello di accesso al server.

La funzione eliminerà le miniature delle immagini utilizzate se sono allegati non associati?

No. Le miniature (thumbnail, medium, large) hanno lo stesso post_parent dell'allegato originale. La funzione filtra rigorosamente per post_parent = 0, solo record senza alcun post padre. Le dimensioni degli originali ereditano post_parent e non rientrano nella selezione. Il problema sorge solo se l'originale stesso ha perso il suo collegamento, allora la funzione lo eliminerà insieme a tutte le dimensioni.

Cosa succede se alcune immagini sono in campi personalizzati e altre sono allegati regolari?

Combina gli approcci delle sezioni 4 e 5. Prima raccogli una whitelist dai campi personalizzati (sezione 5.1). Poi quando scansioni gli upload (sezione 4) per ogni file, verifica entrambe le condizioni: il file è un allegato WordPress tramite attachment_url_to_postid() ED è presente nella whitelist. Un file viene eliminato solo se nessuna delle due condizioni è soddisfatta: if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.

Quanto è sicuro per un sito WooCommerce?

WooCommerce archivia le immagini dei prodotti come allegati standard di WordPress, sono collegate al tipo di post product. La funzione di eliminazione degli allegati non associati (sezione 1) non le toccherà. Ma la funzione per un CPT specifico (sezione 2), sì, se passi 'product'. Per WooCommerce il metodo più sicuro è quello della sezione 5 (whitelist): opera su ciò che è effettivamente utilizzato, non su ciò che è collegato. Prima di eseguire, esporta gli ID di tutti gli allegati dei prodotti per un controllo incrociato.

Cosa usare sul tuo progetto: riepilogo finale

La scelta del metodo dipende dall'architettura del sito:

  • Blog o sito di notizie standard, le funzioni della sezione 1 (allegati non associati) e della sezione 3 (allegati 404) sono sufficienti. Esegui una volta ogni sei mesi, la libreria media sarà a posto.
  • Sito con vecchi CPT (portfolio, catalogo, annunci), aggiungi la sezione 2. Pulisci con precisione la spazzatura da tipi di post eliminati o abbandonati.
  • Progetto su ACF/Meta Box con campi personalizzati per le immagini, la tua opzione: sezione 5. Raccogli una whitelist, elimina il resto. Configura una volta, poi ripeti semplicemente al bisogno.
  • Tutto insieme e non chiaro, inizia con la scansione ricorsiva degli upload (sezione 4). Vedi quanta spazzatura risiede sul disco. Poi applica le sezioni 1-3 e 5 selettivamente in base alla situazione.

Nessuno di questi script sostituisce una regolare igiene del sito. Ma una volta scritta la funzione necessaria e salvata nella documentazione di progetto, risparmierai ore di lavoro manuale al prossimo audit.

E sì, hai già fatto un backup.

Pulizia della libreria media di WordPress dai file spazzatura