Initial remote commit
This commit is contained in:
131
test/restore_meta.py
Normal file
131
test/restore_meta.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Restore ext4 metadata (permissions, ownership, timestamps) to a recovered tree.
|
||||
Run after dump_tree.py has extracted files.
|
||||
|
||||
Usage: python3 restore_meta.py <inode> <dest_dir>
|
||||
"""
|
||||
import struct, os, sys, stat, ctypes, ctypes.util
|
||||
|
||||
DEV = '/dev/dm-0'
|
||||
BLOCK = 4096
|
||||
BACKUP_SB_BLOCK = 32768
|
||||
|
||||
# ── reuse same low-level helpers from dump_tree.py ───────────────────────────
|
||||
# (paste parse_superblock, parse_gdt_entry, read_at, read_inode,
|
||||
# read_extent_tree_blocks, read_dir_entries here)
|
||||
# or factor them into a shared ext4lib.py and import from both scripts
|
||||
|
||||
import ext4lib
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
|
||||
|
||||
class Timeval(ctypes.Structure):
|
||||
_fields_ = [('tv_sec', ctypes.c_long), ('tv_usec', ctypes.c_long)]
|
||||
|
||||
def lutimes(path, atime, mtime):
|
||||
times = (Timeval * 2)((atime, 0), (mtime, 0))
|
||||
libc.lutimes(path.encode(), ctypes.byref(times))
|
||||
|
||||
def get_inode_meta(idata, slot, sb):
|
||||
mode = struct.unpack_from('<H', idata, slot + 0)[0]
|
||||
uid = struct.unpack_from('<H', idata, slot + 2)[0]
|
||||
gid = struct.unpack_from('<H', idata, slot + 24)[0]
|
||||
atime = struct.unpack_from('<I', idata, slot + 8)[0]
|
||||
mtime = struct.unpack_from('<I', idata, slot + 16)[0]
|
||||
|
||||
if sb['inode_size'] >= 256:
|
||||
atime_extra = struct.unpack_from('<I', idata, slot + 132)[0]
|
||||
mtime_extra = struct.unpack_from('<I', idata, slot + 140)[0]
|
||||
atime |= (atime_extra & 0x3) << 32
|
||||
mtime |= (mtime_extra & 0x3) << 32
|
||||
|
||||
uid_hi, gid_hi = struct.unpack_from('<HH', idata, slot + 120)
|
||||
uid |= uid_hi << 16
|
||||
gid |= gid_hi << 16
|
||||
|
||||
return stat.S_IMODE(mode), uid, gid, atime, mtime
|
||||
|
||||
def restore_meta(f, sb, gdt_data, inum, dest_path):
|
||||
try:
|
||||
idata, slot = ext4lib.read_inode(f, sb, gdt_data, inum)
|
||||
mode, uid, gid, atime, mtime = get_inode_meta(idata, slot, sb)
|
||||
is_symlink = os.path.islink(dest_path)
|
||||
|
||||
try:
|
||||
os.lchown(dest_path, uid, gid)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if not is_symlink:
|
||||
try:
|
||||
os.chmod(dest_path, mode)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
lutimes(dest_path, atime, mtime)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
print(f" WARN {dest_path}: {e}", file=sys.stderr)
|
||||
|
||||
def walk_and_restore(f, sb, gdt_data, inum, dest_dir, visited=None):
|
||||
if visited is None:
|
||||
visited = set()
|
||||
if inum in visited:
|
||||
return
|
||||
visited.add(inum)
|
||||
|
||||
# Restore the directory itself
|
||||
restore_meta(f, sb, gdt_data, inum, dest_dir)
|
||||
|
||||
try:
|
||||
entries = ext4lib.read_dir_entries(f, sb, gdt_data, inum)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
for name, (child_inum, ftype) in entries.items():
|
||||
if name in ('.', '..'):
|
||||
continue
|
||||
safe_name = name.replace('/', '_').replace('\x00', '')
|
||||
dest = os.path.join(dest_dir, safe_name)
|
||||
|
||||
if not os.path.lexists(dest):
|
||||
# File wasn't recovered - skip
|
||||
continue
|
||||
|
||||
if os.path.isdir(dest) and not os.path.islink(dest):
|
||||
walk_and_restore(f, sb, gdt_data, child_inum, dest, visited)
|
||||
else:
|
||||
restore_meta(f, sb, gdt_data, child_inum, dest)
|
||||
|
||||
# Restore directory timestamps AFTER processing children
|
||||
# (writing children updates parent dir mtime/atime)
|
||||
restore_meta(f, sb, gdt_data, inum, dest_dir)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(f"Usage: {sys.argv[0]} <inode> <dest_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
root_inum = int(sys.argv[1])
|
||||
dest_dir = sys.argv[2]
|
||||
|
||||
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"Restoring metadata: inode {root_inum} -> {dest_dir}")
|
||||
walk_and_restore(f, sb, gdt_data, root_inum, dest_dir)
|
||||
print("Done")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user