134 lines
3.9 KiB
Python
134 lines
3.9 KiB
Python
#!/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')
|