Esegui du -sh su una cartella e confrontalo con la dimensione del Finder: i due valori spesso non coincidono, a volte di diversi gigabyte. Nessuno dei due sbaglia. Rispondono a domande diverse: quanto sono lunghi i file e quanto spazio occupano sul disco. Quando il disco è pieno conta solo la seconda, e calcolarla bene richiede più che sommare st_size.
Le regole qui sotto sono quelle seguite dalla schermata Disk dell'app desktop osFoundry. Il suo scanner è una breve funzione Rust su std::fs, e la stessa logica sta in circa 40 righe di Python. Tutti i numeri vengono da una cartella di prova su un volume APFS con blocchi di allocazione da 4 KB.
Conta i blocchi, non i byte
st_size è la lunghezza del file. st_blocks è il numero di unità da 512 byte che il file system gli ha allocato, qualunque sia la dimensione reale del blocco del volume. Lo scanner dell'app usa di default la seconda:
fn allocated_bytes(meta: &fs::Metadata, apparent: bool) -> u64 { if apparent { return meta.len(); } #[cfg(unix)] { use std::os::unix::fs::MetadataExt; meta.blocks().saturating_mul(512) } #[cfg(not(unix))] { meta.len() } } Tre file della cartella di prova mostrano il perché:
| File | st_size | st_blocks × 512 |
|---|---|---|
small.txt (2 byte di testo) | 2 | 4,096 |
sparse.img (troncato a 1 GiB, mai scritto) | 1,073,741,824 | 0 |
big.bin (200 MiB di dati) | 209,715,200 | 209,715,200 |
Un file minuscolo occupa comunque un blocco intero, quindi una cartella con 100.000 file piccoli pesa su disco diverse centinaia di megabyte in più del suo conteggio in byte. Un file sparso (sparse) è l'opposto: dischi di macchine virtuali, file di database e alcuni download riservano una lunghezza grande e usano blocchi solo per le parti scritte. Il du di macOS conta i blocchi; du -A stampa invece la dimensione apparente. La casella Apparent size nell'app fa lo stesso passaggio.
Non seguire i link, non uscire dal volume
Uno scanner che segue i collegamenti simbolici può contare due volte la stessa cartella, o girare all'infinito su un link che punta alla propria cartella madre. Uno che entra in un'unità montata somma i file di un altro disco al totale di questo. L'app legge ogni voce con symlink_metadata, che descrive il link e non la sua destinazione, e confronta gli ID del dispositivo con quelli della cartella da cui è partita la scansione:
if child_meta.file_type().is_symlink() || !same_device(root_meta, &child_meta) { continue; } same_device confronta st_dev, ovvero lo stesso controllo di du -x o find -xdev.
Hard link e cloni
Gli hard link sono un solo file con più nomi. Nella cartella di prova, dopo ln big.bin hard.bin, ls -l mostra due file da 200 MiB, ma du conta i dati una volta sola perché ricorda ogni coppia (st_dev, st_ino) già vista quando st_nlink è maggiore di 1. Qualsiasi scanner che somma i blocchi dovrebbe fare lo stesso. I backup di Time Machine su dischi HFS+ e alcuni gestori di pacchetti usano molto gli hard link.
I cloni APFS sono più difficili. cp -c big.bin clone.bin (o Duplica nel Finder) crea un secondo file che condivide i blocchi dell'originale finché uno dei due non cambia. Ogni file ha il proprio inode, il proprio st_blocks e un conteggio dei link pari a 1, quindi du conta 400 MiB per la coppia. Misurato con df prima e dopo, il clone ha occupato 0 KB di spazio libero, mentre un normale cp dello stesso file ne ha occupati 204.984 KB. Nessun campo POSIX indica la condivisione, quindi un totale basato sui blocchi sovrastima ciò che si recupera eliminando un clone. Per cartelle piene di progetti duplicati o librerie di foto, df prima e dopo è l'unico controllo affidabile.
Una versione in Python
Le stesse regole, con la sola libreria standard:
import os import stat import sys def usage(root): root_dev = os.lstat(root).st_dev seen = set() allocated = apparent = 0 skipped = 0 stack = [root] while stack: path = stack.pop() try: entries = list(os.scandir(path)) except OSError: skipped += 1 continue for e in entries: try: st = e.stat(follow_symlinks=False) except OSError: skipped += 1 continue if stat.S_ISLNK(st.st_mode) or st.st_dev != root_dev: continue if stat.S_ISDIR(st.st_mode): stack.append(e.path) continue if st.st_nlink > 1: key = (st.st_dev, st.st_ino) if key in seen: continue seen.add(key) allocated += st.st_blocks * 512 apparent += st.st_size return allocated, apparent, skipped if __name__ == "__main__": alloc, appar, skipped = usage(sys.argv[1]) print(f"on disk {alloc / 2**30:8.2f} GiB") print(f"apparent {appar / 2**30:8.2f} GiB") if skipped: print(f"skipped {skipped} unreadable entries") Sulla cartella di prova stampa 0,39 GiB su disco e 1,39 GiB apparenti, in linea con du -sk (409.604 KB) e du -sAk. Su un albero di sorgenti da 14 GiB coincide con du -sk fino alla seconda cifra decimale. Conserva il conteggio delle voci saltate: su macOS le cartelle protette dai permessi di privacy (Mail, Messaggi, Safari) non si possono leggere senza l'Accesso completo al disco, e un totale che le esclude in silenzio sembra più completo di quanto sia.
L'app mantiene il risultato abbastanza piccolo da poterlo disegnare. Ogni cartella conserva i suoi 36 figli più grandi e il resto viene sommato in un unico nodo «Other items»; aprendo una cartella node_modules con mille pacchetti si vedono 36 riquadri e uno in più per tutto il resto, non mille scaglie.
Eliminare in sicurezza
Il vero rischio di uno strumento per il disco sta nel passaggio di eliminazione. L'app sposta gli elementi nel Cestino di sistema con il crate trash anziché rimuoverli con unlink, e controlla ogni percorso prima di spostare qualsiasi cosa:
if !target.starts_with(&root) || target == root || home.as_ref() == Some(&target) { return Err("Cannot move a scan root or home folder to Trash".into()); } if mount_points .iter() .any(|mount| target == *mount || target.parent() == Some(mount.as_path())) { return Err("Cannot move a volume or its top-level items to Trash".into()); } I percorsi vengono prima canonicalizzati, così .. o un collegamento simbolico non possono introdurre una destinazione fuori dalla cartella scansionata. Viene respinta anche una selezione in cui una cartella contrassegnata ne contiene un'altra, perché una volta che la cartella madre è nel Cestino il percorso del figlio non esiste più e il lotto fallirebbe a metà.
L'app desktop è gratuita dalla pagina di download di osFoundry.




