Lancez du -sh sur un dossier et comparez avec la taille affichée par le Finder : les deux valeurs divergent souvent, parfois de plusieurs gigaoctets. Aucune des deux n'est fausse. Elles répondent à des questions différentes : quelle est la longueur des fichiers, et quelle place ils occupent sur le disque. Quand le disque est plein, seule la seconde compte, et la calculer correctement demande plus que d'additionner st_size.
Les règles ci-dessous sont celles que suit l'écran Disk de l'application de bureau osFoundry. Son scanner est une courte fonction Rust au-dessus de std::fs, et la même logique tient en une quarantaine de lignes de Python. Tous les chiffres viennent d'un dossier de test sur un volume APFS avec des blocs d'allocation de 4 Ko.
Compter les blocs, pas les octets
st_size est la longueur du fichier. st_blocks est le nombre d'unités de 512 octets que le système de fichiers lui a allouées, quelle que soit la taille de bloc réelle du volume. Le scanner de l'application utilise par défaut la seconde :
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() } } Trois fichiers du dossier de test montrent pourquoi :
| Fichier | st_size | st_blocks × 512 |
|---|---|---|
small.txt (2 octets de texte) | 2 | 4,096 |
sparse.img (tronqué à 1 Gio, jamais écrit) | 1,073,741,824 | 0 |
big.bin (200 Mio de données) | 209,715,200 | 209,715,200 |
Un tout petit fichier occupe quand même un bloc entier : un dossier de 100 000 petits fichiers pèse donc plusieurs centaines de mégaoctets de plus sur le disque que son nombre d'octets. Un fichier creux (sparse) est l'inverse : disques de machines virtuelles, fichiers de bases de données et certains téléchargements réservent une grande longueur et n'utilisent des blocs que pour les parties écrites. Le du de macOS compte les blocs ; du -A affiche la taille apparente. La case Apparent size de l'application fait la même bascule.
Ne pas suivre les liens, ne pas quitter le volume
Un scanner qui suit les liens symboliques peut compter deux fois le même dossier, ou tourner indéfiniment sur un lien qui pointe vers son propre parent. Celui qui entre dans un disque monté ajoute les fichiers d'un autre disque au total de celui-ci. L'application lit chaque entrée avec symlink_metadata, qui décrit le lien et non sa cible, et compare les identifiants de périphérique avec ceux du dossier de départ du scan :
if child_meta.file_type().is_symlink() || !same_device(root_meta, &child_meta) { continue; } same_device compare st_dev, ce qui est la même vérification que du -x ou find -xdev.
Liens physiques et clones
Les liens physiques (hard links) sont un seul fichier avec plusieurs noms. Dans le dossier de test, ln big.bin hard.bin fait afficher à ls -l deux fichiers de 200 Mio, mais du ne compte les données qu'une fois, car il mémorise chaque paire (st_dev, st_ino) déjà vue dès que st_nlink dépasse 1. Tout scanner qui additionne des blocs devrait faire de même. Les sauvegardes Time Machine sur disques HFS+ et certains gestionnaires de paquets s'appuient beaucoup sur les liens physiques.
Les clones APFS sont plus délicats. cp -c big.bin clone.bin (ou Dupliquer dans le Finder) crée un second fichier qui partage les blocs de l'original jusqu'à ce que l'un des deux change. Chaque fichier a son propre inode, son propre st_blocks et un compteur de liens égal à 1, donc du compte 400 Mio pour la paire. Mesuré avec df avant et après, le clone a consommé 0 Ko d'espace libre, alors qu'un cp normal du même fichier en a consommé 204 984 Ko. Aucun champ POSIX n'indique ce partage : un total fondé sur les blocs surestime donc ce que libère la suppression d'un clone. Pour des dossiers pleins de projets dupliqués ou de photothèques, comparer df avant et après est la seule vérification fiable.
Une version Python
Les mêmes règles, avec la seule bibliothèque 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") Sur le dossier de test, le script affiche 0,39 Gio sur le disque et 1,39 Gio en taille apparente, ce qui correspond à du -sk (409 604 Ko) et à du -sAk. Sur une arborescence de sources de 14 Gio, il concorde avec du -sk à deux décimales près. Gardez le compte des entrées ignorées : sur macOS, les dossiers protégés par les autorisations de confidentialité (Mail, Messages, Safari) sont illisibles sans l'accès complet au disque, et un total qui les omet en silence paraît plus complet qu'il ne l'est.
L'application garde le résultat assez petit pour être dessiné. Chaque dossier conserve ses 36 plus gros enfants et le reste est additionné dans un seul nœud « Other items » ; ouvrir un dossier node_modules de mille paquets affiche donc 36 cases et une de plus pour tout le reste, pas mille fragments.
Supprimer sans risque
Le vrai risque d'un outil de disque se trouve à l'étape de suppression. L'application déplace les éléments vers la corbeille du système avec le crate trash plutôt que de les supprimer par unlink, et vérifie chaque chemin avant de déplacer quoi que ce soit :
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()); } Les chemins sont d'abord canonisés, de sorte que .. ou un lien symbolique ne peut pas glisser une cible en dehors du dossier analysé. Une sélection où un dossier coché en contient un autre est aussi refusée : une fois le parent dans la corbeille, le chemin de l'enfant n'existe plus et le lot échouerait à mi-parcours.
L'application de bureau est gratuite sur la page de téléchargement d'osFoundry.



![Laravel 13 Puts #[Table] on Your Models. PHP Won't Load That Class Until newInstance()](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcz4ytbajureytql7wwi4.jpg)
