قال GNU ddrescue إن النسبة بلغت 100.00%. أما أول محاولة منظّمة أجريتها لاستعادة الملفات فقد أنتجت 0 useful recovered user files.
هاتان النتيجتان خرجتا من تعطل القرص الصلب نفسه ذي سعة 4 TB، وهذا التناقض هو ما جعل عملية الاستعادة أكثر إثارة للاهتمام بكثير من مجرد «استنسخ القرص التالف وانسخ الملفات». كان ddrescue قد أدى مهمته بكفاءة لافتة: فقد نسخ تقريبًا 99.999654% من المصدر المادي. وكانت النسخة المستنسخة موجودة على عتاد سليم. ومع ذلك ظل APFS تالفًا، ولم يتمكن macOS من إتاحة نظام الملفات لي بالطريقة المعتادة، كما أن أول حزمة استعادة لـ APFS استطاعت تعداد أجزاء كبيرة من شجرة الأدلة لكنها فشلت في قراءة محتويات الملفات العادية.
في النهاية استعدت كل شجرة مجلدات محددة ومعدودة كنت أحتاج إليها، لكن ذلك لم يحدث إلا بعد أن تعاملت مع الحادثة باعتبارها ثلاث مشكلات منفصلة: استعادة الكتل المادية، وتفسير نظام الملفات التالف، واستخراج الملفات على نطاق واسع مع التحقق ودعم الاستئناف.
توقف العطل أولًا عن كونه مشكلة نظام ملفات
وصل قرص Toshiba الأصلي بسعة 4 TB إلى مرحلة لم أعد أثق فيها بإجراء نشاط عادي لنظام الملفات عليه. كانت بعض عمليات القراءة تستغرق 60–75 ثانية. وكان يمكن للعمليات أن تتجمد. وكان القرص يختفي من macOS على نحو متقطع، ويصدر صوت نقر مسموعًا، وأحيانًا يفصل طاقته.
قبل العطل بقليل، كنت قد كتبت نحو 300 GB من البيانات الإضافية ونفذت إعادة تسمية جماعية طالت قرابة 500,000 ملف ودليل. جعل التوقيت هذا العبء الكثيف على البيانات الوصفية موضع اشتباه، لكن لا يمكنني إثبات أنه تسبب في تعطل العتاد. وربما لم يفعل سوى الضغط على قرص غير سليم أصلًا بما يكفي لكشف المشكلة.
ما استطعت إثباته هو سلوك القرص. عندما يبدأ التخزين الميكانيكي بالتعطل والتوقف والاختفاء وإصدار النقرات، يصبح تصفح الأدلة مرارًا هو التجريد الخاطئ للمشكلة. فقد يؤدي اجتياز الأدلة إلى مزيد من عمليات القراءة والبحث. وقد يؤدي تركيب نظام الملفات إلى أعمال إضافية على البيانات الوصفية. وكل تجربة تستهلك وقتًا من المكوّن الوحيد الذي لا أعرف كم بقي من عمره القابل للاستخدام.
لذلك غيّرت الهدف من:
recover my files
إلى:
recover as many readable sectors as possible
استخدمت GNU ddrescue 1.30 مع mapfile دائم. وكان الحجم المادي الدقيق للقرص المصدر:
4,000,787,027,968 bytes
كان الـ mapfile ضروريًا لأن المصدر لم يكن مستقرًا بما يكفي لنسخة من محاولة واحدة. فقد سمح لعملية الإنقاذ بتجاوز حالات التوقف والانقطاع وإعادة التشغيل والجولات اللاحقة من دون نسيان المناطق التي سبق استعادتها.
واجهت أيضًا مشكلة عملية مع مسار جهاز macOS الخام: فقد يصبح نطاق الإنقاذ الظاهر غير منطقي بدلًا من أن ينتهي عند الحد الحقيقي للجهاز. لذلك قيّدت نطاق الإنقاذ صراحة بالحجم المادي المعروف أعلاه. وفي هذه الحالة كان تحديد حد الإدخال إجراءً للصحة، لا تحسينًا للسرعة.
لماذا لم تكن نسبة 100.00% في ddrescue تعني أن الملفات أصبحت آمنة
قرب نهاية الاستعادة المادية، أبلغ ddrescue تقريبًا عن:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
كانت النسبة البارزة:
100.00%
لكن الحالات غير المحسومة كانت لا تزال تبلغ إجمالًا:
13,835,816 bytes
أو نحو:
13.84 MB
وبالمقارنة مع مصدر حجمه 4,000,787,027,968 بايت، كانت الحصة المستعادة تقريبًا:
99.999654%
هذه نتيجة ممتازة لاستعادة الكتل. لكنها ليست نتيجة عن سلامة الملفات.
موضع البايتات المفقودة أهم من كميتها الإجمالية. فقد لا يكون لفقدان عدة ميغابايتات من مساحة غير مستخدمة أي أثر ظاهر. وقد تتلف منطقة صغيرة غير قابلة للقراءة داخل فيديو ملفًا واحدًا. أما فقدان أصغر بكثير داخل بيانات نظام الملفات الوصفية فقد يجعل تحديد مواقع كثير من امتدادات البيانات السليمة أصلًا أمرًا صعبًا.
وأصبح هذا هو النموذج الذهني الأساسي لبقية عملية الاستعادة:
| الطبقة | السؤال الذي تجيب عنه | ما الذي لا يثبته النجاح |
|---|---|---|
| استعادة الكتل | هل نُسخت القطاعات المادية؟ | أن APFS يستطيع إعادة بناء كل ملف |
| استعادة نظام الملفات | هل يمكن حل المسارات والبيانات الوصفية والامتدادات؟ | أن كل بايت مستخرج صالح |
| التحقق من الملفات | هل وصل الملف بالحجم أو التجزئة المتوقعة؟ | أنه لم يوجد قط ملف تعذر اكتشافه |
أجريت بضع محاولات أخيرة على المناطق المتبقية غير القابلة للقراءة. وفي النهاية توقفت عن إنتاج قراءات جديدة مفيدة بينما كان قرص Toshiba يصدر نقرات شديدة. عند تلك النقطة توقفت عن استخدام القرص الأصلي كمصدر استعادة نشط.
لماذا اشتريت قرصين بسعة 5 TB لاستعادة قرص واحد بسعة 4 TB
كان أول قرص جديد هو Seagate Expansion بسعة 5 TB، وكانت سعته المادية الدقيقة:
5,000,981,077,504 bytes
كتبت إليه نسخة Toshiba على مستوى الكتل. احتل تخطيط المصدر نحو أول 4 TB، وبقي قرابة 1 TB بعد التخطيط المنسوخ. وتركت هذه السعة الإضافية عمدًا من دون استخدام.
لم أوسّع حاوية APFS. ولم أعد تقسيم النسخة المستنسخة للراحة. ولم أشغّل إصلاحًا لنظام الملفات عليها. أصبح ذلك القرص هو النسخة الرئيسية المستنسخة.
ثم اشتريت قرصًا ثانيًا بسعة 5 TB. كان مهيأً حديثًا، قابلًا للكتابة، مختبرًا بصورة مستقلة، ومخصصًا فقط لمخرجات الاستعادة.
failing 4 TB HDD
│
│ GNU ddrescue
▼
5 TB drive #1
master block-level clone
READ-ONLY
│
│ APFS parsing and extraction
▼
5 TB drive #2
recovered files
WRITABLE
كان ذلك يعني شراء نحو 10 TB من السعة الجديدة الاسمية لاستعادة وحدة تحتوي على نحو 3.26 TB من البيانات المستخدمة. لم يكن القرص الإضافي من أجل السعة، بل من أجل الحفاظ على ثابت:
If an experiment is wrong, I can return to the same untouched master clone.
كان إصلاح النسخة الرئيسية أو تغيير حجمها أو إعادة تقسيمها أو كتابة المخرجات المستعادة عليها سيخلط بين الحفظ والتجريب. أما إبقاء المصدر والوجهة على قرصين ماديين منفصلين فجعل الأخطاء قابلة للتراجع.
قبل أن أثق بالوجهة، أجريت اختبار كتابة/قراءة بحجم يقارب 10 GB. وفي ذلك الاختبار حافظ القرص على نحو 144.4 MB/s في الاتجاهين. وكانت عينات القراءة الخام من النسخة الرئيسية في حدود 28–49 MB/s تقريبًا، ولم تُعِد إنتاج نمط فشل الإدخال/الإخراج المادي الذي كان يظهر على القرص الأصلي أثناء تلك الاختبارات.
عند هذه النقطة تغيرت المشكلة. لم أعد أشخّص عتادًا يتعطل، بل أصبحت أشخّص بيانات APFS وصفية تالفة محفوظة على عتاد تصرف بصورة طبيعية في اختباراتي.
كانت نسخة APFS قابلة للقراءة كجهاز لكنها غير صالحة كنظام ملفات
كان مخزن APFS المادي المستنسخ:
4,000,650,887,168 bytes
بدأ القسم المعني عند القطاع:
264192
ومع قطاعات بحجم 512 بايت، يكون إزاحة البايت:
135,266,304 bytes
أبلغت وحدة APFS عن استهلاك يقارب:
3,260,976,717,824 bytes
مستهلكة.
وصل فحص APFS للقراءة فقط في النهاية إلى شجرة fsroot وأبلغ عن:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
عند هذه النقطة لم تعد عبارة «نسخ ddrescue القرص كله تقريبًا» مفيدة كتشخيص كامل. كانت النسخة الخام موجودة، لكن بنية نظام الملفات داخلها ظلت غير متسقة.
أبقيت فحص نظام الملفات عمدًا من دون أي تعديل. لم أحوّل fsck_apfs -n إلى عملية إصلاح على النسخة الرئيسية الوحيدة عالية الجودة التي أملكها. قد يكون الإصلاح مناسبًا على تخزين عادي، لكنه هنا كان سيغيّر الدليل الذي ما زلت أحاول فهمه.
استطاعت The Sleuth Kit عرض مساحة أسماء APFS لكنها فشلت في محتويات الملفات
كانت The Sleuth Kit 4.15.0 أول حزمة استعادة جعلت النسخة تبدو واعدة. باستخدام الجهاز المستنسخ كاملًا، وإزاحة القسم المعروفة، وكتلة APFS الفائقة التي كنت قد حددتها، استطعت تعداد أسماء أدلة حقيقية بواسطة fls:
fls \\
-o 264192 \\
-B 4594668 \\
-p \\
/dev/rdiskN
أستخدم N عمدًا. فقد كانت أرقام أقراص macOS تتغير بعد إعادة التوصيل وإعادة التشغيل، لذلك لم أتعامل مع تعيين سابق مثل /dev/disk6 على أنه هوية ثابتة.
استطاع fls اجتياز أجزاء كبيرة من مساحة الأسماء. كشفت شجرة كبيرة واحدة وحدها عن نحو 8,900 دليل. وللحظة بدا أن الجزء الأصعب قد حُل.
ثم حاولت استرجاع محتويات الملفات.
وكان أحد الإخفاقات الممثلة للمشكلة:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
كان بإمكان tsk_recover أن يبدأ الاستخراج ثم يفشل عند كتلة APFS. وأظهرت محاولات icat الفردية الفئة نفسها من المشكلة.
كان الفرق المهم هو أن:
directory traversal works
لا يعني أن:
file content retrieval works
قد يملك المحلل قدرًا كافيًا من البيانات الوصفية الباقية لاكتشاف اسم مسار، ثم يفشل لاحقًا عند حل كائن الملف أو بيانات الامتدادات الوصفية أو كتل المحتوى اللازمة لإرجاع تدفق البايتات.
جعلت أول محرك للاستعادة الجماعية متحمّلًا للأخطاء، لكن المحلل ظل غير مناسب لهذا النوع من التلف
كان رد فعلي الأول هو جعل استخراج TSK أكثر مرونة بدلًا من تغيير المحلل فورًا.
أنشأت غلاف استعادة بلغة Python حول fls وicat. احتفظ بحالة دائمة في SQLite، وسجّل الإخفاقات، ودعم الاستئناف، وكتب المخرجات الجزئية بصورة منفصلة، وخفّض أولوية بيانات الصيانة منخفضة القيمة في macOS خلال الجولة الأولى.
أضفت أيضًا قاعدة «نقطة ساخنة»: إذا فشلت أربعة ملفات متتالية في دليل واحد بالنمط نفسه ذي الصفر بايت من APFSBlock، توقف السكربت عن إضاعة الوقت على بقية ذلك الفرع، وأجّله، وانتقل إلى مكان آخر. كانت فرضية العمل أن مجموعة من الإخفاقات المتطابقة قد تشترك في تبعية واحدة تالفة من البيانات الوصفية بدلًا من أن تمثل مئات الحمولات المتلفة بشكل مستقل.
كان التنسيق مفيدًا. أما قارئ APFS الأساسي فلم يكن كذلك.
في إحدى النقاط التي التقطتها، كانت قاعدة بيانات الاستعادة تحتوي على:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
وتوزعت حالات الفشل الـ 486 على:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
وكانت الجولة المنظمة قد أنتجت:
0 useful recovered user files
كشفت حالات عدم تطابق الحجم الـ 75 عن خطأ في الغلاف الذي كتبته: فقد أعادت بعض ملفات بيانات النظام الوصفية بيانات، لكن محللي سجّل حجمًا متوقعًا يساوي صفرًا. كان تصحيح هذا التفسير مهمًا، لكنه لم يغير النتيجة الغالبة. ظلت ملفات المستخدم العادية تنتهي بصفر بايت مع could not read APFSBlock.
اخترت ملف AVIF صغيرًا واحدًا ليكون حالة اختبار قابلة لإعادة الإنتاج. كان TSK يعرف حجمه المتوقع:
expected size: 56,309 bytes
recovered: 0 bytes
أصبح ذلك الملف أكثر قيمة بكثير من جولة جماعية أخرى تمتد لساعات. إذا لم يستطع نهج جديد استعادة ملف اختبار واحد حجمه 56 KB يفشل باستمرار، فلا يستحق أن أسمح له بالعمل على التيرابايتات المتبقية.
كانت كتل القرص ومسار بيانات APFS الوصفية نطاقي فشل مختلفين
بحلول هذه النقطة كانت لدي ثلاث ملاحظات:
GNU ddrescue:
almost the entire physical source was copied
TSK fls:
many real paths were discoverable
TSK icat:
many ordinary file contents still failed
تصبح هذه الملاحظات متوافقة عندما نفصل سلسلة البحث مفاهيميًا:
pathname
↓
directory record
↓
file object / inode metadata
↓
extent mapping
↓
physical data blocks
قد تبقى كتل البيانات القريبة من أسفل السلسلة سليمة بينما تكون وصلة أعلى منها في سلسلة البيانات الوصفية تالفة. والاحتمال الآخر هو أن تطبيقين مختلفين لـ APFS يجتازان البنى التالفة نفسها بطرق مختلفة.
لم أعزل كائن APFS تالفًا واحدًا يفسر كل حالات الفشل، لذلك لن أدعي أنه السبب الجذري المؤكد. لكن الأدلة دعمت تجربة أكثر فائدة بكثير: الإبقاء على البايتات المستنسخة من دون تغيير وتبديل المحلل.
لم تتحول 142 نقطة تحقق في APFS إلى مسار رجوع فوري
عثر مسح خام للقراءة فقط لمنطقة واصفات نقاط تحقق APFS على 142 كتلة فائقة مرشحة من نوع NXSB لنقاط التحقق، وكانت معرّفات المعاملات تتراوح من:
223133
نزولًا إلى:
222992
كان السؤال البديهي هو ما إذا كانت نقطة تحقق أقدم تشير إلى شجرة بيانات وصفية أكثر سلامة.
بنيت apfs-fuse وجرّبت معرّفات معاملات مختلفة لنقاط التحقق عبر fuse-t. علقت أحدث نقطة تحقق، وفعلت XIDs الأقدم الشيء نفسه. أضفت مهلًا صارمة لكل نقطة تحقق، وفشلت أكثر من 50 محاولة متتالية في إنتاج تركيب صالح للاستخدام. كما جربت واجهتي NFS وSMB الخلفيتين في fuse-t.
هذا لم يثبت أن جميع نقاط التحقق كانت تالفة. فقد اعتمدت التجربة على نقطة التحقق وapfs-fuse وfuse-t وسلوك جهاز macOS وواجهة التركيب الخلفية. ويمكن لأي فشل في أي موضع من هذه السلسلة أن ينتج النتيجة المرئية نفسها.
أبلغ محلل لاحق عن عدم وجود أي لقطات لـ APFS، وهو ما عزز أيضًا تمييزًا كان عليّ الحفاظ عليه: حالات معاملات نقاط التحقق هذه ليست الشيء نفسه الذي تمثله لقطات في APFS المرئية للمستخدم.
محلل يتجمد عند --help لا يستطيع تشخيص قرصي
بنيت أيضًا go-apfs-v2. اكتمل البناء، لكن حتى:
apfs --help
تجمد واضطررت إلى إنهائه بمهلة زمنية.
كما تجمد فحص كتل بسيط عبر مساري الجهاز الخام والمخزن مؤقتًا. وأجريت اختبارًا محدودًا مشابهًا باستخدام apfsutil؛ وانتهت مهلة كلا شكلي الجهاز بعد نحو 15 ثانية.
كانت هذه إخفاقات مفيدة لأنها منعتني من الوصول إلى الاستنتاج الخاطئ. الأداة التي لا تستطيع إكمال مسار المساعدة الخاص بها بصورة موثوقة تقدم دليلًا ضعيفًا على ما إذا كان ملف تالف قابلًا للاستعادة.
وأصبحت قاعدتي:
تحقق من أداة الاستعادة قبل أن تعتبر فشل أداة الاستعادة دليلًا على حالة البيانات.
منع macOS من تركيب النسخة تلقائيًا أزال مصدرًا آخر للمخاطر
كان macOS نفسه عنصرًا متحركًا آخر. أردت تركيب وجهة الاستعادة السليمة بالطريقة المعتادة، لكنني لم أرد أن يحاول Disk Arbitration تلقائيًا تركيب نسخة APFS التالفة كلما أعدت توصيل وحدات التخزين.
التسلسل الآمن الذي انتهيت إلى استخدامه كان:
- وصّل وجهة الاستعادة السليمة واركبها.
- تحقق من هوية وحدة التخزين والمساحة الحرة.
- جمّد
diskarbitrationd. - تحقق من عدم وجود عملية
mount_apfsنشطة. - وصّل النسخة الرئيسية المستنسخة.
- حدّد النسخة الرئيسية ديناميكيًا باستخدام الحجم المادي المعروف وهوية APFS.
- تحقق من أن النسخة الرئيسية غير مركبة.
- نفّذ أعمال الاستعادة للقراءة فقط.
- عند التوقف، احفظ الحالة، واستأنف Disk Arbitration، ثم أوقف التشغيل أو أخرج القرص بالطريقة المعتادة.
الأمر الذي استخدمته فعليًا لتجميد Disk Arbitration كان:
sudo kill -STOP "$(pgrep -x diskarbitrationd)"
تحققت من أن حالة العملية تحتوي على T، وفحصت وجود أي عمليات mount_apfs شاردة قبل المتابعة.
لم يكن درس السلامة المهم هو رقم القرص المحدد، بل العكس: لا تثق أبدًا برقم قرص الأمس. بعد إعادة التشغيل قد يشير /dev/disk6 السابق إلى جهاز مادي مختلف. تعاملت مع UUID الخاص بـ APFS والحجم المادي المعروف باعتبارهما الهوية، ثم اشتققت منهما مسار الجهاز الحالي.
استعاد libfsapfs الملف نفسه الذي أعاده TSK بحجم صفر بايت
جاء الاختراق الحقيقي من libfsapfs، وهو تطبيق مختلف لـ APFS.
بنيته من المصدر على macOS. وعرّف الملف التنفيذي الناتج fsapfsinfo نفسه بأنه:
fsapfsinfo 20260923
ثم ظهر فرق مهم في واجهة الجهاز على macOS.
قسم جهاز المحارف الخام:
/dev/rdiskNs2
فشل سريعًا بقراءة ذات وسيطة غير صالحة قرب الإزاحة 4096.
أما صيغة جهاز الكتل ذات التخزين المؤقت:
/dev/diskNs2
فقد عملت.
النسخة المادية نفسها. قسم APFS نفسه. واجهة جهاز مختلفة في macOS.
فتح fsapfsinfo الحاوية وعثر على وحدة واحدة. ثم اختبرت ملف الـ 56,309 بايت نفسه الذي كان TSK قد فشل في استخراجه.
كان TSK قد أعطى:
expected: 56,309
recovered: 0
APFSBlock failure
أما عبر libfsapfs، فقد أعطى إدخال الملف:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
كانت هذه أول نتيجة غيّرت التشخيص بصورة جوهرية. لم تتغير البايتات المستنسخة. ولم يتغير ملف الاختبار. ولم يُصلح APFS التالف.
الذي تغيّر هو تطبيق APFS.
على أقل تقدير، أصبحت أعرف أن ملف مستخدم حقيقي بدا غير قابل للاستعادة عبر TSK كان لا يزال قابلًا للوصول بعمق كافٍ عبر libfsapfs لقراءة محتواه كاملًا وحساب بصمته.
استخرجت ملفًا واحدًا معروف الفشل قبل أن أثق بـ libfsapfs مع تيرابايتات من البيانات
لم تكن بصمة ناجحة كافية لأبدأ عملية استعادة متعددة التيرابايتات. كنت أريد البايتات الفعلية على قرص الوجهة.
بدلًا من تخمين واجهة C الخاصة بـ libfsapfs، فحصت مصدر المكتبة وتتبعت مسار القراءة الذي كان fsapfsinfo يستخدمه أصلًا عند حساب البصمة. ثم بنيت مستخرجًا صغيرًا للقراءة فقط لذلك الملف التجريبي المعروف.
كتب المستخرج بالضبط:
56,309 bytes
إلى قرص الاستعادة المنفصل. وكانت قيمة MD5 للملف المستخرج:
c6f56db33eafc1de0f52a035bc255dc7
وقد طابقت البصمة السابقة.
عندها فقط وسّعت النهج. أثبت اختبار الملف الواحد ثلاثة أشياء منفصلة: أنه يمكن حل اسم المسار، وأنه يمكن استخراج العدد الكامل المتوقع من البايتات، وأن البايتات المستخرجة تنتج البصمة نفسها التي نتجت عن القراءة الكاملة السابقة للمحتوى.
كانت مشكلة الاستعادة الجماعية تتعلق أساسًا باحتواء الأعطال والاستئناف
بمجرد أن استطاع libfsapfs استعادة ملف لم يستطع TSK استعادته، تغيرت المشكلة الصعبة مرة أخرى. احتجت إلى نظام يستطيع معالجة شجرة أدلة ضخمة من دون أن يؤدي فرع تالف واحد أو إعادة تشغيل واحدة أو Ctrl+C واحدة إلى إعادة المهمة من الصفر.
لذلك استخدم خط أنابيب الاستعادة الجماعية مجموعة من الثوابت الصارمة:
- فُتحت النسخة الرئيسية للقراءة فقط.
- كُتبت المخرجات المستعادة فقط إلى القرص الثاني بسعة 5 TB.
- حُفظت أسماء الأدلة والملفات.
- حُفظ التقدم في SQLite كي يبقى بعد خروج العملية وإعادة التشغيل.
- كُتب كل ملف أولًا إلى مسار مؤقت.
- لم يُعد تسمية الملف المؤقت إلى مساره النهائي إلا بعد كتابة الحجم المتوقع كاملًا.
- أمكن التعرف على الملفات الموجودة بالحجم المتوقع أثناء الاستئناف.
- عُزل العمل الفاشل أو الإشكالي عن العمل المكتمل.
- فُحصت المساحة الحرة وحوفظ على احتياطي أمان.
- أمكن تخطي مخرجات التطوير القابلة لإعادة التوليد وبيانات النظام الوصفية منخفضة القيمة أو خفض أولويتها.
ظهر تغيير واحد في الأداء أثره فورًا: توقفت عن إعادة فتح حاوية APFS بصورة مستقلة لكل ملف.
كان المسار السريع يعالج دليلًا واحدًا في كل مرة. يفتح عامل المصدر، ويعدّد ذلك الدليل، ويستعيد ملفاته العادية المباشرة، ثم يعيد الأدلة الفرعية إلى الطابور. وإذا فشل دليل أو انتهت مهلته، كان المتحكم يعلّمه DEFERRED ويتابع بدلًا من حجب الجولة بأكملها.
بعد انتهاء كل العمل العادي المعلّق، أُعيدت زيارة الأدلة المؤجلة عبر مسار احتياطي أبطأ يعزل العمل على كل ملف بصورة أكبر. وبعد ذلك أمكن إعادة محاولة الإخفاقات المتبقية بصورة مستقلة.
discover directory
↓
recover immediate files
↓
verify expected sizes
↓
commit durable state
↓
queue child directories
↓
defer local failures
↓
continue globally
↓
fallback and retry later
طابق هذا التصميم نمط الفشل الفعلي أفضل بكثير من أمر تكراري ضخم واحد. لم يكن التلف متجانسًا، لذلك لا ينبغي لنظام الاستعادة أن يفرض تقدمًا متجانسًا أيضًا.
لماذا استخدمت فحص الحجم المتوقع وإعادة التسمية الذرية بدلًا من تجزئة ملايين الملفات
كان MD5 مفيدًا أثناء إثبات الملف الواحد لأنني كنت أحتاج إلى دليل قوي على أن المحلل يقرأ المحتوى الكامل لملف لم يستطع TSK قراءته.
إن إجراء قراءة كاملة ثانية لكل بايت مستعاد فقط من أجل حساب تجزئة ملايين الملفات كان سيضيف قدرًا كبيرًا من عمليات الإدخال/الإخراج. لذلك استخدمت ثابتًا مختلفًا في جولة الاستخراج الرئيسية.
لكل ملف عادي، وفرت بيانات APFS الوصفية حجمًا متوقعًا. كتب العامل إلى ملف مؤقت، ولم ينقله إلى اسم المسار النهائي إلا بعد أن طابقت القراءة الكاملة ذلك الحجم المتوقع.
وهذا يعني أن الانقطاع لا ينبغي أن يترك ملفًا ناقصًا متخفيًا تحت الاسم النهائي.
تطابق الحجم ليس إثباتًا تشفيريًا للسلامة، ولا أتعامل معه على أنه كذلك. لكن التحقق من الحجم المتوقع مع إعادة التسمية الذرية شكّلا حدًا عمليًا للصحة في الجولة ذات الحجم الكبير، بينما ظلت التجزئات الموجهة مفيدة للعينات وحالات الفشل المعروفة.
جعل SQLite إعادة التشغيل أمرًا مملًا بدلًا من كارثيًا
استمرت الاستعادة وقتًا طويلًا بما يكفي لأن أحتاج إلى إيقاف الحاسوب ثم الاستئناف لاحقًا. هذا المتطلب غيّر التصميم من «سكربت» إلى «سير عمل قابل للاستعادة».
لم يكن Ctrl+C يترك العملية الفرعية النشطة ببساطة. كان المتحكم يلتقط المقاطعة، ويوقف العامل، ويعيد الدليل النشط إلى حالة قابلة للاستعادة، ويثبت حالة SQLite، ثم يخرج.
كان التوقف النظيف يبدو مفاهيميًا هكذا:
CURRENT DIRECTORY -> PENDING
CTRL+C: RECOVERY STOPPED SAFELY
STATE SAVED. RUN THE SAME COMMAND TO RESUME.
بعد إعادة التشغيل، كررت فحوصات هوية الأقراص وشغلت أمر الاستعادة نفسه. استأنفت قاعدة بيانات الحالة الطابور الموجود.
بدأت إحدى عمليات الاستئناف اللاحقة بـ:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
كان ذلك أكثر دلالة بكثير من شريط تقدم عام. فقد أظهر أن عشرات الآلاف من وحدات الأدلة المكتملة نجت من إعادة التشغيل، وأن الطابور المتبقي كان محددًا بوضوح.
في استئناف سابق، أُخذ الدليل الذي انقطعت معالجته في الجلسة السابقة من جديد. جرى التعرف على الملفات الموجودة أصلًا بالحجم المتوقع، ولم يلزم كتابة سوى العمل المفقود. هذا هو السلوك الذي أردته: يجب أن تكون إعادة تشغيل الاستعادة إجراءً روتينيًا لا مخيفًا.
كانت 326,799 ملفات أول دليل على أن الطريقة تتوسع
قبل توسيع المستخرج الجديد ليشمل كل البيانات المحددة، استخدمت شجرة أولوية كبيرة واحدة كهدف للتحقق الجماعي.
أبلغت الحالة المكتملة عن:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
احتوت الوجهة على:
326,799 files
145,039,215,948 bytes
أو نحو:
135.08 GiB
وتطابقت أعداد الملفات بالضبط:
302,541 + 24,258 = 326,799
لم تبق أي ملفات .partial.* في تلك الشجرة المكتملة.
كان ملف الاختبار بحجم 56,309 بايت قد أثبت أن المحلل يمكن أن ينجح حيث فشل TSK. وأثبتت استعادة 326,799 ملفًا أن النهج نفسه يستطيع الصمود داخل تسلسل هرمي حقيقي كبير مع منطق الاستئناف واكتشاف الملفات الموجودة ومن دون أي حالات فشل ملفات مسجلة في تلك الجولة المكتملة.
تجاوزت عملية الاستعادة الأكبر 1.6 مليون ملف جديد قبل اكتمالها
بعد أن اكتملت شجرة الأولوية بنظافة، وسّعت الاستعادة إلى بقية بيانات المستوى الأعلى المحددة.
في إحدى نقاط التوقف الآمنة المتعمدة، أبلغ SQLite عن:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
وكان ذلك تقريبًا:
840.72 GiB
من البيانات الجديدة المكتوبة التي سجلتها جولات الأدلة عند تلك النقطة.
لم يكن وجود دليل واحد بحالة DEFERRED يعني فقدان بيانات. بل كان يعني أن المسار السريع توقف عمدًا عن السماح لتلك المشكلة المحلية بتأخير عمل غير مرتبط بها. وقد وُجدت مرحلة المسار الاحتياطي تحديدًا للعودة إلى مثل هذه الحالات لاحقًا.
استأنفت الجلسات اللاحقة من قاعدة البيانات نفسها. ازداد عدد المكتملات وانخفض الطابور المعلّق. وفي النهاية استعدت كل شجرة مجلدات محددة ومعدودة كنت أحتاج إليها.
ما الذي يمكنني ادعاؤه بصدق بشأن النتيجة النهائية
لن أصف النتيجة بأنها «استعادة كل بايت». الأدلة لا تدعم ذلك.
ظلت خريطة ddrescue الأصلية تحتوي على نحو 13.84 MB لم يُؤكد نجاح نسخها. كما لا يمكنني إثبات أن أي كائن في نظام الملفات لم يصبح غير قابل للاكتشاف تمامًا لأن البيانات الوصفية اللازمة لتعداده كانت ضمن المناطق التالفة.
هذه القيود مهمة لأن الاستعادة المنظمة يمكنها إثبات أن كائنًا جرى تعداده قد استُخرج؛ لكنها لا تستطيع إثبات عدم الوجود التاريخي لكائن لم تعد مساحة الأسماء التالفة قادرة على كشفه.
أقوى صياغة نهائية أستطيع تقديمها أضيق نطاقًا:
استُعيدت بنجاح، عبر عملية الاستعادة المنظمة، كل شجرة مجلدات محددة ومعدودة كنت أحتاج إليها.
لم أحتج إلى إصلاح النسخة الرئيسية في مكانها. ولم أحتج إلى إعادة استخدام قرص Toshiba الأصلي الذي كان يصدر النقرات من أجل الاستخراج الجماعي. ولم أحتج إلى جولة استخراج خام على القرص كله بأسلوب PhotoRec كانت ستضحي ببنية الأدلة وأسماء الملفات.
حتى الأدوات التي فشلت كانت أدلة مفيدة
عند النظر إلى الوراء، يبدو المسار الناجح بسيطًا:
ddrescue clone
↓
libfsapfs
↓
resumable extractor
↓
recovered files
لكن التحقيق لم يكن يبدو هكذا أثناء حدوثه، وإزالة النهج الفاشلة من القصة ستزيل جزءًا كبيرًا من الدرس الهندسي المفيد.
علّمني TSK أن اجتياز مساحة أسماء APFS واسترجاع المحتوى نمطان مختلفان من الفشل.
وعلّمني الخطأ في أول غلاف كتبته أن تصنيف سكربت الاستعادة للأخطاء ليس حقيقة مطلقة.
وعلّمتني آلية النقطة الساخنة أن أعزل الفشل المحلي بدلًا من السماح له بتعطيل التقدم العام.
وعلّمتني تجربة نقاط التحقق ألا أخلط بين نقاط تحقق معاملات APFS ولقطات.
وعلّمتني محاولات FUSE أن فشل التركيب قد يورط عدة طبقات غير بيانات نظام الملفات نفسها.
وعلّمني قارئ APFS الذي تجمد عند --help أن أتحقق من الأداة قبل تفسير تشخيصاتها.
وعلّمني اختلاف السلوك بين /dev/rdiskNs2 و/dev/diskNs2 أن مسار الإدخال/الإخراج في نظام التشغيل يمكن أن يغيّر سلوك المحلل حتى عندما يكون القرص الأساسي نفسه.
ومنحتني بنية القرصين حرية ارتكاب الأخطاء في كل مكان آخر مع إبقاء النسخة الرئيسية من دون تغيير.
سير عمل الاستعادة الذي سأستخدمه مرة أخرى
- أوقف نشاط نظام الملفات العادي على التخزين الذي يتعطل ميكانيكيًا. إذا كانت القراءات تتوقف أو يختفي الجهاز أو يصدر نقرات، فسأعطي الأولوية لنسخة على مستوى الكتل قابلة للاستئناف بدلًا من استكشاف Finder.
- استخدم GNU ddrescue مع mapfile دائم ونطاق إنقاذ متحقق منه. يحفظ mapfile التقدم، ويمنع الحجم المعروف للمصدر التباس حجم الجهاز من أن يصبح جزءًا من المشكلة.
- احتفظ بنسخة رئيسية واحدة للقراءة فقط. لا تصلحها ولا تغيّر حجمها ولا تعِد تقسيمها ولا تستخدمها لتخزين الملفات المستعادة.
- اكتب الملفات المستعادة إلى قرص مادي ثانٍ. حفظ المصدر وتخزين المخرجات مهمتان مختلفتان.
- شخّص نظام الملفات المستنسخ للقراءة فقط أولًا. وجود نسخة مادية سليمة تحتوي على بيانات APFS وصفية تالفة هو مشكلة استعادة منطقية، وليس المشكلة نفسها التي يمثلها قرص مصدر يصدر نقرات.
- اختر ملفًا فاشلًا واحدًا قابلًا لإعادة الإنتاج لاختبار المحلل. أخبرني ملف معروف الفشل بحجم 56 KB عن المحللات البديلة أكثر مما أخبرتني به ساعات من الاستخراج الجماعي الأعمى.
- تحقق من المحلل نفسه. إذا تجمدت أداة قبل أن تقرأ المصدر قراءة ذات معنى، فلا تفسر ذلك على أنه دليل على ضياع البيانات.
- لا تفترض أن تطبيقًا واحدًا لـ APFS يحدد قابلية الاستعادة. تصرف TSK و
libfsapfsبشكل مختلف جدًا على البايتات المستنسخة نفسها. - حدّد الأقراص بخصائص ثابتة، لا بأرقام أجهزة مؤقتة. UUID لنظام الملفات والحجم المادي المعروف أكثر أمانًا من
/dev/diskNالخاص بالأمس. - اجعل الاستخراج طويل المدة قابلًا للاستئناف منذ البداية. الحالة الدائمة والملفات المؤقتة وإعادة التسمية الذرية والعمل المؤجل وإعادات المحاولة المحدودة والتعامل النظيف مع الإيقاف كلها أجزاء من الصحة على هذا النطاق.
- افصل مستويات التحقق. نسبة ddrescue واسم المسار المرئي وتطابق الحجم المتوقع وبصمة المحتوى يثبت كل منها شيئًا مختلفًا.
الرقم الذي بدا كخط النهاية لم يكن سوى نهاية المرحلة الأولى
ظل الرقم الأكثر تضليلًا في عملية الاستعادة كلها هو:
100.00%
بدا كأنه إجابة عن سؤال «هل أنقذت القرص؟»
أما ما أجاب عنه فعليًا فكان أضيق بكثير:
How much of the physical rescue domain did ddrescue successfully copy?
لم يُجب عما إذا كان APFS يستطيع إعادة بناء مساحة الأسماء. ولم يُجب عما إذا كان محلل يستطيع اجتياز بيانات وصفية تالفة رفضها محلل آخر. ولم يُجب عما إذا كان الملف المستعاد بالحجم المتوقع. ولم يُجب عما إذا كان استخراج ملايين الملفات يستطيع النجاة من الأخطاء وإعادة التشغيل من دون إفساد حالته الداخلية.
كنت أحتاج إلى دليل منفصل لكل طبقة.
نجحت الاستعادة المادية أولًا. وظل نظام الملفات تالفًا. استطاع المحلل الأول كشف الأسماء لكنه فشل في كثير من محتويات الملفات. وقرأ تطبيق APFS مختلف ملف الاختبار نفسه بنجاح. تحوّل ذلك الإثبات إلى مستخرج لملف واحد، ثم تحوّل المستخرج إلى محرك استعادة قابل للاستئناف، وفي النهاية استعاد المحرك أشجار المجلدات المحددة التي احتجت إليها على قرص ثانٍ، بينما بقيت النسخة الرئيسية من دون تغيير.
لم يصبح القرص الصلب الأصلي سليمًا قط. ولم يصلح APFS نفسه بسحر. الذي تغيّر هو نموذج الاستعادة.
استعادة الكتل واستعادة نظام الملفات والتحقق من الملفات مراحل هندسية منفصلة. جعل التعامل معها كمشكلة واحدة الوضع يبدو شبه ميؤوس منه. أما فصلها فجعل المشكلة قابلة للمعالجة.