#!/usr/bin/python3

import os
import sys
import argparse
import shutil
import subprocess
from pathlib import PosixPath, PurePath

def get_parser():
    parser = argparse.ArgumentParser(
        prog="bak | unbak",
        description="""
    A simple utility to append .bak to files (either by renaming or copying)
    and then restore them (again, either renaming or copying).
    """,
        epilog="""
    Note: when overwriting existing folders, the destination is first removed,
    then the source copied or moved.
    File metadata should be preserved in all cases.
    """
    )
    parser.add_argument("-f", "--force", action="store_true",
                        help="Overwrite (bak) or restore (unbak) a file if the target exists.")
    parser.add_argument("-c", "--copy", action="store_true",
                        help="When backing up, leave the original. When restoring, leave the backup.")
    parser.add_argument(metavar='pattern', type=str, nargs='+', dest='files',
                        help='Filenames or patterns to backup or restore')
    group = parser.add_argument_group("miscellaneous")
    group.add_argument("-d", "--debug", action="store_true",
                       help="Show info about what's going on.")
    group.add_argument("-t", "--test", action="store_true",
                       help=argparse.SUPPRESS)
    group.add_argument("-v", "--version", action="store_true",
                       help="Print the program version.")
    return parser

class Bakunbak():
    def run(self):
        args = get_parser().parse_args()

        def debug(msg):
            if args.debug:
                print("debug: %s" % msg)

        mode = "unbak" if sys.argv[0].endswith("unbak") else "bak"

        debug("mode is %s, copying: %s, force: %s" % (mode, args.copy, args.force))

        # for tests
        testing = args.test

        if mode == "bak":
            for filename in args.files:
                path = PurePath(filename)

                bakked_file = PosixPath(path.with_suffix(path.suffix + ".bak"))
                source_file = PosixPath(path)

                if not source_file.exists():
                    if testing:
                        print("BAK_SOURCE_MISSING")
                        exit(0)
                    else:
                        print("%s does not exist, skipping" % (source_file,))
                        continue

                if source_file.suffix == ".bak":
                    if testing:
                        print("BAK_IS_A_BAK_ALREADY")
                        exit(0)
                    print("%s is already a backup, ignoring" % source_file)
                    continue

                if bakked_file.exists():
                    if not args.force:
                        if testing:
                            print("BAK_TARGET_EXISTS")
                            exit(0)
                        print("%s exists, skipping (use --force to copy anyway)." % bakked_file)
                        continue
                    else:
                        debug("%s exists, but continuing anyway because of --force" % bakked_file)

                if not self.can_modify(source_file) or not self.can_modify(bakked_file):
                    print("Skipping %s - insufficient permissions. Try again as root." % path)
                    continue

                if args.copy:
                    debug("copying %s to %s" % (source_file, bakked_file))
                    if source_file.is_dir():
                        shutil.rmtree(bakked_file, ignore_errors=True)
                        # FIXME: copytree won't copy dangling symlinks ?? even with symlinks=True
                        subprocess.run(["cp", "-rP", source_file, bakked_file])
                        # shutil.copytree(filename, bakked_filename, dirs_exist_ok=True)
                    else:
                        shutil.copy2(source_file, bakked_file)
                else:
                    debug("moving %s to %s" % (source_file, bakked_file))
                    shutil.rmtree(bakked_file, ignore_errors=True)
                    # move supports path-like objects starting in 3.9
                    shutil.move(source_file.as_posix(), bakked_file.as_posix())

                print("bak done")
        else:
            for filename in args.files:
                path = PurePath(filename)

                if path.suffix == ".bak":
                    unbakked_file = PosixPath(path.with_suffix(""))
                    source_file = PosixPath(path)
                else:
                    unbakked_file = PosixPath(path)
                    source_file = PosixPath(path.with_suffix(path.suffix + ".bak"))

                if not os.path.exists(source_file):
                    if os.path.exists(unbakked_file):
                        if testing:
                            print("UNBAK_FILE_NOT_BAKKED")
                            exit(0)
                        print("%s is not bakked, skipping" % (source_file,))
                    else:
                        if testing:
                            print("UNBAK_NO_FILES_FOUND")
                            exit(0)
                        print("%s does not exist in any form, skipping" % (unbakked_file,))
                    continue

                debug("file: %s ---> %s" % (source_file, unbakked_file))

                if unbakked_file.exists():
                    if not args.force:
                        if testing:
                            print("UNBAK_TARGET_EXISTS")
                            exit(0)
                        print("%s exists (probably from using -c originally) - skipping (use --force to copy anyway)." % unbakked_file)
                        continue
                    else:
                        debug("%s exists (probably from using -c originally), but continuing anyway because of --force" % unbakked_file)

                if not self.can_modify(source_file) or not self.can_modify(unbakked_file):
                    print("Skipping %s - insufficient permissions. Try again as root." % path)
                    continue

                if args.copy:
                    debug("copying %s to %s" % (source_file, unbakked_file))
                    if source_file.is_dir():
                        shutil.rmtree(unbakked_file, ignore_errors=True)
                        # FIXME: copytree won't copy dangling symlinks ?? even with symlinks=True
                        subprocess.run(["cp", "-rP", source_file, unbakked_file])
                        # shutil.copytree(source_file, unbakked_file, dirs_exist_ok=True)
                    else:
                        shutil.copy2(source_file, unbakked_file)
                else:
                    debug("moving %s to %s" % (source_file, unbakked_file))
                    shutil.rmtree(unbakked_file, ignore_errors=True)
                    # move supports path-like objects starting in 3.9
                    shutil.move(source_file.as_posix(), unbakked_file.as_posix())

                print("unbak done")

    def can_modify(self, path):
        # Ignore if a file doesn't exist, by the time this is called, we've already tested
        # for missing/existing files.
        if not os.access(path.parent, os.R_OK | os.W_OK):
            return False

        if not path.exists():
            return True

        return os.access(path, os.R_OK | os.W_OK)


if __name__ == '__main__':
    Bakunbak().run()
    exit(0)
