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
130 lines
4.7 KiB
8 years ago
|
#!/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))
|
||
|
|