def extlink(self, path: RelPath, **kwargs: Options) -> None: """Link any file specified by its absolute path""" read_opt = self.__make_read_opt(kwargs) path = expanduser(expandvars(path)) if not os.path.isabs(path): log_warning("'path' should be specified as an absolut path" + " for extlink(). Relative paths are not forbidden" + " but can cause undesired side-effects.") if not read_opt("optional") or os.path.exists(path): self.__create_link_descriptor(os.path.abspath(path), **kwargs)
def inspect_file(self, target: Path) -> None: """Checks if file is dynamic and has changed. """ # Calculate new hash and get old has of file md5_calc = hashlib.md5(open(target, "rb").read()).hexdigest() md5_old = os.path.basename(target)[-32:] # Check for changes if is_dynamic_file(target) and md5_calc != md5_old: log_warning(f"You made changes to '{target}'. Those changes " + "will be lost, if you don't write them back to " + "the original file.") self.user_interaction(target)
def print_installed_profiles(self) -> None: """Shows only the profiles specified. If none are specified shows all.""" if self.args.profiles: for profilename in self.args.profiles: if profilename in self.installed: self.print_installed(self.installed[profilename]) else: log_warning("\nThe profile '" + profilename + "' is not installed. Skipping...\n") else: for key in self.installed.keys(): if key[0] != "@": self.print_installed(self.installed[key])
def dryrun(self, difflog: DiffLog) -> None: """Runs Checks and pretty prints the DiffLog""" log_warning("This is just a dry-run! Nothing of this " + "is actually happening.") difflog.run_interpreter( CheckProfilesI(self.installed, self.args.parent)) tests = [ CheckLinksI(self.installed), CheckLinkBlacklistI(self.args.superforce), CheckLinkDirsI(self.args.makedirs), CheckLinkExistsI(self.args.force), CheckDynamicFilesI(True) ] difflog.run_interpreter(*tests) difflog.run_interpreter(RootNeededI()) difflog.run_interpreter(PrintI())
def user_interaction(self, target: Path) -> None: """Gives the user the ability to interact with a changed file""" target_bak = target + "." + constants.BACKUP_EXTENSION done = False while not done: inp = input("[A]bort / [I]gnore / Show [D]iff " + "/ Create [P]atch / [U]ndo changes: ") if inp == "A": raise UserAbortion elif inp == "I": done = True elif inp == "D": # Create a colored diff between the file and its original process = Popen(["diff", "--color=auto", target_bak, target]) process.communicate() elif inp == "P": # Create a git patch with git diff patch_file = os.path.join(constants.TARGET_FILES, os.path.basename(target)) patch_file += ".patch" patch_file = input("Enter filename for patch [" + patch_file + "]: ") or patch_file args = ["git", "diff", "--no-index", target_bak, target] process = Popen(args, stdout=PIPE) try: with open(patch_file, "wb") as file: file.write(process.stdout.read()) print("Patch file written successfully") except IOError: msg = f"Could not write patch file '{patch_file}'." raise PreconditionError(msg) elif inp == "U": if self.dryrun: print("This does nothing this time since " + "this is just a dry-run") else: # Copy the original to the changed copyfile(target_bak, target) done = True else: log_warning("Invalid option")
def check_blacklist(self, symlink_name: Path, action: str) -> None: """Checks if the symlink matches on a pattern in the blacklist""" for entry in self.blacklist: if re.search(entry, symlink_name): log_warning(f"You are trying to {action} '" + symlink_name + "' which is blacklisted. It is considered " + f"dangerous to {action} those files!") if self.superforce: log_warning(f"Are you sure that you want to {action} " + "a blacklisted file?") confirmation = input("Type \"YES\" to confirm or " + "anything else to cancel: ") if confirmation != "YES": raise UserError("Canceled by user") else: log_warning("If you really want to modify this file" + " you can use the --superforce flag to" + " ignore the blacklist.") raise IntegrityError(f"Won't {action} blacklisted file!")
def _root_needed(self, operation: str, filename: Path) -> None: self.root_needed = True if (operation, filename) not in self.logged: log_warning("You will need to give me root permission to " + operation + " '" + filename + "'.") self.logged.append((operation, filename))
def __create_link_descriptor(self, target: Path, directory: RelPath = "", **kwargs: Options) -> None: """Creates a link entry for current options and a given target. Also lets you set the dir like cd or options temporarily only for a link""" read_opt = self.__make_read_opt(kwargs) # Now generate the correct name for the symlink replace = read_opt("replace") if replace: # When using regex pattern, name property is ignored if read_opt("name") != "": log_warning("'name'-property is useless if 'replace' is used") replace_pattern = read_opt("replace_pattern") if replace_pattern: base = os.path.basename(target) if "%" in base: base = base.split("%", 1)[1] name = re.sub(replace_pattern, replace, base) else: msg = "You are trying to use 'replace', but no " msg += "'replace_pattern' was set." self.__raise_generation_error(msg) else: name = expandvars(read_opt("name")) # And prevent exceptions in os.symlink() if name and name[-1:] == "/": self.__raise_generation_error("name mustn't represent a directory") # Put together the path of the dir we create the link in if not directory: directory = self.directory # Use the current dir else: directory = expandvars(directory) if directory[0] != '/': # Path is realtive, join with current directory = os.path.join(self.directory, directory) directory = expandvars(directory) # Concat directory and name. The users $HOME needs to be set for this # when executing as root, otherwise ~ will be expanded to the home # directory of the root user (/root) name = expanduser(os.path.join(directory, name)) # Add prefix an suffix to name base, ext = os.path.splitext(os.path.basename(name)) if not base: # If base is empty it means that "name" was never set by the user, # so we fallback to use the target name (but without the tag) base, ext = os.path.splitext( os.path.basename(target.split("%", 1)[-1])) name = os.path.join( os.path.dirname(name), read_opt("prefix") + base + read_opt("suffix") + ext) name = os.path.normpath(name) # Get user and group id of owner owner = read_opt("owner") if owner: # Check for the correct format of owner try: user, group = owner.split(":") except ValueError: msg = "The owner needs to be specified in the format" self.__raise_generation_error(msg + 'user:group') try: uid = shutil._get_uid(user) except LookupError: msg = "You want to set the owner of '" + name + "' to '" + user msg += "', but there is no such user on this system." self.__raise_generation_error(msg) try: gid = shutil._get_gid(group) except LookupError: msg = "You want to set the owner of '" + name + "' to '" msg += group + "', but there is no such group on this system." self.__raise_generation_error(msg) else: # if no owner was specified, we need to set it # to the owner of the dir uid, gid = get_dir_owner(name) # Finally create the result entry linkdescriptor = {} linkdescriptor["target"] = target linkdescriptor["name"] = name linkdescriptor["uid"] = uid linkdescriptor["gid"] = gid linkdescriptor["permission"] = read_opt("permission") self.result["links"].append(linkdescriptor)
dotm.load_installed() dotm.execute_arguments() except CustomError as err: # An error occured that we (more or less) expected. # Print error, a stacktrace and exit logger.debug(traceback.format_exc()) if isinstance(err, FatalError): logger.critical(err.message) else: logger.error(err.message) sys.exit(err.exitcode) except Exception: # This works because all critical parts will catch also all # exceptions and convert them into a CustomError logger.info(traceback.format_exc()) log_warning("The error above was unexpected. But it's fine," + " I haven't done anything yet :)") sys.exit(100) finally: # Write installed back to json file try: with open(constants.INSTALLED_FILE, "w") as file: file.write(json.dumps(dotm.installed, indent=4)) file.flush() os.chown(constants.INSTALLED_FILE, get_uid(), get_gid()) except Exception as err: unkw = UnkownError( err, "An unkown error occured when trying to " + "write all changes back to the installed-file") logger.error(unkw.message)