Example #1
0
def smart_copy(src, dest):
    """ Copy (overwrite) file only if the contents have changed. """
    ret = SMART_CREATE
    if os.path.isfile(dest):
        if file_compare(src, dest):
            # Files already match.  Nothing to do.
            return SMART_NOCHANGE
        else:
            ret = SMART_UPDATE
            os.unlink(dest)
    shutil.copy2(src, dest)
    return ret
Example #2
0
    def run(self, args):
        # Ignores case sensitivity.  If you're on Windows, name your files right.
        conf_file_re = re.compile("([a-z]+\.conf|(default|local)\.meta)$")

        if args.target is None:
            self.stderr.write("Must provide the '--target' directory.\n")
            return EXIT_CODE_MISSING_ARG

            self.stderr.write("Combining conf files into {}\n".format(args.target))
        args.source = list(_expand_glob_list(args.source))
        for src in args.source:
            self.stderr.write("Reading conf files from {}\n".format(src))

        marker_file = os.path.join(args.target, CONTROLLED_DIR_MARKER)
        if os.path.isdir(args.target):
            if not os.path.isfile(os.path.join(args.target, CONTROLLED_DIR_MARKER)):
                self.stderr.write("Target directory already exists, but it appears to have been"
                                  "created by some other means.  Marker file missing.\n")
                return EXIT_CODE_COMBINE_MARKER_MISSING
        elif args.dry_run:
            self.stderr.write(
                "Skipping creating destination folder {0} (dry-run)\n".format(args.target))
        else:
            self.stderr.write("Creating destination folder {0}\n".format(args.target))
            os.mkdir(args.target)
            open(marker_file, "w").write("This directory is managed by KSCONF.  Don't touch\n")

        # Build a common tree of all src files.
        src_file_index = defaultdict(list)
        for src_root in args.source:
            for (root, dirs, files) in relwalk(src_root):
                for fn in files:
                    # Todo: Add blacklist CLI support:  defaults to consider: *sw[po], .git*, .bak, .~
                    if fn.endswith(".swp") or fn.endswith("*.bak"):
                        continue  # pragma: no cover  (peephole optimization)
                    src_file = os.path.join(root, fn)
                    src_path = os.path.join(src_root, root, fn)
                    src_file_index[src_file].append(src_path)

        # Find a set of files that exist in the target folder, but in NO source folder (for cleanup)
        target_extra_files = set()
        for (root, dirs, files) in relwalk(args.target):
            for fn in files:
                tgt_file = os.path.join(root, fn)
                if tgt_file not in src_file_index:
                    # Todo:  Add support for additional blacklist wildcards (using fnmatch)
                    if fn == CONTROLLED_DIR_MARKER or fn.endswith(".bak"):
                        continue  # pragma: no cover (peephole optimization)
                    target_extra_files.add(tgt_file)

        for (dest_fn, src_files) in sorted(src_file_index.items()):
            dest_path = os.path.join(args.target, dest_fn)

            # Make missing destination folder, if missing
            dest_dir = os.path.dirname(dest_path)
            if not os.path.isdir(dest_dir) and not args.dry_run:
                os.makedirs(dest_dir)

            # Handle conf files and non-conf files separately
            if not conf_file_re.search(dest_fn):
                # self.stderr.write("Considering {0:50}  NON-CONF Copy from source:  {1!r}\n".format(dest_fn, src_files[-1]))
                # Always use the last file in the list (since last directory always wins)
                src_file = src_files[-1]
                if args.dry_run:
                    if os.path.isfile(dest_path):
                        if file_compare(src_file, dest_path):
                            smart_rc = SMART_NOCHANGE
                        else:
                            if (_is_binary_file(src_file) or _is_binary_file(dest_path)):
                                # Binary files.  Can't compare...
                                smart_rc = "DRY-RUN (NO-DIFF=BIN)"
                            else:
                                show_text_diff(self.stdout, dest_path, src_file)
                                smart_rc = "DRY-RUN (DIFF)"
                    else:
                        smart_rc = "DRY-RUN (NEW)"
                else:
                    smart_rc = smart_copy(src_file, dest_path)
                if smart_rc != SMART_NOCHANGE:
                    self.stderr.write(
                        "Copy <{0}>   {1:50}  from {2}\n".format(smart_rc, dest_path, src_file))
            else:
                # Handle merging conf files
                dest = ConfFileProxy(os.path.join(args.target, dest_fn), "r+",
                                     parse_profile=PARSECONF_MID)
                srcs = [ConfFileProxy(sf, "r", parse_profile=PARSECONF_STRICT) for sf in src_files]
                # self.stderr.write("Considering {0:50}  CONF MERGE from source:  {1!r}\n".format(dest_fn, src_files[0]))
                smart_rc = merge_conf_files(dest, srcs, dry_run=args.dry_run,
                                            banner_comment=args.banner)
                if smart_rc != SMART_NOCHANGE:
                    self.stderr.write(
                        "Merge <{0}>   {1:50}  from {2!r}\n".format(smart_rc, dest_path,
                                                                    src_files))

        if True and target_extra_files:  # Todo: Allow for cleanup to be disabled via CLI
            self.stderr.write("Cleaning up extra files not part of source tree(s):  {0} files.\n".
                format(len(target_extra_files)))
            for dest_fn in target_extra_files:
                self.stderr.write("Remove unwanted file {0}\n".format(dest_fn))
                os.unlink(os.path.join(args.target, dest_fn))