GNU ddrescue meldde 100.00%. Mijn eerste gestructureerde bestandsherstelronde leverde 0 useful recovered user files op.
Die twee resultaten kwamen voort uit hetzelfde defect van een harde schijf van 4 TB, en juist die tegenstrijdigheid maakte dit herstel veel interessanter dan “de defecte schijf klonen en de bestanden kopiëren”. ddrescue had zijn werk opmerkelijk goed gedaan: het had ongeveer 99.999654% van de fysieke bron gekopieerd. De kloon stond op gezonde hardware. Toch was APFS nog steeds beschadigd, kon macOS mij het bestandssysteem niet op de normale manier beschikbaar stellen en kon de eerste APFS-herstelstack grote delen van de directoryboom opsommen terwijl het lezen van de inhoud van gewone bestanden mislukte.
Uiteindelijk heb ik elke geselecteerde, opgesomde directoryboom teruggehaald die ik nodig had, maar pas nadat ik het incident als drie afzonderlijke problemen behandelde: fysiek blokherstel, interpretatie van een beschadigd bestandssysteem en grootschalige bestandsextractie met validatie en ondersteuning voor hervatten.
Het probleem hield eerst op een bestandssysteemprobleem te zijn
De oorspronkelijke Toshiba van 4 TB was op een punt gekomen waarop ik hem niet meer vertrouwde voor normale bestandssysteemactiviteit. Sommige leesacties duurden 60–75 seconden. Bewerkingen konden blijven hangen. De schijf verdween af en toe uit macOS, maakte hoorbare klikgeluiden en schakelde soms uit.
Kort voor het defect had ik ongeveer 300 GB extra gegevens geschreven en een massale hernoemactie uitgevoerd die rond de 500.000 bestanden en directories raakte. Door de timing leek die metadata-intensieve belasting verdacht, maar ik kan niet bewijzen dat die de hardwarestoring heeft veroorzaakt. Mogelijk heeft ze een al ongezonde schijf alleen genoeg belast om het probleem zichtbaar te maken.
Wat ik wel kon vaststellen, was het gedrag van de schijf. Zodra mechanische opslag begint te haperen, verdwijnt en klikt, is herhaaldelijk door directories bladeren de verkeerde abstractie. Het doorlopen van directories kan meer leesacties en seeks veroorzaken. Het mounten van een bestandssysteem kan metadatawerk activeren. Elk experiment kost tijd op de enige component waarvan de resterende bruikbare levensduur onbekend is.
Dus veranderde ik het doel van:
mijn bestanden herstellen
naar:
zo veel mogelijk leesbare sectoren herstellen
Ik gebruikte GNU ddrescue 1.30 met een permanente mapfile. De exacte fysieke grootte van de bronschijf was:
4,000,787,027,968 bytes
De mapfile was essentieel omdat de bron niet stabiel genoeg was voor een kopie in één keer. Daarmee kon het herstel haperingen, verbroken verbindingen, herstarts en latere rondes overleven zonder te vergeten welke gebieden al waren hersteld.
Ik liep ook tegen een praktisch probleem aan met het raw-devicepad van macOS: het ogenschijnlijke herstelbereik kon onzinnig worden in plaats van bij de echte apparaatgrens te eindigen. Daarom beperkte ik het hersteldomein tot de bekende fysieke grootte hierboven. In dit geval was het expliciet begrenzen van de invoer een correctheidsmaatregel, geen snelheidsoptimalisatie.
Waarom 100.00% in ddrescue niet betekende dat de bestanden veilig waren
Tegen het einde van het fysieke herstel rapporteerde ddrescue ongeveer:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
Het opvallende percentage was:
100.00%
Maar de nog onopgeloste toestanden kwamen samen nog uit op:
13,835,816 bytes
of ongeveer:
13.84 MB
Ten opzichte van een bron van 4,000,787,027,968 bytes was het herstelde aandeel ongeveer:
99.999654%
Dat is een uitstekend resultaat voor blokherstel. Het is geen resultaat voor bestandsintegriteit.
De locatie van de ontbrekende bytes is belangrijker dan de totale hoeveelheid. Enkele megabytes verlies in ongebruikte ruimte hoeven niets zichtbaars te beïnvloeden. Een kleine onleesbare regio in een video kan één bestand beschadigen. Een veel kleiner verlies in bestandssysteemmetadata kan ervoor zorgen dat veel verder intacte data-extents moeilijk te vinden zijn.
Dit werd het centrale denkmodel voor de rest van het herstel:
| Laag | Welke vraag deze beantwoordt | Wat succes niet bewijst |
|---|---|---|
| Blokherstel | Zijn de fysieke sectoren gekopieerd? | Dat APFS elk bestand kan reconstrueren |
| Bestandssysteemherstel | Kunnen paden, metadata en extents worden opgelost? | Dat elke geëxtraheerde byte geldig is |
| Bestandsvalidatie | Heeft een bestand de verwachte grootte of hash? | Dat er nooit een niet meer vindbaar bestand heeft bestaan |
Ik deed nog een paar laatste pogingen op de resterende onleesbare gebieden. Uiteindelijk leverden die geen bruikbare nieuwe leesresultaten meer op terwijl de Toshiba hard klikte. Op dat punt stopte ik met het origineel als actieve herstelbron te gebruiken.
Waarom ik twee schijven van 5 TB kocht om één schijf van 4 TB te herstellen
De eerste nieuwe schijf was een Seagate Expansion van 5 TB met een exacte fysieke capaciteit van:
5,000,981,077,504 bytes
Ik schreef de Toshiba-kloon op blokniveau ernaartoe. De layout van de bron besloeg ongeveer de eerste 4 TB, waardoor er zo'n 1 TB buiten de gekopieerde layout overbleef. Die extra capaciteit liet ik bewust met rust.
Ik vergrootte de APFS-container niet. Ik partitioneerde de kloon niet opnieuw voor het gemak. Ik voerde er geen bestandssysteemreparatie op uit. Die schijf werd de masterkloon.
Daarna kocht ik een tweede schijf van 5 TB. Die was nieuw geformatteerd, beschrijfbaar, onafhankelijk getest en werd alleen gebruikt voor herstelde uitvoer.
defecte HDD van 4 TB
│
│ GNU ddrescue
▼
schijf #1 van 5 TB
masterkloon op blokniveau
ALLEEN-LEZEN
│
│ APFS-parsing en extractie
▼
schijf #2 van 5 TB
herstelde bestanden
BESCHRIJFBAAR
Dat betekende dat ik nominaal ongeveer 10 TB aan nieuwe opslag kocht om een volume met ongeveer 3,26 TB aan gebruikte gegevens te herstellen. De extra schijf draaide niet om capaciteit. Het ging om het behouden van een invariant:
Als een experiment fout is, kan ik terug naar dezelfde onaangeroerde masterkloon.
Repareren, vergroten, opnieuw partitioneren of herstelde uitvoer naar de master schrijven zou behoud en experimenteren door elkaar hebben gehaald. Door bron en bestemming op afzonderlijke fysieke schijven te houden, bleven fouten herstelbaar.
Voordat ik de bestemming vertrouwde, voerde ik een schrijf-/leestest uit van ongeveer 10 GB. In die test haalde de schijf in beide richtingen ongeveer 144,4 MB/s. Steekproefsgewijze ruwe leesacties van de masterkloon lagen rond 28–49 MB/s en reproduceerden tijdens die controles niet het fysieke I/O-foutpatroon van de oorspronkelijke schijf.
Op dat moment was het probleem veranderd. Ik was niet langer defecte hardware aan het debuggen. Ik debugde beschadigde APFS-metadata die bewaard waren op hardware die zich in mijn tests normaal gedroeg.
De APFS-kloon was als apparaat leesbaar maar als bestandssysteem ongeldig
De gekloonde fysieke APFS-store was:
4,000,650,887,168 bytes
De relevante partitie begon bij sector:
264192
Met sectoren van 512 bytes is dat een byte-offset van:
135,266,304 bytes
Het APFS-volume meldde ongeveer:
3,260,976,717,824 bytes
in gebruik.
Een alleen-lezen APFS-controle bereikte uiteindelijk de fsroot-boom en rapporteerde:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
Dit was het punt waarop “ddrescue heeft bijna de hele schijf gekopieerd” niet langer bruikbaar was als volledige diagnose. De raw-kloon bestond. De bestandssysteemstructuur daarin was nog steeds inconsistent.
Ik hield de bestandssysteemcontrole bewust niet-wijzigend. Ik veranderde fsck_apfs -n niet in een reparatieactie op de enige hoogwaardige masterkloon die ik had. Een reparatie kan bij gewone opslag gepast zijn, maar hier zou die het bewijs hebben veranderd dat ik nog probeerde te begrijpen.
The Sleuth Kit kon de APFS-namespace opsommen maar faalde bij bestandsinhoud
The Sleuth Kit 4.15.0 was de eerste herstelstack waardoor de kloon er veelbelovend uitzag. Met het volledige gekloonde apparaat, de bekende partitie-offset en het APFS-superblock dat ik had geïdentificeerd, kon ik met fls echte directorynamen opsommen:
fls \
-o 264192 \
-B 4594668 \
-p \
/dev/rdiskN
Ik gebruik N bewust. macOS-schijfnummers veranderden bij opnieuw verbinden en herstarten, dus ik behandelde een eerdere toewijzing zoals /dev/disk6 niet als identiteit.
fls kon aanzienlijke delen van de namespace doorlopen. Eén grote boom alleen al liet ongeveer 8.900 directories zien. Even leek het alsof het moeilijke deel opgelost was.
Toen probeerde ik de bestandsinhoud op te halen.
Een representatieve fout was:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover kon beginnen met extraheren en vervolgens falen op een APFS-block. Afzonderlijke pogingen met icat lieten hetzelfde soort probleem zien.
Het belangrijke onderscheid was:
directorydoorloop werkt
betekende niet:
ophalen van bestandsinhoud werkt
Een parser kan genoeg overgebleven metadata hebben om een padnaam te ontdekken en later toch falen bij het oplossen van het bestandsobject, de extent-metadata of de inhoudsblokken die nodig zijn om de bytestroom terug te geven.
Ik maakte de eerste bulkherstel-engine fouttolerant, maar de parser bleef ongeschikt voor deze schade
Mijn eerste reactie was om de TSK-extractie robuuster te maken in plaats van meteen van parser te wisselen.
Ik bouwde een Python-herstelwrapper rond fls en icat. Die hield duurzame toestand bij in SQLite, logde fouten, ondersteunde hervatten, schreef gedeeltelijke uitvoer apart weg en gaf macOS-huishoudgegevens met lage waarde tijdens de eerste ronde een lagere prioriteit.
Ik voegde ook een “hotspot”-regel toe: als vier opeenvolgende bestanden in één directory faalden met hetzelfde nulbytes-APFSBlock-patroon, stopte het script met tijd besteden aan de rest van die tak, stelde die uit en ging elders verder. De werkhypothese was dat een cluster identieke fouten één beschadigde metadata-afhankelijkheid kon delen in plaats van honderden onafhankelijk vernietigde payloads te vertegenwoordigen.
De orkestratie was nuttig. De onderliggende APFS-reader niet.
Op één vastgelegd moment bevatte de hersteldatabase:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
De 486 fouten vielen uiteen in:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
En de gestructureerde ronde had opgeleverd:
0 useful recovered user files
De 75 grootteverschillen legden een fout in mijn eigen wrapper bloot: sommige systeemmetadatabestanden hadden gegevens teruggegeven, maar mijn parser had een verwachte grootte van nul vastgelegd. Die interpretatie corrigeren was belangrijk, maar veranderde het dominante resultaat niet. Gewone gebruikersbestanden eindigden nog steeds op nul bytes met could not read APFSBlock.
Ik koos één klein AVIF-bestand als reproduceerbare testcase. TSK kende de verwachte grootte:
expected size: 56,309 bytes
recovered: 0 bytes
Dat bestand werd veel waardevoller dan nog een bulkronde van meerdere uren. Als een nieuwe aanpak één testbestand van 56 KB dat consequent faalde niet kon herstellen, verdiende die geen toegang tot de resterende terabytes.
De schijfblokken en het APFS-metadatapad waren verschillende foutdomeinen
Op dit punt had ik drie waarnemingen:
GNU ddrescue:
bijna de volledige fysieke bron was gekopieerd
TSK fls:
veel echte paden waren vindbaar
TSK icat:
veel gewone bestandsinhouden faalden nog steeds
Die waarnemingen zijn verenigbaar zodra de lookup-keten conceptueel wordt opgesplitst:
padnaam
↓
directoryrecord
↓
bestandsobject / inode-metadata
↓
extent-mapping
↓
fysieke datablokken
Datablokken onderaan kunnen intact blijven terwijl een schakel hoger in de metadataketen beschadigd is. Een andere mogelijkheid is dat twee APFS-implementaties dezelfde beschadigde structuren verschillend doorlopen.
Ik heb niet één beschadigd APFS-object geïsoleerd dat elke fout verklaarde, dus dat zou ik niet als bevestigde hoofdoorzaak claimen. Wat het bewijs wel ondersteunde, was een veel nuttiger experiment: de gekloonde bytes ongewijzigd laten en de parser veranderen.
142 APFS-checkpoints werden geen directe rollbackroute
Een raw, alleen-lezen scan van het APFS-checkpointdescriptorgebied vond 142 kandidaat-NXSB-checkpointsuperblocks, met transactie-ID's van:
223133
tot en met:
222992
Een voor de hand liggende vraag was of een ouder checkpoint naar een gezondere metadataboom verwees.
Ik bouwde apfs-fuse en probeerde via fuse-t verschillende checkpointtransactie-ID's. Het nieuwste checkpoint liep vast. Oudere XID's deden hetzelfde. Ik voegde harde time-outs per checkpoint toe en meer dan 50 opeenvolgende pogingen leverden geen bruikbare mount op. Ik probeerde ook zowel NFS- als SMB-backends van fuse-t.
Dat bewees niet dat alle checkpoints beschadigd waren. Het experiment hing af van het checkpoint, apfs-fuse, fuse-t, het apparaatgedrag van macOS en de mountbackend. Een fout ergens in die stack kon hetzelfde zichtbare resultaat opleveren.
Een latere parser rapporteerde nul APFS-snapshots, wat ook een onderscheid versterkte dat ik goed uit elkaar moest houden: die checkpointtransactietoestanden waren niet hetzelfde als voor gebruikers zichtbare APFS-snapshots.
Een parser die op --help blijft hangen kan mijn schijf niet diagnosticeren
Ik bouwde ook go-apfs-v2. De build werd voltooid, maar zelfs:
apfs --help
bleef hangen en moest via een time-out worden beëindigd.
Minimale blokinspectie op zowel raw- als gebufferde apparaatpaden bleef eveneens hangen. Ik deed hetzelfde soort begrensde test met apfsutil; beide apparaatvormen liepen na ongeveer 15 seconden in een time-out.
Die mislukkingen waren nuttig omdat ze voorkwamen dat ik de verkeerde conclusie trok. Een tool die niet betrouwbaar zijn eigen helppad kan afronden, is zwak bewijs voor de vraag of een beschadigd bestand herstelbaar is.
Mijn regel werd:
Valideer het herstelgereedschap voordat je de mislukking van dat gereedschap als bewijs over de gegevens behandelt.
Voorkomen dat macOS de kloon automatisch mountte nam nog een risicobron weg
macOS zelf was nog een bewegend onderdeel. Ik wilde dat de gezonde bestemming normaal gemount was, maar ik wilde niet dat Disk Arbitration automatisch probeerde de beschadigde APFS-kloon te mounten wanneer ik de opslag opnieuw aansloot.
De veilige volgorde die ik uiteindelijk gebruikte was:
- De gezonde herstelbestemming aansluiten en mounten.
- De volume-identiteit en vrije ruimte controleren.
diskarbitrationdbevriezen.- Controleren dat er geen actief
mount_apfs-proces is. - De masterkloon aansluiten.
- De master dynamisch identificeren aan de hand van de bekende fysieke grootte en APFS-identiteit.
- Controleren dat de master niet gemount is.
- Herstelwerk alleen-lezen uitvoeren.
- Bij het stoppen de toestand opslaan, Disk Arbitration hervatten en daarna normaal afsluiten of uitwerpen.
Het commando dat ik daadwerkelijk gebruikte om Disk Arbitration te bevriezen was:
sudo kill -STOP \"$(pgrep -x diskarbitrationd)\"
Ik controleerde dat de processtatus T bevatte en keek naar achtergebleven mount_apfs-processen voordat ik verderging.
De belangrijke veiligheidsles ging niet over het specifieke schijfnummer. Juist het tegenovergestelde: vertrouw nooit op het schijfnummer van gisteren. Na een herstart kan een eerdere /dev/disk6 naar een ander fysiek apparaat verwijzen. Ik gebruikte de APFS-UUID en de bekende fysieke grootte als identiteit en leidde daar vervolgens het huidige apparaatpad uit af.
libfsapfs herstelde hetzelfde bestand dat TSK als nul bytes teruggaf
De doorbraak kwam van libfsapfs, een andere APFS-implementatie.
Ik bouwde die vanuit de broncode op macOS. De resulterende fsapfsinfo-binary identificeerde zichzelf als:
fsapfsinfo 20260923
Toen werd een belangrijk verschil in de apparaatinterface van macOS zichtbaar.
De raw character-devicepartitie:
/dev/rdiskNs2
faalde snel met een invalid-argument-leesfout rond offset 4096.
De gebufferde block-devicevorm:
/dev/diskNs2
werkte.
Dezelfde fysieke kloon. Dezelfde APFS-partitie. Een andere macOS-apparaatinterface.
fsapfsinfo opende de container en vond één volume. Daarna testte ik exact het bestand van 56.309 bytes dat TSK niet had kunnen extraheren.
TSK had geproduceerd:
expected: 56,309
recovered: 0
APFSBlock failure
Via libfsapfs leverde de bestandsvermelding:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
Dit was het eerste resultaat dat de diagnose wezenlijk veranderde. De gekloonde bytes waren niet veranderd. Het testbestand was niet veranderd. Het beschadigde APFS was niet gerepareerd.
De APFS-implementatie was veranderd.
Op zijn minst wist ik nu dat een echt gebruikersbestand dat via TSK onherstelbaar leek, via libfsapfs nog diep genoeg bereikbaar was om de volledige inhoud te lezen en een digest te berekenen.
Ik extraheerde één bekend problematisch bestand voordat ik libfsapfs terabytes toevertrouwde
Een geslaagde digest was voor mij niet genoeg om een herstel van meerdere terabytes te starten. Ik wilde de daadwerkelijke bytes op de doelschijf.
In plaats van de C-API van libfsapfs te raden, inspecteerde ik de broncode van de bibliotheek en volgde ik het leespad dat fsapfsinfo al gebruikte bij het berekenen van de digest. Daarna bouwde ik een kleine alleen-lezen extractor voor dat ene bekende testbestand.
De extractor schreef exact:
56,309 bytes
naar de aparte herstelschijf. De MD5 van het geëxtraheerde bestand was:
c6f56db33eafc1de0f52a035bc255dc7
Die kwam overeen met de eerdere digest.
Pas daarna schaalde ik de aanpak op. De test met één bestand had drie afzonderlijke dingen bewezen: de padnaam kon worden opgelost, het volledige verwachte aantal bytes kon worden geëxtraheerd en de geëxtraheerde bytes leverden dezelfde digest op als de eerdere volledige inhoudslezing.
Bij bulkherstel draaide het vooral om foutisolatie en hervatten
Zodra libfsapfs een bestand kon herstellen waar TSK niet in slaagde, veranderde het moeilijke probleem opnieuw. Ik had een systeem nodig dat een zeer grote directoryboom kon verwerken zonder dat één beschadigde tak, één herstart of één Ctrl+C de taak vanaf nul liet beginnen.
De bulkherstelpipeline gebruikte daarom een paar strikte invarianten:
- De masterkloon werd alleen-lezen geopend.
- Herstelde uitvoer werd alleen naar de tweede schijf van 5 TB geschreven.
- Directory- en bestandsnamen bleven behouden.
- De voortgang stond in SQLite zodat die procesafsluitingen en herstarts overleefde.
- Elk bestand werd eerst naar een tijdelijk pad geschreven.
- Een tijdelijk bestand werd pas naar het definitieve pad hernoemd nadat de volledige verwachte grootte was geschreven.
- Bestaande bestanden met de verwachte grootte konden bij hervatten worden herkend.
- Mislukt of problematisch werk werd gescheiden gehouden van voltooid werk.
- Vrije ruimte werd gecontroleerd en er bleef een veiligheidsreserve over.
- Opnieuw genereerbare ontwikkelartefacten en systeemmetadata met lage waarde konden worden overgeslagen of lager geprioriteerd.
Eén prestatieverbetering maakte meteen verschil: ik stopte met het voor elk bestand afzonderlijk opnieuw openen van de APFS-container.
Het snelle pad verwerkte één directory per keer. Een worker opende de bron, somde die directory op, herstelde de direct daarin aanwezige reguliere bestanden en zette subdirectories terug in de wachtrij. Als een directory faalde of een time-out kreeg, markeerde de controller die als DEFERRED en ging verder in plaats van de globale ronde te blokkeren.
Nadat er geen normaal werk meer in behandeling was, werden uitgestelde directories opnieuw bezocht via een trager terugvalpad met sterker geïsoleerd werk per bestand. Resterende fouten konden daarna afzonderlijk opnieuw worden geprobeerd.
directory ontdekken
↓
directe bestanden herstellen
↓
verwachte groottes controleren
↓
duurzame toestand vastleggen
↓
subdirectories in de wachtrij zetten
↓
lokale fouten uitstellen
↓
globaal doorgaan
↓
later fallback en opnieuw proberen
Deze architectuur paste veel beter bij het werkelijke foutpatroon dan één enorm recursief commando. De schade was niet uniform, dus het herstelsysteem moest de voortgang ook niet uniform maken.
Waarom ik controles op verwachte grootte en atomisch hernoemen gebruikte in plaats van miljoenen bestanden te hashen
MD5 was nuttig bij het bewijs met één bestand omdat ik sterk bewijs nodig had dat de parser de volledige inhoud las van een bestand dat TSK niet kon lezen.
Elke herstelde byte nog een tweede keer volledig lezen alleen om miljoenen bestanden te hashen, zou veel extra I/O hebben toegevoegd. Voor de hoofdextractieronde gebruikte ik daarom een andere invariant.
Voor elk regulier bestand leverden de APFS-metadata een verwachte grootte. De worker schreef naar een tijdelijk bestand en promoveerde het pas naar het definitieve pad nadat de volledige leesactie overeenkwam met die verwachte grootte.
Dat betekent dat een onderbreking geen te kort bestand onder de definitieve naam zou moeten achterlaten.
Een overeenkomst in grootte is geen cryptografisch bewijs van integriteit. Ik behandel het ook niet als zodanig. Maar verificatie van de verwachte grootte plus atomisch hernoemen vormde een praktische correctheidsgrens voor de ronde met hoog volume, terwijl gerichte hashes nuttig bleven voor steekproeven en bekende foutgevallen.
SQLite maakte een herstart saai in plaats van rampzalig
Het herstel duurde lang genoeg dat ik de computer moest uitzetten en later verder moest gaan. Die eis veranderde het ontwerp van “script” in “hervatbare workflow”.
Een Ctrl+C liet het actieve subproces niet gewoon achter. De controller ving de onderbreking op, stopte de worker, bracht de actieve directory terug naar een herstelbare toestand, committe SQLite en sloot af.
Een nette stop zag er conceptueel zo uit:
HUIDIGE DIRECTORY -> PENDING
CTRL+C: HERSTEL VEILIG GESTOPT
TOESTAND OPGESLAGEN. VOER HETZELFDE COMMANDO UIT OM TE HERVATTEN.
Na het herstarten herhaalde ik de controles van de schijfidentiteit en startte ik hetzelfde herstelcommando. De toestandsdatabase hervatte de bestaande wachtrij.
Een latere herstart begon met:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
Dat was veel betekenisvoller dan een generieke voortgangsbalk. Het liet zien dat tienduizenden voltooide directory-eenheden de herstart hadden overleefd en dat de resterende wachtrij expliciet bekend was.
Bij een eerdere hervatting werd de directory die in de vorige sessie was onderbroken opnieuw opgepakt. Bestanden die al met de verwachte grootte aanwezig waren, werden herkend en alleen het ontbrekende werk hoefde te worden geschreven. Dat was het gedrag dat ik wilde: herstel opnieuw starten moest routine zijn, niet beangstigend.
326,799 bestanden waren het eerste bewijs dat de methode schaalde
Voordat ik de nieuwe extractor uitbreidde naar alle geselecteerde gegevens, gebruikte ik één grote prioriteitsboom als doel voor bulkvalidatie.
De voltooide toestand rapporteerde:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
De bestemming bevatte:
326,799 files
145,039,215,948 bytes
of ongeveer:
135.08 GiB
De bestandsaantallen sloten exact op elkaar aan:
302,541 + 24,258 = 326,799
Er bleven geen .partial.*-bestanden achter in die voltooide boom.
Het testbestand van 56.309 bytes had bewezen dat de parser kon slagen waar TSK faalde. Het herstel van 326.799 bestanden bewees dat dezelfde aanpak een substantiële echte hiërarchie aankon met hervatlogica, detectie van bestaande bestanden en zonder geregistreerde bestandsfouten in die voltooide ronde.
Het grotere herstel passeerde 1,6 miljoen nieuwe bestanden voordat het klaar was
Nadat de prioriteitsboom netjes was voltooid, breidde ik het herstel uit naar de resterende geselecteerde gegevens op het hoogste niveau.
Bij één bewust veilige stop rapporteerde 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
Dat was ongeveer:
840.72 GiB
aan nieuw geschreven gegevens die op dat moment door de directoryrondes waren geregistreerd.
De ene DEFERRED-directory betekende niet dat er gegevens verloren waren. Het betekende dat het snelle pad bewust was gestopt met dat lokale probleem niet-gerelateerd werk te laten vertragen. De terugvalfase bestond juist om zulke gevallen later opnieuw te bezoeken.
Latere sessies hervatten vanuit dezelfde database. Het aantal voltooide eenheden nam toe en de wachtrij met openstaand werk nam af. Uiteindelijk herstelde ik elke geselecteerde, opgesomde directoryboom die ik nodig had.
Wat ik eerlijk kan beweren over het uiteindelijke herstel
Ik zal het resultaat niet omschrijven als “elke byte hersteld”. Het bewijs ondersteunt dat niet.
De oorspronkelijke ddrescue-map bevatte nog steeds ongeveer 13,84 MB waarvan niet was bevestigd dat die met succes was gekopieerd. Ik kan ook niet bewijzen dat geen enkel bestandssysteemobject volledig onvindbaar is geworden doordat de metadata die nodig waren om het op te sommen zich in de beschadigde regio's bevonden.
Die beperkingen zijn belangrijk omdat gestructureerd herstel kan bewijzen dat een opgesomd object is geëxtraheerd; het kan niet bewijzen dat een object historisch nooit heeft bestaan wanneer de beschadigde namespace het niet meer kan onthullen.
De sterkste eindconclusie is smaller:
Elke geselecteerde, opgesomde directoryboom die ik nodig had, is via het gestructureerde herstelproces met succes hersteld.
Ik hoefde de masterkloon niet ter plaatse te repareren. Ik hoefde de oorspronkelijke klikkende Toshiba niet opnieuw te gebruiken voor de bulkextractie. Ik had geen PhotoRec-achtige carvingronde over de hele schijf nodig die de directorystructuur en bestandsnamen zou opofferen.
De mislukte tools waren nog steeds bruikbaar bewijs
Achteraf klinkt het succesvolle pad eenvoudig:
ddrescue-kloon
↓
libfsapfs
↓
hervatbare extractor
↓
herstelde bestanden
Zo voelde het onderzoek niet terwijl het bezig was, en de mislukte benaderingen weglaten zou een groot deel van de nuttige engineeringles wegnemen.
TSK leerde me dat het doorlopen van de APFS-namespace en het ophalen van inhoud verschillende foutmodi waren.
De fout in mijn eerste wrapper leerde me dat de foutclassificatie van een herstelscript zelf niet de absolute waarheid is.
Het hotspotmechanisme leerde me lokale fouten te isoleren in plaats van toe te staan dat ze de globale voortgang stillegden.
Het checkpointexperiment leerde me APFS-transactiecheckpoints niet te verwarren met snapshots.
De FUSE-pogingen leerden me dat een mislukte mount meerdere lagen naast de bestandssysteemgegevens zelf kan impliceren.
De APFS-reader die op --help bleef hangen leerde me het gereedschap te valideren voordat ik de diagnostiek ervan interpreteerde.
Het gedrag van /dev/rdiskNs2 tegenover /dev/diskNs2 leerde me dat het I/O-pad van het besturingssysteem het gedrag van een parser kan veranderen, zelfs wanneer de onderliggende schijf identiek is.
En de architectuur met twee schijven gaf me de vrijheid om overal elders fouten te maken terwijl de masterkloon ongewijzigd bleef.
De herstelworkflow die ik opnieuw zou gebruiken
- Stop gewone bestandssysteemactiviteit op mechanisch falende opslag. Als leesacties blijven hangen, het apparaat verdwijnt of klikt, zou ik een hervatbare kloon op blokniveau prioriteit geven boven verkennen in Finder.
- Gebruik GNU ddrescue met een permanente mapfile en een geverifieerd hersteldomein. De mapfile bewaart de voortgang; een bekende brongrootte voorkomt dat verwarring over de apparaatgrootte onderdeel van het herstel wordt.
- Houd één masterkloon alleen-lezen. Repareer hem niet, vergroot hem niet, partitioneer hem niet opnieuw en gebruik hem niet als opslag voor herstelde bestanden.
- Schrijf herstelde bestanden naar een tweede fysieke schijf. Het behouden van de bron en het opslaan van uitvoer zijn verschillende taken.
- Diagnosticeer het gekloonde bestandssysteem eerst alleen-lezen. Een fysiek gezonde kloon met beschadigde APFS-metadata is een logisch herstelprobleem, niet hetzelfde probleem als een klikkende bronschijf.
- Kies één reproduceerbaar mislukt bestand als parsertest. Een bekend problematisch bestand van 56 KB vertelde me meer over alternatieve parsers dan uren blinde bulkextractie.
- Valideer de parser zelf. Als een tool blijft hangen voordat die de bron betekenisvol leest, interpreteer dat dan niet als bewijs dat de gegevens weg zijn.
- Ga er niet van uit dat één APFS-implementatie herstelbaarheid definieert. TSK en
libfsapfsgedroegen zich zeer verschillend op dezelfde gekloonde bytes. - Identificeer schijven aan stabiele eigenschappen, niet aan tijdelijke apparaatnummers. Een bestandssysteem-UUID en bekende fysieke grootte zijn veiliger dan het
/dev/diskNvan gisteren. - Maak langdurige extractie vanaf het begin hervatbaar. Duurzame toestand, tijdelijke bestanden, atomisch hernoemen, uitgesteld werk, begrensde retries en nette afhandeling van afsluiten horen op deze schaal bij correctheid.
- Scheid validatieniveaus. Een ddrescue-percentage, een zichtbare padnaam, een overeenkomst met de verwachte grootte en een inhoudshash bewijzen verschillende dingen.
Het getal dat op de finish leek, was alleen het einde van fase één
Het meest misleidende getal van het hele herstel was nog steeds:
100.00%
Het leek een antwoord op “Heb ik de schijf gered?”
Wat het werkelijk beantwoordde, was veel beperkter:
Hoeveel van het fysieke hersteldomein heeft ddrescue met succes gekopieerd?
Het beantwoordde niet of APFS de namespace kon reconstrueren. Het beantwoordde niet of de ene parser beschadigde metadata kon doorlopen die een andere parser afwees. Het beantwoordde niet of een hersteld bestand de verwachte lengte had. En het beantwoordde niet of een extractie van miljoenen bestanden fouten en herstarts kon overleven zonder de eigen toestand te beschadigen.
Voor elke laag had ik afzonderlijk bewijs nodig.
Het fysieke herstel slaagde eerst. Het bestandssysteem was nog steeds beschadigd. De eerste parser kon namen zichtbaar maken maar faalde op veel bestandsinhoud. Een andere APFS-implementatie las hetzelfde testbestand met succes. Dat bewijs werd een extractor voor één bestand, die extractor werd een hervatbare herstel-engine en die engine herstelde uiteindelijk de geselecteerde directorybomen die ik nodig had naar een tweede schijf terwijl de masterkloon onaangeroerd bleef.
De oorspronkelijke harde schijf werd nooit weer gezond. APFS repareerde zichzelf niet op magische wijze. Wat veranderde was het herstelmodel.
Blokherstel, bestandssysteemherstel en bestandsvalidatie zijn afzonderlijke engineeringfasen. Door ze als één probleem te behandelen leek de situatie bijna hopeloos. Door ze van elkaar te scheiden werd ze beheersbaar.