#!/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()