|
|
@ -4,11 +4,13 @@ |
|
|
|
#pylint: disable=missing-module-docstring,missing-function-docstring |
|
|
|
#pylint: disable=missing-module-docstring,missing-function-docstring |
|
|
|
|
|
|
|
|
|
|
|
import argparse |
|
|
|
import argparse |
|
|
|
|
|
|
|
import fnmatch |
|
|
|
import pathlib |
|
|
|
import pathlib |
|
|
|
import contextlib |
|
|
|
import contextlib |
|
|
|
import os |
|
|
|
import os |
|
|
|
import ctypes |
|
|
|
import ctypes |
|
|
|
import textwrap |
|
|
|
import textwrap |
|
|
|
|
|
|
|
import re |
|
|
|
import shlex |
|
|
|
import shlex |
|
|
|
|
|
|
|
|
|
|
|
import libarchive |
|
|
|
import libarchive |
|
|
@ -38,10 +40,10 @@ except AttributeError: |
|
|
|
ffi.c_int, ffi.check_int) |
|
|
|
ffi.c_int, ffi.check_int) |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
try: |
|
|
|
read_extract = ffi.read_extract |
|
|
|
read_extract2 = ffi.read_extract2 |
|
|
|
except AttributeError: |
|
|
|
except AttributeError: |
|
|
|
read_extract = ffi.ffi("read_extract", |
|
|
|
read_extract2 = ffi.ffi("read_extract2", |
|
|
|
[ffi.c_archive_p, ffi.c_archive_entry_p, ffi.c_int], |
|
|
|
[ffi.c_archive_p, ffi.c_archive_entry_p, ffi.c_archive_p], |
|
|
|
ffi.c_int, ffi.check_int) |
|
|
|
ffi.c_int, ffi.check_int) |
|
|
|
|
|
|
|
|
|
|
|
def to_bytes(path, encoding="utf-8"): |
|
|
|
def to_bytes(path, encoding="utf-8"): |
|
|
@ -89,11 +91,14 @@ def disk_reader(path=None, flags=0, lookup=True): |
|
|
|
ffi.read_free(ard_p) |
|
|
|
ffi.read_free(ard_p) |
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager |
|
|
|
@contextlib.contextmanager |
|
|
|
def file_writer_ext(filename): |
|
|
|
def file_writer_ext(filename, target_filename=None): |
|
|
|
archive_p = ffi.write_new() |
|
|
|
archive_p = ffi.write_new() |
|
|
|
try: |
|
|
|
try: |
|
|
|
bfn = to_bytes(filename, "fsencode") |
|
|
|
bfn = to_bytes(filename, "fsencode") |
|
|
|
|
|
|
|
if target_filename is None: |
|
|
|
write_set_format_filter_by_ext(archive_p, bfn) |
|
|
|
write_set_format_filter_by_ext(archive_p, bfn) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
write_set_format_filter_by_ext(archive_p, to_bytes(target_filename, "fsencode")) |
|
|
|
ffi.write_open_filename(archive_p, bfn) |
|
|
|
ffi.write_open_filename(archive_p, bfn) |
|
|
|
try: |
|
|
|
try: |
|
|
|
yield libarchive.write.ArchiveWrite(archive_p) |
|
|
|
yield libarchive.write.ArchiveWrite(archive_p) |
|
|
@ -102,40 +107,150 @@ def file_writer_ext(filename): |
|
|
|
finally: |
|
|
|
finally: |
|
|
|
ffi.write_free(archive_p) |
|
|
|
ffi.write_free(archive_p) |
|
|
|
|
|
|
|
|
|
|
|
def extract_to_disk(entry: libarchive.entry.ArchiveEntry, path: os.PathLike=None): |
|
|
|
def extract_entry(entry: libarchive.entry.ArchiveEntry, dst_p: libarchive.ffi.c_archive_p, path: os.PathLike=None): |
|
|
|
archive_p, entry_p = entry._archive_p, entry._entry_p #pylint:disable=protected-access |
|
|
|
archive_p, entry_p = entry._archive_p, entry._entry_p #pylint:disable=protected-access |
|
|
|
if path: |
|
|
|
if path: |
|
|
|
ffi.entry_update_pathname_utf8(entry_p, to_bytes(path, "utf-8")) |
|
|
|
ffi.entry_update_pathname_utf8(entry_p, to_bytes(path, "utf-8")) |
|
|
|
read_extract(archive_p, entry_p, |
|
|
|
read_extract2(archive_p, entry_p, dst_p) |
|
|
|
libarchive.extract.EXTRACT_TIME|libarchive.extract.EXTRACT_UNLINK) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def copy_entry(entry: libarchive.entry.ArchiveEntry, dst: libarchive.write.ArchiveWrite, path: os.PathLike=None, |
|
|
|
|
|
|
|
*, bufsize=ffi.page_size*8): |
|
|
|
|
|
|
|
archive_p, entry_p, dst_p = entry._archive_p, entry._entry_p, dst._pointer #pylint:disable=protected-access |
|
|
|
|
|
|
|
if path: |
|
|
|
|
|
|
|
ffi.entry_update_pathname_utf8(entry_p, to_bytes(path, "utf-8")) |
|
|
|
|
|
|
|
# read_extract2 and write_data_block only supported with archive_write_disk :( |
|
|
|
|
|
|
|
#read_extract2(archive_p, entry_p, dst_p) |
|
|
|
|
|
|
|
ffi.write_header(dst_p, entry_p) |
|
|
|
|
|
|
|
buf_p = ctypes.create_string_buffer(bufsize) |
|
|
|
|
|
|
|
read_data, write_data = ffi.read_data, ffi.write_data |
|
|
|
|
|
|
|
while (bytes_read := read_data(archive_p, buf_p, bufsize)) > 0: |
|
|
|
|
|
|
|
write_data(dst_p, buf_p, bytes_read) |
|
|
|
|
|
|
|
ffi.write_finish_entry(dst_p) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def copy_all_entries(src: libarchive.read.ArchiveRead, dst: libarchive.write.ArchiveWrite) -> list[pathlib.Path]: |
|
|
|
|
|
|
|
entries = [] |
|
|
|
|
|
|
|
with libarchive.entry.new_archive_entry() as entry_p: |
|
|
|
|
|
|
|
archive_p, dst_p = src._pointer, dst._pointer |
|
|
|
|
|
|
|
buf_p = ctypes.create_string_buffer(bufsize) |
|
|
|
|
|
|
|
read_data, write_data = ffi.read_data, ffi.write_data |
|
|
|
|
|
|
|
while ffi.read_next_header2(archive_p, entry_p) != ARCHIVE_EOF: |
|
|
|
|
|
|
|
entries.append(pathlib.Path(entry_pathname_w(entry_p))) |
|
|
|
|
|
|
|
#read_extract2(archive_p, entry_p, dst_p) |
|
|
|
|
|
|
|
ffi.write_header(dst_p, entry_p) |
|
|
|
|
|
|
|
while (bytes_read := read_data(archive_p, buf_p, bufsize)) > 0: |
|
|
|
|
|
|
|
write_data(dst_p, buf_p, bytes_read) |
|
|
|
|
|
|
|
ffi.write_finish_entry(dst_p) |
|
|
|
|
|
|
|
return entries |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_all_entries(src: libarchive.read.ArchiveRead) -> list[pathlib.Path]: |
|
|
|
|
|
|
|
entries = [] |
|
|
|
|
|
|
|
with libarchive.entry.new_archive_entry() as entry_p: |
|
|
|
|
|
|
|
src_p = src._pointer |
|
|
|
|
|
|
|
read_next_header2, ARCHIVE_EOF, entry_pathname_w, Path = \ |
|
|
|
|
|
|
|
ffi.read_next_header2, ffi.ARCHIVE_EOF, ffi.entry_pathname_w, pathlib.Path |
|
|
|
|
|
|
|
while read_next_header2(src_p, entry_p) != ffi.ARCHIVE_EOF: |
|
|
|
|
|
|
|
entries.append(Path(entry_pathname_w(entry_p))) |
|
|
|
|
|
|
|
return entries |
|
|
|
|
|
|
|
|
|
|
|
# Main |
|
|
|
# Main |
|
|
|
|
|
|
|
def confirm(msg, code=0): |
|
|
|
|
|
|
|
print(msg) |
|
|
|
|
|
|
|
reply = input("Continue? [n] ") |
|
|
|
|
|
|
|
if not (res := reply.lower().startswith("y")) and code != 0: |
|
|
|
|
|
|
|
sys.exit(code) |
|
|
|
|
|
|
|
return res |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def glob_compile(pattern): |
|
|
|
|
|
|
|
return re.compile(fnmatch.translate(pattern)) |
|
|
|
|
|
|
|
|
|
|
|
def parse_args(argv): |
|
|
|
def parse_args(argv): |
|
|
|
parser = argparse.ArgumentParser(prog=argv[0], description=""" |
|
|
|
parser = argparse.ArgumentParser(prog=argv[0], description=""" |
|
|
|
Patch a folder structure with files from an archive. |
|
|
|
Patch a folder structure with files from an archive. |
|
|
|
This will replace existing files with those of the same name in 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. |
|
|
|
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, |
|
|
|
parser.add_argument("archive", type=pathlib.Path, |
|
|
|
help="Strip NUM leading components from archived file names.") |
|
|
|
help="Achive file name") |
|
|
|
parser.add_argument("-C", "--directory", type=pathlib.Path, default=pathlib.Path("."), |
|
|
|
parser.add_argument("-C", "--directory", type=pathlib.Path, default=pathlib.Path("."), |
|
|
|
help="Operate in <direcory>") |
|
|
|
help="Operate in <direcory>") |
|
|
|
parser.add_argument("-b", "--backup", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Create backup copies of overwritten files") |
|
|
|
|
|
|
|
parser.add_argument("-B", "--backup-archive", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Create an archive of the original files") |
|
|
|
|
|
|
|
parser.add_argument("-m", "--match", default=None, |
|
|
|
|
|
|
|
help="Only extract files matching GLOB", metavar="GLOB") |
|
|
|
|
|
|
|
parser.add_argument("-u", "--uninstall-script", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Filename to save an uninstall-scipt to.", metavar="FILE") |
|
|
|
|
|
|
|
parser.add_argument("-n", "--dry-run", action="store_true", |
|
|
|
parser.add_argument("-n", "--dry-run", action="store_true", |
|
|
|
help="Perform a dry run") |
|
|
|
help="Perform a dry run") |
|
|
|
parser.add_argument("archive", help="Achive file name") |
|
|
|
parser.add_argument("--noconfirm", action="store_true", |
|
|
|
|
|
|
|
help="Don't ask for confirmation on possibly dangerous operations") |
|
|
|
return parser.parse_args(argv[1:]) |
|
|
|
target = parser.add_argument_group("Target") |
|
|
|
|
|
|
|
target.add_argument("-p", "--strip", type=int, default=0, |
|
|
|
|
|
|
|
help="Strip N leading path components from archived file names.", metavar="N") |
|
|
|
|
|
|
|
target.add_argument("-P", "--prefix", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Prepend leading path components to archived file names.") |
|
|
|
|
|
|
|
fmatch = target.add_mutually_exclusive_group() |
|
|
|
|
|
|
|
fmatch.add_argument("-m", "--match", dest="match", type=glob_compile, default=None, |
|
|
|
|
|
|
|
help="Only extract files matching GLOB", metavar="GLOB") |
|
|
|
|
|
|
|
fmatch.add_argument("-M", "--regex", dest="match", type=re.compile, default=None, |
|
|
|
|
|
|
|
help="Only extract files matching REGEX", metavar="REGEX") |
|
|
|
|
|
|
|
backup = parser.add_argument_group("Backup") |
|
|
|
|
|
|
|
backup.add_argument("-B", "--backup-archive", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Create an archive of the original files", metavar="FILE") |
|
|
|
|
|
|
|
backup.add_argument("-u", "--uninstall-script", type=pathlib.Path, default=None, |
|
|
|
|
|
|
|
help="Filename to save an uninstall-scipt to. Shoul be combined with -B", metavar="FILE") |
|
|
|
|
|
|
|
backup.add_argument("-a", "--append", action="store_true", default=False, |
|
|
|
|
|
|
|
help="Update existing backup files") |
|
|
|
|
|
|
|
preset = parser.add_mutually_exclusive_group() |
|
|
|
|
|
|
|
preset.add_argument("-U", "--uninstall-preset", action="store_const", dest="preset", const="uninstall", |
|
|
|
|
|
|
|
help="Short-hand for '-B %%n.uninstall.tar.xz -u %%n.uninstall.sh', %%n being the archive basename") |
|
|
|
|
|
|
|
preset.add_argument("-A", "--append-preset", action="store_const", dest="preset", const="append", |
|
|
|
|
|
|
|
help="Short-hand for '-a -B patchdir_backup.tar.xz -u patchdir_restore.sh") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args(argv[1:]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if args.preset == "uninstall": |
|
|
|
|
|
|
|
# Ignore when combined with explicit -u and -B; allows general-purpose alias patchdir='patchdir -U' |
|
|
|
|
|
|
|
if args.backup_archive is None: |
|
|
|
|
|
|
|
args.backup_archive = pathlib.Path(f"{args.archive.stem}.uninstall.tar.xz") |
|
|
|
|
|
|
|
if args.uninstall_script is None: |
|
|
|
|
|
|
|
args.uninstall_script = pathlib.Path(f"{args.archive.stem}.uninstall.sh") |
|
|
|
|
|
|
|
elif args.preset == "append": |
|
|
|
|
|
|
|
args.append = True |
|
|
|
|
|
|
|
if args.backup_archive is None: |
|
|
|
|
|
|
|
args.backup_archive = pathlib.Path("patchdir_backup.tar.xz") |
|
|
|
|
|
|
|
if args.uninstall_script is None: |
|
|
|
|
|
|
|
args.uninstall_script = pathlib.Path("patchdir_restore.sh") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not args.noconfirm: |
|
|
|
|
|
|
|
if not args.uninstall_script or not args.backup_archive: |
|
|
|
|
|
|
|
confirm("Original files will be lost without -B and u. Consider using -U.", 1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return args |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Parsing uninstall scripts |
|
|
|
|
|
|
|
SCRIPT_TAG = "#> patchdir restore script v1 <#" |
|
|
|
|
|
|
|
SCRIPT_BACKUP_CMDS = {"restore"} |
|
|
|
|
|
|
|
SCRIPT_REMOVE_CMDS = {"remove"} |
|
|
|
|
|
|
|
SCRIPT_DIR_CMDS = {"remove_dir"} |
|
|
|
|
|
|
|
SCRIPT_META_CMDS = {"cleanup"} |
|
|
|
|
|
|
|
SCRIPT_CMDS = SCRIPT_BACKUP_CMDS|SCRIPT_REMOVE_CMDS|SCRIPT_DIR_CMDS|SCRIPT_META_CMDS |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_script(path): |
|
|
|
|
|
|
|
# Relies on ordered dictionary implementation |
|
|
|
|
|
|
|
is_script = False |
|
|
|
|
|
|
|
operations = {} |
|
|
|
|
|
|
|
with open(path) as f: |
|
|
|
|
|
|
|
for ln, line in enumerate(f): |
|
|
|
|
|
|
|
if line.strip() == SCRIPT_TAG: |
|
|
|
|
|
|
|
is_script = True |
|
|
|
|
|
|
|
elif is_script: |
|
|
|
|
|
|
|
cmd = shlex.split(line) |
|
|
|
|
|
|
|
if len(cmd) != 2 or cmd[0] not in SCRIPT_CMDS: |
|
|
|
|
|
|
|
raise ValueError(f"Failed to read invalid line {ln} in script {path}", line) |
|
|
|
|
|
|
|
if cmd not in SCRIPT_META_CMDS: |
|
|
|
|
|
|
|
operations[pathlib.Path(cmd[1])] = cmd[0] |
|
|
|
|
|
|
|
if not is_script: |
|
|
|
|
|
|
|
raise ValueError("Script for appending {path} doesn't seem to be a pathdir script") |
|
|
|
|
|
|
|
return operations |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_script(fp, cmd, path): |
|
|
|
|
|
|
|
fp.write(f"{cmd:10s} {shlex.quote(str(path))}\n") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Helpers |
|
|
|
def makedirs(path, dryrun=False): |
|
|
|
def makedirs(path, dryrun=False): |
|
|
|
if path.is_dir(): |
|
|
|
if path.is_dir(): |
|
|
|
return set() |
|
|
|
return set() |
|
|
@ -156,27 +271,51 @@ def makedirs(path, dryrun=False): |
|
|
|
|
|
|
|
|
|
|
|
return created |
|
|
|
return created |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv): |
|
|
|
def main(argv): |
|
|
|
args = parse_args(argv) |
|
|
|
args = parse_args(argv) |
|
|
|
|
|
|
|
|
|
|
|
with contextlib.ExitStack() as ctx: |
|
|
|
with contextlib.ExitStack() as ctx: |
|
|
|
# archive name is the only argument not affected by -C |
|
|
|
# archive name is the only argument not affected by -C |
|
|
|
archive = ctx.enter_context(libarchive.file_reader(args.archive)) |
|
|
|
archive = ctx.enter_context(libarchive.file_reader(str(args.archive))) |
|
|
|
|
|
|
|
|
|
|
|
# Change directory to target |
|
|
|
# Change directory to target |
|
|
|
os.chdir(args.directory) |
|
|
|
os.chdir(args.directory) |
|
|
|
|
|
|
|
|
|
|
|
if args.backup_archive and os.path.exists(args.backup_archive): |
|
|
|
do_append = False |
|
|
|
|
|
|
|
if args.append: |
|
|
|
|
|
|
|
# Check consistency for append |
|
|
|
|
|
|
|
checks = [p.exists() |
|
|
|
|
|
|
|
for p in (args.backup_archive, args.uninstall_script) |
|
|
|
|
|
|
|
if p is not None] |
|
|
|
|
|
|
|
do_append = any(check) |
|
|
|
|
|
|
|
if do_append and not all(check): |
|
|
|
|
|
|
|
raise FileExistsError("Inconsistency in existing files to append to.") |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
if args.backup_archive and args.backup_archive.exists(): |
|
|
|
|
|
|
|
if args.noconfirm: |
|
|
|
print("\033[31mError: Backup archive file already exist\033[0m") |
|
|
|
print("\033[31mError: Backup archive file already exist\033[0m") |
|
|
|
return 3 |
|
|
|
return 3 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
confirm("Backup archive already exists.", 3) |
|
|
|
|
|
|
|
|
|
|
|
uninstall_script = None |
|
|
|
uninstall_script = None |
|
|
|
|
|
|
|
operations = {} |
|
|
|
folders = set() |
|
|
|
folders = set() |
|
|
|
files = set() |
|
|
|
files = set() |
|
|
|
if args.uninstall_script: |
|
|
|
if args.uninstall_script: |
|
|
|
uninstall_script = ctx.enter_context(open(args.uninstall_script, "x")) |
|
|
|
if do_append: |
|
|
|
|
|
|
|
operations = read_script(args.uninstall_script) |
|
|
|
|
|
|
|
elif args.uninstall_script.exists(): |
|
|
|
|
|
|
|
if args.noconfirm: |
|
|
|
|
|
|
|
print("\033[31mError: Uninstall script file already exist\033[0m") |
|
|
|
|
|
|
|
return 3 |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
confirm("Uninstall script already exists. Overwrite?", 3) |
|
|
|
|
|
|
|
if not args.dry_run: |
|
|
|
|
|
|
|
uninstall_script = ctx.enter_context(open(args.uninstall_script, "w")) |
|
|
|
os.chmod(uninstall_script.fileno(), 0o755) |
|
|
|
os.chmod(uninstall_script.fileno(), 0o755) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
uninstall_script = ctx.enter_context(open("/dev/null", "w")) |
|
|
|
uninstall_script_dir = args.uninstall_script.parent.resolve() |
|
|
|
uninstall_script_dir = args.uninstall_script.parent.resolve() |
|
|
|
|
|
|
|
|
|
|
|
uninstall_script.write(textwrap.dedent(f"""\ |
|
|
|
uninstall_script.write(textwrap.dedent(f"""\ |
|
|
@ -186,7 +325,6 @@ def main(argv): |
|
|
|
|
|
|
|
|
|
|
|
cd "`dirname "$0"`/{pathlib.Path.cwd().relative_to(uninstall_script_dir)}" |
|
|
|
cd "`dirname "$0"`/{pathlib.Path.cwd().relative_to(uninstall_script_dir)}" |
|
|
|
|
|
|
|
|
|
|
|
BACKUP_DIR={f"'{args.backup}'" if args.backup else ' # No backup directory was created (-b)'} |
|
|
|
|
|
|
|
BACKUP_ARCHIVE={f"'{args.backup_archive}'" if args.backup_archive else ' # No backup archive was created (-B)'} |
|
|
|
BACKUP_ARCHIVE={f"'{args.backup_archive}'" if args.backup_archive else ' # No backup archive was created (-B)'} |
|
|
|
|
|
|
|
|
|
|
|
remove() {{ |
|
|
|
remove() {{ |
|
|
@ -196,12 +334,7 @@ def main(argv): |
|
|
|
remove_dir() {{ |
|
|
|
remove_dir() {{ |
|
|
|
rmdir "$1" || echo "\\033[34mFailed to remove folder $1\\033[0m" |
|
|
|
rmdir "$1" || echo "\\033[34mFailed to remove folder $1\\033[0m" |
|
|
|
}} |
|
|
|
}} |
|
|
|
if [ -n "$BACKUP_DIR" -a -d "$BACKUP_DIR" ]; then |
|
|
|
if [ -n "$BACKUP_ARCHIVE" -a -f "$BACKUP_ARCHIVE" ]; then |
|
|
|
restore() {{ |
|
|
|
|
|
|
|
echo "Restoring $1 from $BACKUP_DIR" |
|
|
|
|
|
|
|
mv "$BACKUP_DIR/$1" "$1" |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
elif [ -n "$BACKUP_ARCHIVE" -a -f "$BACKUP_ARCHIVE" ]; then |
|
|
|
|
|
|
|
restore() {{ |
|
|
|
restore() {{ |
|
|
|
echo "Restoring $1 from $BACKUP_ARCHIVE" |
|
|
|
echo "Restoring $1 from $BACKUP_ARCHIVE" |
|
|
|
bsdtar -Uqxf "$BACKUP_ARCHIVE" "$1" |
|
|
|
bsdtar -Uqxf "$BACKUP_ARCHIVE" "$1" |
|
|
@ -211,36 +344,84 @@ def main(argv): |
|
|
|
echo "Removing $1 \\033[31m[Previously existing file is lost]\\033[0m" |
|
|
|
echo "Removing $1 \\033[31m[Previously existing file is lost]\\033[0m" |
|
|
|
rm "$1" |
|
|
|
rm "$1" |
|
|
|
}} |
|
|
|
}} |
|
|
|
elif ! grep -qE "restore\\s+[\\"'].+[\\"']" "$0"; then |
|
|
|
elif ! grep -qE "restore\\s+([\\"'].+[\\"']|(?\\!script)\\S+\\s*(#.*)?\\$)" "$0"; then |
|
|
|
: No files were overwritten |
|
|
|
: No files were overwritten |
|
|
|
else |
|
|
|
else |
|
|
|
echo "\\033[31mError: Cannot restore original files because no backup is available\\033[0m" |
|
|
|
echo "\\033[31mError: Cannot restore original files because no backup is available\\033[0m" |
|
|
|
echo "Set PATCHDIR_LOSE_FILES=1 to remove changed files without restoring originals" |
|
|
|
echo "Set PATCHDIR_LOSE_FILES=1 to remove changed files without restoring originals" |
|
|
|
exit 66 |
|
|
|
exit 66 |
|
|
|
fi |
|
|
|
fi |
|
|
|
|
|
|
|
if [ "$1" = "-k" -o -n "$PATCHDIR_KEEP_BACKUP" ]; |
|
|
|
|
|
|
|
cleanup() {{ |
|
|
|
|
|
|
|
echo "Keeping $1" |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
else |
|
|
|
|
|
|
|
cleanup() {{ |
|
|
|
|
|
|
|
echo "Cleaning up $1" |
|
|
|
|
|
|
|
rm "$1" |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{SCRIPT_TAG} |
|
|
|
# Restore Files # |
|
|
|
# Restore Files # |
|
|
|
""")) |
|
|
|
""")) |
|
|
|
|
|
|
|
|
|
|
|
backup_dir = None |
|
|
|
if operations: |
|
|
|
backup_folders = set() |
|
|
|
for cmd, path in operations: |
|
|
|
if args.backup: |
|
|
|
if cmd not in SCRIPT_DIR_CMDS: |
|
|
|
backup_dir = args.backup |
|
|
|
write_script(uninstall_script, cmd, path) |
|
|
|
|
|
|
|
|
|
|
|
backup_archive = None |
|
|
|
backup_archive = None |
|
|
|
|
|
|
|
backup_archive_prepended = set() |
|
|
|
if args.backup_archive: |
|
|
|
if args.backup_archive: |
|
|
|
|
|
|
|
if do_append: |
|
|
|
|
|
|
|
# Prepend old contents |
|
|
|
|
|
|
|
backup_archive_tmpname = args.backup_archive.with_name(args.backup_archive.name + ".tmp") |
|
|
|
|
|
|
|
if not args.dry_run: |
|
|
|
|
|
|
|
backup_archive = ctx.enter_context(file_writer_ext(backup_archive_tmpname, args.backup_archive)) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
backup_archive = ctx.enter_context(file_writer_ext("/dev/null", args.backup_archive)) |
|
|
|
|
|
|
|
with libarchive.file_reader(str(args.backup_archive)) as ar: |
|
|
|
|
|
|
|
backup_archive_prepended.update(copy_all_entries(ar, backup_archive)) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
if not args.dry_run: |
|
|
|
backup_archive = ctx.enter_context(file_writer_ext(args.backup_archive)) |
|
|
|
backup_archive = ctx.enter_context(file_writer_ext(args.backup_archive)) |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
backup_archive = ctx.enter_context(file_writer_ext("/dev/null", args.backup_archive)) |
|
|
|
disk_rdr = ctx.enter_context(disk_reader()) |
|
|
|
disk_rdr = ctx.enter_context(disk_reader()) |
|
|
|
|
|
|
|
|
|
|
|
for entry in archive: |
|
|
|
# Check consistency |
|
|
|
epath = pathlib.Path(entry.path) |
|
|
|
prev_restore_files = set() |
|
|
|
|
|
|
|
prev_remove_files = set() |
|
|
|
|
|
|
|
for op, path in operations: |
|
|
|
|
|
|
|
if op in SCRIPT_BACKUP_CMDS: |
|
|
|
|
|
|
|
if backup_archive and path not in backup_archive_prepended: |
|
|
|
|
|
|
|
print("\033[34mWarn: {path} referenced in uninstall script but missing in backup archive\033[0m") |
|
|
|
|
|
|
|
prev_restore_files.add(path) |
|
|
|
|
|
|
|
elif op in SCRIPT_REMOVE_CMDS: |
|
|
|
|
|
|
|
prev_remove_files.add(path) |
|
|
|
|
|
|
|
elif op == "remove_dir": |
|
|
|
|
|
|
|
folders.add(path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Open disk for writing |
|
|
|
|
|
|
|
if not args.dry_run: |
|
|
|
|
|
|
|
extract = ctx.enter_context(libarchive.extract.new_archive_write_disk( |
|
|
|
|
|
|
|
libarchive.extract.EXTRACT_TIME|libarchive.extract.EXTRACT_UNLINK)) |
|
|
|
|
|
|
|
|
|
|
|
if args.match and not epath.match(args.match): |
|
|
|
root = pathlib.PurePath('/') |
|
|
|
|
|
|
|
for entry in archive: |
|
|
|
|
|
|
|
if args.match: |
|
|
|
|
|
|
|
if not args.match.match('/'+entry.path): |
|
|
|
continue |
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
epath = pathlib.Path(entry.path) |
|
|
|
|
|
|
|
|
|
|
|
if args.strip: |
|
|
|
if args.strip: |
|
|
|
epath = pathlib.Path(*epath.parts[args.strip:]) |
|
|
|
epath = pathlib.Path(*epath.parts[args.strip:]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if args.prefix: |
|
|
|
|
|
|
|
epath = args.prefix / epath |
|
|
|
|
|
|
|
|
|
|
|
if entry.isdir: |
|
|
|
if entry.isdir: |
|
|
|
folders |= makedirs(epath, args.dry_run) |
|
|
|
folders |= makedirs(epath, args.dry_run) |
|
|
|
else: |
|
|
|
else: |
|
|
@ -254,37 +435,33 @@ def main(argv): |
|
|
|
# Backup |
|
|
|
# Backup |
|
|
|
if backup_archive: |
|
|
|
if backup_archive: |
|
|
|
print(f"Backing up {epath} to {args.backup_archive}") |
|
|
|
print(f"Backing up {epath} to {args.backup_archive}") |
|
|
|
|
|
|
|
# Skip if an older version in archive or it originally didn't exist |
|
|
|
|
|
|
|
if epath not in backup_archive_prepended and epath not in prev_remove_files: |
|
|
|
disk_rdr.add_file_to_archive(backup_archive, epath) |
|
|
|
disk_rdr.add_file_to_archive(backup_archive, epath) |
|
|
|
if backup_dir: |
|
|
|
if uninstall_script and epath not in operations: |
|
|
|
print(f"Moving old {epath} to {args.backup}") |
|
|
|
write_script(uninstall_script, "restore", epath) |
|
|
|
bpath = args.backup.joinpath(epath) |
|
|
|
|
|
|
|
backup_folders |= makedirs(bpath.parent, args.dry_run) |
|
|
|
|
|
|
|
if not args.dry_run: |
|
|
|
|
|
|
|
os.rename(epath, bpath) |
|
|
|
|
|
|
|
if uninstall_script: |
|
|
|
|
|
|
|
uninstall_script.write(f"restore '{epath}'\n") |
|
|
|
|
|
|
|
elif uninstall_script: |
|
|
|
elif uninstall_script: |
|
|
|
uninstall_script.write(f"remove '{epath}'\n") |
|
|
|
write_script(uninstall_script, "remove", epath) |
|
|
|
|
|
|
|
|
|
|
|
print(f"Extracting {epath}") |
|
|
|
print(f"Extracting {epath}") |
|
|
|
if not args.dry_run: |
|
|
|
if not args.dry_run: |
|
|
|
extract_to_disk(entry, epath) |
|
|
|
extract_entry(entry, extract, epath) |
|
|
|
|
|
|
|
|
|
|
|
if uninstall_script and (folders or backup_folders): |
|
|
|
if do_append and backup_archive and not args.dry_run: |
|
|
|
uninstall_script.write("\n# Remove folders #\n") |
|
|
|
os.rename(backup_archive_tmpname, args.backup_archive) |
|
|
|
|
|
|
|
|
|
|
|
if backup_folders: |
|
|
|
if uninstall_script and folders: |
|
|
|
uninstall_script.write('if [ -n "$BACKUP_DIR" -a -d "$BACKUP_DIR" ]; then\n') |
|
|
|
uninstall_script.write("\n# Remove folders #\n") |
|
|
|
for dname in sorted(backup_folders, key=lambda x: len(x.parts), reverse=True): |
|
|
|
|
|
|
|
uninstall_script.write(f"\tremove_dir '{dname}'\n") |
|
|
|
|
|
|
|
uninstall_script.write('fi\n') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if folders: |
|
|
|
if folders: |
|
|
|
for dname in sorted(folders, key=lambda x: len(x.parts), reverse=True): |
|
|
|
for dname in sorted(folders, key=lambda x: len(x.parts), reverse=True): |
|
|
|
uninstall_script.write(f"remove_dir '{dname}'\n") |
|
|
|
write_script(uninstall_script, "remove_dir", dname) |
|
|
|
|
|
|
|
|
|
|
|
if uninstall_script: |
|
|
|
if uninstall_script: |
|
|
|
uninstall_script.write(f"\n# Remove script #\nremove '{args.uninstall_script}'\n") |
|
|
|
uninstall_script.write(f"\n# Remove script #\n") |
|
|
|
|
|
|
|
write_script(uninstall_script, "cleanup", args.uninstall_script) |
|
|
|
|
|
|
|
if args.backup_archive: |
|
|
|
|
|
|
|
write_script(uninstall_script, "cleanup", args.backup_archive) |
|
|
|
|
|
|
|
|
|
|
|
return 0 |
|
|
|
return 0 |
|
|
|
|
|
|
|
|
|
|
|