#!/usr/bin/env python3 """ Build directory tree from ils output using fls parent pointers. For each directory inode: 1. Run fls to get its contents and parent (..) 2. Record parent->child relationships 3. Walk parent chain to resolve full path 4. Place unreachable dirs in /orphans/ """ import subprocess, sys, os, collections DEVICE = '/dev/nbd0' OUTDIR = '/mnt/recovered' MIN_INODE = 106497 # first intact group def fls(inode): try: r = subprocess.run( ['fls', DEVICE, str(inode)], capture_output=True, text=True, timeout=30 ) entries = [] for line in r.stdout.splitlines(): try: parts = line.split(None, 2) if len(parts) < 3: continue type_str = parts[0] ino_str = parts[1].rstrip(':').lstrip('*') name = parts[2].strip() ino = int(ino_str) etype = type_str[0] entries.append((etype, ino, name)) except: continue return entries except: return [] # Load directory inodes from ils output print('Loading directory inodes...') dir_inodes = [] with open('/tmp/dir_inodes.txt') as f: for line in f: parts = line.strip().split('|') if len(parts) < 2: continue try: ino = int(parts[0]) if ino >= MIN_INODE: dir_inodes.append(ino) except: continue print(f'Found {len(dir_inodes)} directory inodes to process') # For each directory, get its parent and children # parent_of[inode] = parent_inode # children_of[inode] = [(child_inode, name, type)] parent_of = {} children_of = collections.defaultdict(list) names = {} # inode -> name (as seen from parent) print('Running fls on each directory...') for idx, ino in enumerate(dir_inodes): entries = fls(ino) for etype, eino, ename in entries: if ename == '..': parent_of[ino] = eino elif ename == '.': continue else: children_of[ino].append((eino, ename, etype)) names[eino] = ename if idx % 1000 == 0: print(f' {idx}/{len(dir_inodes)} processed...', flush=True) print(f'Built tree: {len(parent_of)} dirs with known parents') # Resolve full paths by walking parent chain resolved = {} # inode -> full path def resolve(ino, depth=0): if ino in resolved: return resolved[ino] if depth > 50: return None parent = parent_of.get(ino) if parent is None: path = f'orphans/{ino}' resolved[ino] = path return path # Check if parent is in intact groups if parent < MIN_INODE: # Parent is in zeroed groups — this is a root-level orphan # Use the name if we know it name = names.get(ino, str(ino)) path = f'orphans/{name}_{ino}' resolved[ino] = path return path parent_path = resolve(parent, depth + 1) if parent_path is None: path = f'orphans/{ino}' else: name = names.get(ino, str(ino)) path = os.path.join(parent_path, name) resolved[ino] = path return path print('Resolving paths...') for ino in dir_inodes: resolve(ino) # Print summary orphans = sum(1 for p in resolved.values() if p.startswith('orphans')) resolved_count = len(resolved) - orphans print(f'Resolved paths: {resolved_count}') print(f'Orphaned dirs: {orphans}') print() # Show interesting paths print('Sample resolved paths:') for ino, path in sorted(resolved.items(), key=lambda x: x[1]): if any(x in path for x in ['var','pterodactyl','docker','mysql', 'www','log','lib']): print(f' inode {ino:10d}: {path}') # Save full tree with open('/tmp/dir_tree.txt','w') as f: for ino, path in sorted(resolved.items(), key=lambda x: x[1]): f.write(f'{ino}\t{path}\n') print(f'Saved {len(resolved)} paths to /tmp/dir_tree.txt')