MacBook Proに接続したこの外付けSSDへ巨大なデータをコピーしていると、なぜかコピー処理がハングした。外付けSSDを取り出そうにも「プログラムが使用している可能性があります」とか言われて通常の取り出し操作ができなくなった。選択肢に「強制的に取り出す」があったので一瞬嫌な予感がしたもののしょうがないのでクリックしたところ、案の定、外付けSSDが認識されなくなった。
ストレージ動作不良の時に使う定番のFirst Aidを試すも復旧できず。diskutilでストレージの状態を調べると
% sudo diskutil list
Password:
/dev/disk0 (internal, physical):
0: GUID_partition_scheme *2.0 TB disk0
1: EFI EFI 314.6 MB disk0s1
2: Apple_APFS Container disk1 2.0 TB disk0s2
/dev/disk1 (synthesized):
0: APFS Container Scheme - +2.0 TB disk1
Physical Store disk0s2
1: APFS Volume Macintosh HD 11.2 GB disk1s1
2: APFS Volume Macintosh HD - Data 609.6 GB disk1s2
3: APFS Volume Preboot 86.6 MB disk1s3
4: APFS Volume Recovery 530.0 MB disk1s4
5: APFS Volume VM 5.4 GB disk1s5
/dev/disk2 (external, physical):
0: GUID_partition_scheme *960.2 GB disk2
1: EFI EFI 209.7 MB disk2s1
2: Apple_APFS Container disk3 960.0 GB disk2s2
/dev/disk3 (synthesized):
0: APFS Container Scheme - +ERROR disk3
Physical Store disk2s2
/dev/disk3
のボリューム0のSIZEが ERROR
になってる。
「Mac ストレージ 復旧」とかで検索すると復旧できそうなサードパーティーアプリが見つかるものの、怪しい中華製アプリ入れたくないし$100も払いたくないので、もう少し悪足掻きすることにした。
Drat
インターネットを検索していると、DratというOSSのツールに辿り着いた。
github.com
Drat is a tool for analysing and recovering data from APFS (Apple File System) partitions.
DratはAPFSパーティションからデータを復旧するためのツールということで、どうやらこれが使えそう。
実際、
Currently, all of Drat's commands (except modify, which is currently disabled as it is not fit for use) operate in a read-only fashion, as they are intended to be used in situations involving data recovery or data forensics.
とあり、Dratの実行が対象データに変更を加えないことが謳われていることは安心材料にもなった。
Dratの主要機能
Dratには複数のコマンドがあるが、今回使うのは list
と recover
list
指定したパスに存在するファイルとディレクトリの一覧を表示する。
たとえば、以下のコマンドでは /dev/disk2s2
コンテナのボリュームIDが 0
のボリュームの /
(ルートディレクトリ)に存在するファイルとディレクトリの一覧を表示する。
% sudo ./drat list /dev/disk2s2 0 /
Opening file at `/dev/disk2s2` in read-only mode ... OK.
Simulating a mount of the APFS container.
Validating checksum of block 0x0 ... OK.
Locating the checkpoint descriptor area:
- Its length is 280 blocks.
- It is contiguous.
- The address of its first block is 0x1.
Loading the checkpoint descriptor area into memory ... OK.
Locating the most recent well-formed container superblock in the checkpoint descriptor area:
- It lies at index 169 within the checkpoint descriptor area.
- The corresponding checkpoint starts at index 168 within the checkpoint descriptor area, and spans 2 blocks.
Loading the corresponding checkpoint ... OK.
- There are 9 checkpoint-mappings in this checkpoint.
Reading the Ephemeral objects used by this checkpoint ... OK.
Validating the Ephemeral objects ... OK.
The container superblock states that the container object map has Physical OID 0x13fa2d.
Loading the container object map ... OK.
Validating the container object map ... OK.
Reading the root node of the container object map B-tree ... OK.
Validating the root node of the container object map B-tree ... OK.
The container superblock lists 1 APFS volumes, whose superblocks have the following Virtual OIDs:
- 0x402
Reading the APFS volume superblocks ... OK.
Validating the APFS volume superblocks ... OK.
Volume list
================
0: Extreme 900
The volume object map has Physical OID 0x13f96d.
Reading the volume object map ... OK.
Validating the volume object map ... OK.
Reading the root node of the volume object map B-tree ... OK.
Validating the root node of the volume object map B-tree ... OK.
The file-system tree root for this volume has Virtual OID 0x404.
Looking up this Virtual OID in the volume object map ... corresponding block address is 0x140fa9.
Reading ... validating ... OK.
Records for file-system object 0x2 -- `/` --
- INODE
- XATTR || name = purgeable-drecs-fixed
- DIR REC || Dirctry || target ID = 0xa4 || name = TownHallOrgan_SP
- DIR REC || Dirctry || target ID = 0xa1 || name = SanDisk
- DIR REC || Dirctry || target ID = 0x9e || name = JGV1_20180521
- DIR REC || Dirctry || target ID = 0x10 || name = .Spotlight-V100
- DIR REC || Dirctry || target ID = 0xa3 || name = STEAM
- DIR REC || Dirctry || target ID = 0x9b || name = e-onkyo
- DIR REC || Dirctry || target ID = 0xa0 || name = PREMIER SOUND FACTORY 2.0
- DIR REC || Dirctry || target ID = 0x9d || name = Ivory Items
- DIR REC || Dirctry || target ID = 0x2dde4 || name = Logic200531
- DIR REC || Dirctry || target ID = 0xa5 || name = V-Metal Library
- DIR REC || Dirctry || target ID = 0x30dab || name = Logic
- DIR REC || Dirctry || target ID = 0x9c || name = Hummingbird
- DIR REC || Dirctry || target ID = 0xa2 || name = Spitfire
- DIR REC || Dirctry || target ID = 0x2dd90 || name = .Trashes
- DIR REC || Dirctry || target ID = 0x1a || name = .fseventsd
- DIR REC || Dirctry || target ID = 0x2ec46 || name = .DocumentRevisions-V100
- DIR REC || Dirctry || target ID = 0x9f || name = Play Libraries
- DIR REC || Dirctry || target ID = 0x2ec4e || name = .TemporaryItems
recover
指定したパスに存在する単一ファイルを抽出する。
たとえば、以下のコマンドでは /dev/disk2s2
コンテナのボリュームIDが 0
のボリュームの /a/b/c.jpg
のパスに存在するJPGファイルをカレントディレクトリに同名ファイルとして出力する。
sudo ./drat recover /dev/disk2s2 0 /a/b/c.jpg >& ./c.jpg
Dratで再帰的にディレクトリの中身をサルベージする
残念ながらDratの recover
コマンドには重要な欠点がある。2023/1/1時点の最新版であるv0.1.3では、単一ファイルの復元にのみ対応しており、ディレクトリを対象にした復元ができない点だ。このissueによると、v0.2.0でディレクトリを対象とした復元も実装予定だが、現時点では未対応とのこと。(※注:Dratにはやばいトラップがあり、readthedocsのドキュメントはまだリリースされていないv0.2.0の仕様を前提に書かれている。v0.1.3の使用時には参考にしてはいけない)
そこで、指定したパスを再帰的に探索し、ディレクトリの構造を維持した状態でパス直下をすべて recover
で復元するスクリプトをPythonで実装することにした。以下のGitHubレポジトリで公開している。
github.com
実行方法もREADMEに記載した。