Dotfiles
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

130 lines
4.7 KiB

#!/usr/bin/env python3
# Depends on 7z in path
import libarchive
import argparse
import pathlib
import os
def parse_args(argv):
parser = argparse.ArgumentParser(prog=argv[0], description="""
Patch a folder structure with files from an archive.
This will replace existing files with those of the same name in an archive,
with the option to back up the old versions and generate a script to revert the changes.
""")
parser.add_argument("-p", "--strip", type=int, default=0, help="Strip NUM leading components from archived file names.")
parser.add_argument("-C", "--directory", default=".", help="Operate in <direcory>")
parser.add_argument("-b", "--backup", default=None, help="Create backup copies of overwritten files")
parser.add_argument("-m", "--match", default=None, help="Only extract files matching GLOB", metavar="GLOB")
parser.add_argument("-u", "--uninstall-script", default=os.devnull, help="Filename to save an uninstall-scipt to.", metavar="FILE")
parser.add_argument("-n", "--dry-run", action="store_true", help="Perform a dry run")
parser.add_argument("archive", help="Achive file name")
return parser.parse_args(argv[1:])
def makedirs(path, dryrun=False):
if path.is_dir():
return set()
created = set()
stack = [path]
while stack:
path = stack[-1]
if path.parent.is_dir():
if path.exists():
raise IOError("Exists but not a directory: '%s'" % path)
if dryrun:
return set(stack)
os.mkdir(path)
created.add(stack.pop())
else:
stack.append(path.parent)
return created
def main(argv):
args = parse_args(argv)
output_path = pathlib.Path(args.directory)
backup_path = pathlib.Path(args.backup) if args.backup else None
folders = set()
with open(args.uninstall_script, "w") as us:
# Uninstall Header
if args.uninstall_script != os.devnull:
us.write("#!/bin/sh\n"
"# Automated patchdir uninstall script\n"
"# Run from inside patchdir's target directory (-C)\n"
"remove() {\n"
" echo Removing $1\n"
" rm \"$1\"\n"
"}\n\n")
if backup_path:
us.write(("BACKUP_PATH='%s'\n\n"
"restore() {\n"
" echo Restoring $1 from $BACKUP_PATH\n"
" mv \"$BACKUP_PATH/$1\" \"$1\"\n"
"}\n\n") % backup_path.relative_to(output_path))
else:
us.write("remove-unsafe() {\n"
" echo Removing $1\n"
" rm \"$1\"\n"
" echo WARNING: Previously existing file $1 is now missing!\n"
"}\n\n")
us.write("\n# Restore files\n")
with libarchive.file_reader(args.archive) as archive:
for entry in archive:
epath = pathlib.PurePath(entry.path)
if args.match and not epath.match(args.match):
continue
if args.strip:
epath = pathlib.PurePath(*epath.parts[args.strip:])
dpath = output_path.joinpath(epath)
if entry.isdir:
folders |= makedirs(dpath, args.dry_run)
else:
folders |= makedirs(dpath.parent, args.dry_run)
if dpath.exists():
# Backup
if backup_path:
print("Backing up existing %s" % epath)
bpath = backup_path.joinpath(epath)
folders |= makedirs(bpath.parent, args.dry_run)
if not args.dry_run:
os.rename(dpath, bpath)
us.write("restore '%s'\n" % epath)
else:
us.write("remove-unsafe '%s'\n" % epath)
else:
us.write("remove '%s'\n" % epath)
print("Extracting %s" % epath)
if not args.dry_run:
with open(dpath, "wb") as f:
for chunk in entry.get_blocks():
f.write(chunk)
if args.uninstall_script != os.devnull and folders:
us.write("\n# Remove folders\n")
for dir in sorted(folders, key=lambda x: len(x.parts), reverse=True):
us.write("rmdir '%s'\n" % dir)
if __name__ == "__main__":
import sys
sys.exit(main(sys.argv))