47 lines
1.7 KiB
Python
47 lines
1.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Recursive ext4 directory dumper by inode number.
|
|
Bypasses all metadata validation - uses extent trees directly.
|
|
"""
|
|
import struct, os, sys, stat
|
|
from pathlib import Path
|
|
|
|
DEV = '/dev/dm-0'
|
|
BLOCK = 4096
|
|
BACKUP_SB_BLOCK = 32768
|
|
|
|
import ext4lib
|
|
|
|
# ── main ─────────────────────────────────────────────────────────────────────
|
|
|
|
import argparse
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Recover ext4 directory tree by inode')
|
|
parser.add_argument('inode', type=int, help='Root inode number')
|
|
parser.add_argument('dest', help='Destination directory')
|
|
parser.add_argument('--skip-existing', action='store_true',
|
|
help='Skip recovery if destination directory already exists and is non-empty')
|
|
args = parser.parse_args()
|
|
|
|
if args.skip_existing and os.path.isdir(args.dest) and os.listdir(args.dest):
|
|
print(f"Skipping inode {args.inode} -> {args.dest} (already exists)")
|
|
sys.exit(0)
|
|
|
|
with open(DEV, 'rb') as f:
|
|
sb_data = ext4lib.read_at(f, BACKUP_SB_BLOCK * BLOCK, 1024)
|
|
sb = ext4lib.parse_superblock(sb_data)
|
|
assert sb['magic'] == 0xef53
|
|
|
|
num_groups = (sb['blocks_count'] + sb['blocks_per_group'] - 1) \
|
|
// sb['blocks_per_group']
|
|
gdt_data = ext4lib.read_at(f, (BACKUP_SB_BLOCK + 1) * BLOCK,
|
|
num_groups * sb['desc_size'])
|
|
|
|
print(f"Dumping inode {args.inode} -> {args.dest}")
|
|
ext4lib.dump_tree(f, sb, gdt_data, args.inode, args.dest)
|
|
print(f"Done inode {args.inode}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|