किसी फ़ोल्डर पर du -sh चलाएँ और उसे Finder के साइज़ से मिलाएँ, तो दोनों अक्सर अलग निकलते हैं, कभी-कभी कई गीगाबाइट का फ़र्क़। दोनों में से कोई ग़लत नहीं है। वे अलग सवालों का जवाब देते हैं: फ़ाइलें कितनी लंबी हैं, और वे डिस्क की कितनी जगह घेरती हैं। डिस्क भर जाने पर सिर्फ़ दूसरा सवाल मायने रखता है, और उसे सही निकालने के लिए सिर्फ़ st_size जोड़ना काफ़ी नहीं है।
नीचे के नियम osFoundry डेस्कटॉप ऐप की Disk स्क्रीन फ़ॉलो करती है। उसका स्कैनर std::fs के ऊपर एक छोटा Rust फ़ंक्शन है, और वही लॉजिक करीब 40 लाइन के Python में आ जाता है। सारे नंबर 4 KB एलोकेशन ब्लॉक वाले APFS वॉल्यूम पर एक टेस्ट फ़ोल्डर से लिए गए हैं।
बाइट नहीं, ब्लॉक गिनें
st_size फ़ाइल की लंबाई है। st_blocks वह संख्या है जितनी 512-बाइट की इकाइयाँ फ़ाइल सिस्टम ने उस फ़ाइल को दी हैं, चाहे वॉल्यूम का असली ब्लॉक साइज़ कुछ भी हो। ऐप का स्कैनर डिफ़ॉल्ट रूप से दूसरे वाले का इस्तेमाल करता है:
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()
}
}
टेस्ट फ़ोल्डर की तीन फ़ाइलें बताती हैं कि क्यों:
| फ़ाइल | st_size |
st_blocks × 512 |
|---|---|---|
small.txt (2 बाइट का टेक्स्ट) |
2 | 4,096 |
sparse.img (1 GiB तक ट्रंकेट की गई, कभी लिखी नहीं गई) |
1,073,741,824 | 0 |
big.bin (200 MiB डेटा) |
209,715,200 | 209,715,200 |
छोटी-सी फ़ाइल भी एक पूरा ब्लॉक घेरती है, इसलिए 100,000 छोटी फ़ाइलों वाला फ़ोल्डर डिस्क पर अपनी बाइट-गिनती से कई सौ मेगाबाइट बड़ा होता है। स्पार्स फ़ाइल इसका उलटा है: VM डिस्क, डेटाबेस फ़ाइलें और कुछ डाउनलोड बड़ी लंबाई रिज़र्व कर लेते हैं और ब्लॉक सिर्फ़ उन हिस्सों के लिए लेते हैं जिनमें कुछ लिखा गया है। macOS का du ब्लॉक गिनता है; du -A उसकी जगह apparent साइज़ दिखाता है। ऐप का Apparent size बॉक्स यही स्विच करता है।
लिंक फ़ॉलो न करें, वॉल्यूम से बाहर न जाएँ
जो स्कैनर सिम्बॉलिक लिंक फ़ॉलो करता है, वह एक ही फ़ोल्डर को दो बार गिन सकता है, या अपने ही पैरेंट की ओर इशारा करने वाले लिंक पर हमेशा घूमता रह सकता है। जो माउंट की हुई ड्राइव में घुस जाए, वह किसी दूसरी डिस्क की फ़ाइलें इस डिस्क के टोटल में जोड़ देता है। ऐप हर एंट्री को symlink_metadata से पढ़ता है, जो लिंक का वर्णन करता है, उसके टार्गेट का नहीं, और डिवाइस ID की तुलना उस फ़ोल्डर से करता है जहाँ से स्कैन शुरू हुआ था:
if child_meta.file_type().is_symlink() || !same_device(root_meta, &child_meta) {
continue;
}
same_device st_dev की तुलना करता है, जो du -x या find -xdev वाली ही जाँच है।
हार्ड लिंक और क्लोन
हार्ड लिंक एक ही फ़ाइल के कई नाम होते हैं। टेस्ट फ़ोल्डर में ln big.bin hard.bin के बाद ls -l दो 200 MiB की फ़ाइलें दिखाता है, लेकिन du डेटा को एक ही बार गिनता है, क्योंकि st_nlink 1 से ऊपर होने पर वह हर देखे हुए (st_dev, st_ino) जोड़े को याद रखता है। ब्लॉक जोड़ने वाले हर स्कैनर को यही करना चाहिए। HFS+ डिस्क पर Time Machine बैकअप और कुछ पैकेज मैनेजर हार्ड लिंक पर काफ़ी निर्भर हैं।
APFS क्लोन ज़्यादा मुश्किल हैं। cp -c big.bin clone.bin (या Finder में Duplicate) एक दूसरी फ़ाइल बनाता है जो मूल फ़ाइल के ब्लॉक तब तक साझा करती है जब तक दोनों में से कोई बदल न जाए। हर फ़ाइल का अपना inode, अपना st_blocks और लिंक काउंट 1 होता है, इसलिए du इस जोड़ी के लिए 400 MiB गिनता है। df से पहले और बाद में नापने पर क्लोन ने फ़्री स्पेस में 0 KB लिया, जबकि उसी फ़ाइल की सामान्य cp ने 204,984 KB लिया। कोई POSIX फ़ील्ड इस साझेदारी को नहीं दिखाता, इसलिए ब्लॉक-आधारित टोटल यह ज़्यादा बताता है कि क्लोन डिलीट करने से कितनी जगह वापस मिलेगी। डुप्लिकेट प्रोजेक्ट या फ़ोटो लाइब्रेरी से भरे फ़ोल्डरों के लिए पहले और बाद में df देखना ही अकेली भरोसेमंद जाँच है।
Python वर्शन
यही नियम, सिर्फ़ स्टैंडर्ड लाइब्रेरी के साथ:
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")
टेस्ट फ़ोल्डर पर यह डिस्क पर 0.39 GiB और apparent 1.39 GiB छापता है, जो du -sk (409,604 KB) और du -sAk से मेल खाता है। 14 GiB के सोर्स ट्री पर यह du -sk से दशमलव के दो अंकों तक मिलता है। स्किप की गई एंट्रियों की गिनती रखें: macOS में प्राइवेसी परमिशन से सुरक्षित फ़ोल्डर (Mail, Messages, Safari) Full Disk Access के बिना नहीं पढ़े जा सकते, और जो टोटल उन्हें चुपचाप छोड़ दे वह असल से ज़्यादा पूरा दिखता है।
ऐप नतीजे को इतना छोटा रखता है कि उसे बनाया जा सके। हर फ़ोल्डर अपने 36 सबसे बड़े चाइल्ड रखता है और बाक़ी सब एक ही "Other items" नोड में जुड़ जाते हैं, इसलिए हज़ार पैकेज वाला node_modules फ़ोल्डर खोलने पर 36 बॉक्स और बाक़ी सबके लिए एक और बॉक्स दिखता है, हज़ार पतली पट्टियाँ नहीं।
सुरक्षित तरीक़े से डिलीट करना
डिस्क टूल का असली जोखिम डिलीट वाले कदम में है। ऐप आइटम को unlink करने की जगह trash क्रेट से सिस्टम Trash में भेजता है, और कुछ भी हिलाने से पहले हर पाथ की जाँच करता है:
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());
}
पाथ पहले कैनोनिकलाइज़ किए जाते हैं, ताकि .. या कोई सिम्बॉलिक लिंक स्कैन किए गए फ़ोल्डर के बाहर का टार्गेट न घुसा सके। ऐसा चयन भी रिजेक्ट होता है जिसमें एक मार्क किया हुआ फ़ोल्डर दूसरे को समेटे हो, क्योंकि पैरेंट के Trash में जाते ही चाइल्ड का पाथ ख़त्म हो जाता है और बैच बीच में फ़ेल हो जाएगा।
डेस्कटॉप ऐप osFoundry डाउनलोड पेज से मुफ़्त है।



Top comments (0)