Initial remote commit
This commit is contained in:
117
restore_meta.py
Executable file
117
restore_meta.py
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stage 4 – Restore filesystem metadata (permissions, ownership, timestamps) to
|
||||
an already-extracted directory tree.
|
||||
|
||||
Run this after extract_tree.py if you did not use --restore-meta there, or to
|
||||
re-apply metadata without re-extracting files.
|
||||
|
||||
Usage:
|
||||
python3 restore_meta.py [options] <inode> <dest_dir>
|
||||
|
||||
Options:
|
||||
--device DEV Block device [/dev/dm-0]
|
||||
--backup-sb BLOCK Backup superblock block number [32768]
|
||||
--db PATH Optional: read metadata from SQLite DB instead of
|
||||
re-reading the device (faster) [inodes.db if present]
|
||||
"""
|
||||
import argparse, ctypes, ctypes.util, os, stat, sys
|
||||
import ext4lib
|
||||
|
||||
DEFAULT_DEV = '/dev/dm-0'
|
||||
DEFAULT_BACKUP_SB = 32768
|
||||
|
||||
# ── libc lutimes ──────────────────────────────────────────────────────────────
|
||||
|
||||
_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)((_Timeval(atime, 0)), (_Timeval(mtime, 0)))
|
||||
_libc.lutimes(path.encode(), ctypes.byref(times))
|
||||
|
||||
|
||||
# ── per-path restore ──────────────────────────────────────────────────────────
|
||||
|
||||
def _apply_meta(dest_path, perms, uid, gid, atime, mtime):
|
||||
is_link = os.path.islink(dest_path)
|
||||
try:
|
||||
os.lchown(dest_path, uid, gid)
|
||||
except OSError:
|
||||
pass
|
||||
if not is_link:
|
||||
try:
|
||||
os.chmod(dest_path, perms)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
_lutimes(dest_path, atime, mtime)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def restore_one(f, sb, gdt_data, inum, dest_path):
|
||||
try:
|
||||
idata, slot = ext4lib.read_inode(f, sb, gdt_data, inum)
|
||||
perms, uid, gid, atime, mtime = ext4lib.get_inode_meta(idata, slot, sb)
|
||||
_apply_meta(dest_path, perms, uid, gid, atime, mtime)
|
||||
except Exception as e:
|
||||
print(f" WARN {dest_path}: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
# ── recursive walker ──────────────────────────────────────────────────────────
|
||||
|
||||
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 first (to set ownership before descending)
|
||||
restore_one(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):
|
||||
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_one(f, sb, gdt_data, child_inum, dest)
|
||||
|
||||
# Re-apply directory timestamps after children are written
|
||||
restore_one(f, sb, gdt_data, inum, dest_dir)
|
||||
|
||||
|
||||
# ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Restore ext4 metadata to extracted tree (Stage 4)')
|
||||
parser.add_argument('inode', type=int)
|
||||
parser.add_argument('dest_dir')
|
||||
parser.add_argument('--device', default=DEFAULT_DEV)
|
||||
parser.add_argument('--backup-sb', type=int, default=DEFAULT_BACKUP_SB)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.device, 'rb') as f:
|
||||
sb, gdt_data, _ = ext4lib.load_fs(f, args.backup_sb)
|
||||
print(f"Restoring metadata: inode {args.inode} → {args.dest_dir}")
|
||||
walk_and_restore(f, sb, gdt_data, args.inode, args.dest_dir)
|
||||
print("Done")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user