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.
129 lines
4.7 KiB
129 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)) |
|
|
|
|