def symlinks_equal(sym1, sym2): return normpath(sym1["name"]) == normpath(sym2["name"]) and \ normpath(sym1["target"]) == normpath(sym2["target"]) and \ sym1["uid"] == sym2["uid"] and \ sym1["gid"] == sym2["gid"] and \ sym1["permission"] == sym2["permission"] and \ sym1["secure"] == sym2["secure"]
def add_source(self, target): """Adds a source path and normalizes it. Args: target (str): A path to a file that will be used as source """ self.sources.append(normpath(target))
def __setattr__(self, name, value): """Setter for :attr:`self.directory<Profile.directory>`. Makes sure that :attr:`self.directory<Profile.directory>` is always expanded and noramlized. """ if name == "directory": if hasattr(self, name): value = os.path.join(self.directory, expandpath(value)) else: value = normpath(value) super(Profile, self).__setattr__(name, value)
def getdir(self): """Gets the path of the directory that is used to store the generated file. Returns: str: The path to the directory """ path = normpath(os.path.join(constants.DATA_DIR, self.SUBDIR)) # Create dir if it doesn't exist if not os.path.isdir(path): log_debug("Creating directory '" + path + "'") os.mkdir(path) return path
def symlinks_similar(sym1, sym2): return normpath(sym1["name"]) == normpath(sym2["name"]) or \ normpath(sym1["target"]) == normpath(sym2["target"])
def __create_link_descriptor(self, target, directory="", **kwargs): """Creates an entry in ``self.result["links"]`` with current options and a given target. Furthermore lets you set the directory like :func:`cd()`. Args: target (str): Full path to target file directory (str): A path to change the cwd kwargs (dict): A set of options that will be overwritten just for this call Raises: :class:`~errors.GenerationError`: One or more options were misused """ read_opt = self._make_read_opt(kwargs) # First generate the correct name for the symlink replace = read_opt("replace") name = read_opt("name") if replace: # When using regex pattern, name property is ignored replace_pattern = read_opt("replace_pattern") if not replace_pattern: msg = "You are trying to use 'replace', but no " msg += "'replace_pattern' was set." self._gen_err(msg) if name: # Usually it makes no sense to set a name when "replace" is # used, but commands might set this if they got an # dynamicfile, because otherwise you would have to match # against the hash too base = name else: base = os.path.basename(target) if constants.TAG_SEPARATOR in base: base = base.split(constants.TAG_SEPARATOR, 1)[1] name = re.sub(replace_pattern, replace, base) elif name: name = expandpath(name) # And prevent exceptions in os.symlink() if name[-1:] == "/": self._gen_err("name mustn't represent a directory") else: # "name" wasn't set by the user, # so fallback to use the target name (but without the tag) name = os.path.basename( target.split(constants.TAG_SEPARATOR, 1)[-1]) # Add prefix an suffix to name base, ext = os.path.splitext(os.path.basename(name)) if read_opt("extension"): ext = "." + read_opt("extension") name = os.path.join( os.path.dirname(name), read_opt("prefix") + base + read_opt("suffix") + ext) # Put together the path of the dir we create the link in if not directory: directory = self.directory # Use the current dir else: directory = os.path.join(self.directory, expandpath(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 = normpath(os.path.join(directory, 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._gen_err(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._gen_err(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._gen_err(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") linkdescriptor["secure"] = read_opt("secure") self.result["links"].append(linkdescriptor)
def parse_arguments(self, arguments=None): """Parses the commandline arguments. This function can parse a custom list of arguments, instead of ``sys.args``. Args: arguments (list): A list of arguments that will be parsed instead of ``sys.args`` Raises: :class:`~errors.UserError`: One ore more arguments are invalid or used in an invalid combination. """ if arguments is None: arguments = sys.argv[1:] # Setup parser parser = CustomParser() # Options parser.add_argument("--config", help="specify another config-file to use") parser.add_argument("--directory", help="set the default directory") parser.add_argument("-d", "--dryrun", help="just simulate what would happen", action="store_true") parser.add_argument("--dui", help="use the DUI strategy for updating links", action="store_true") parser.add_argument("-f", "--force", help="overwrite existing files with links", action="store_true") parser.add_argument("--info", help="print everything but debug messages", action="store_true") parser.add_argument("--log", help="specify a file to log to") parser.add_argument("-m", "--makedirs", help="create directories automatically if needed", action="store_true") parser.add_argument("--option", help="set options for profiles", dest="opt_dict", action=StoreDictKeyPair, nargs="+", metavar="KEY=VAL") parser.add_argument("--parent", help="set the parent of the profiles you install") parser.add_argument("--plain", help="print the internal DiffLog as plain json", action="store_true") parser.add_argument("-p", "--print", help="print what changes uberdot will do", action="store_true") parser.add_argument("-q", "--quiet", help="print nothing but errors", action="store_true") parser.add_argument("--save", help="specify another install-file to use", default="default") parser.add_argument("--silent", help="print absolute nothing", action="store_true") parser.add_argument("--skipafter", help="do not execute events after linking", action="store_true") parser.add_argument("--skipbefore", help="do not execute events before linking", action="store_true") parser.add_argument("--skipevents", help="do not execute any events", action="store_true") parser.add_argument("--skiproot", help="do nothing that requires root permissions", action="store_true") parser.add_argument("--superforce", help="overwrite blacklisted/protected files", action="store_true") parser.add_argument("-v", "--verbose", help="print stacktrace in case of error", action="store_true") # Modes modes = parser.add_mutually_exclusive_group(required=True) modes.add_argument("-h", "--help", help="show this help message and exit", action="help") modes.add_argument("-i", "--install", help="install and update (sub)profiles", action="store_true") modes.add_argument("--debuginfo", help="display internal values", action="store_true") modes.add_argument("-u", "--uninstall", help="uninstall (sub)profiles", action="store_true") modes.add_argument("-s", "--show", help="show infos about installed profiles", action="store_true") modes.add_argument("--version", help="print version number", action="store_true") # Profile list parser.add_argument("profiles", help="list of root profiles", nargs="*") # Read arguments try: self.args = parser.parse_args(arguments) except argparse.ArgumentError as err: raise UserError(err.message) # Options need some extra parsing for tags if self.args.opt_dict and "tags" in self.args.opt_dict: reader = csv.reader([self.args.opt_dict["tags"]]) self.args.opt_dict["tags"] = next(reader) # And the directory was specified relative to the old working directory if self.args.directory: self.args.directory = os.path.join(self.owd, self.args.directory) # Like the path to the config file if self.args.config: self.args.config = os.path.join(self.owd, self.args.config) # Load constants for this installed-file constants.loadconfig(self.args.config, self.args.save) # Write back defaults from config for arguments that weren't set if not self.args.dui: self.args.dui = constants.DUISTRATEGY if not self.args.force: self.args.force = constants.FORCE if not self.args.makedirs: self.args.makedirs = constants.MAKEDIRS if not self.args.skipafter: self.args.skipafter = constants.SKIPAFTER if not self.args.skipbefore: self.args.skipbefore = constants.SKIPBEFORE if not self.args.skipevents: self.args.skipevents = constants.SKIPEVENTS if not self.args.skiproot: self.args.skiproot = constants.SKIPROOT if not self.args.superforce: self.args.superforce = constants.SUPERFORCE if not self.args.directory: self.args.directory = constants.DIR_DEFAULT if not self.args.opt_dict: self.args.opt_dict = constants.DEFAULTS if not self.args.log and constants.LOGFILE: self.args.log = normpath(constants.LOGFILE) else: # Merge options provided by commandline with loaded defaults tmptags = self.args.opt_dict["tags"] + constants.DEFAULTS["tags"] self.args.opt_dict = {**constants.DEFAULTS, **self.args.opt_dict} self.args.opt_dict["tags"] = tmptags # Configure logger if self.args.silent: constants.LOGGINGLEVEL = "SILENT" if self.args.quiet: constants.LOGGINGLEVEL = "QUIET" if self.args.info: constants.LOGGINGLEVEL = "INFO" if self.args.verbose: constants.LOGGINGLEVEL = "VERBOSE" logging_level_mapping = { "SILENT": logging.CRITICAL, "QUIET": logging.WARNING, "INFO": logging.INFO, "VERBOSE": logging.DEBUG } try: logger.setLevel(logging_level_mapping[constants.LOGGINGLEVEL]) except KeyError: msg = "Unkown logginglevel '" + constants.LOGGINGLEVEL + "'" raise UserError(msg) if self.args.log: ch = logging.FileHandler(os.path.join(self.owd, self.args.log)) ch.setLevel(logging.DEBUG) form = '[%(asctime)s] - %(levelname)s - %(message)s' formatter = logging.Formatter(form) ch.setFormatter(formatter) logger.addHandler(ch)
def loadconfig(config_file, installed_filename="default"): """Loads constants from the config files. This will load all configs from :const:`CFG_FILES` or ``config_file`` if provided. The name of the installed-file will be used to load installed-file specific values. Args: config_file (str): Absolute path to the config file to use. If None, the configs from :const:`CFG_FILES` will be loaded. installed_filename (str): Name of the installed-file for that values will be loaded """ global C_OK, C_WARNING, C_FAIL, ENDC, BOLD, C_HIGHLIGHT, NOBOLD, C_DEBUG global DUISTRATEGY, FORCE, LOGGINGLEVEL, MAKEDIRS, DECRYPT_PWD, SUPERFORCE global SKIPROOT, DATA_DIR, SHELL, SHELL_TIMEOUT, SMART_CD, SKIPEVENTS global BACKUP_EXTENSION, PROFILE_FILES, TARGET_FILES, INSTALLED_FILE_BACKUP global COLOR, INSTALLED_FILE, DEFAULTS, DIR_DEFAULT, LOGFILE, CFG_FILES global ASKROOT, TAG_SEPARATOR, HASH_SEPARATOR, SKIPAFTER, SKIPBEFORE global SHELL_ARGS # Load config files if config_file: CFG_FILES = [config_file] else: CFG_FILES = find_files("uberdot.ini", CONFIG_SEARCH_PATHS) config = configparser.ConfigParser() try: for cfg in CFG_FILES: config.read(cfg) # We need to normalize all paths here, relatively to # the config file which it defined path_keys = [ "directory", "profilefiles", "targetfiles", "logfile", "datadir" ] for section in config.sections(): for item in config.items(section): key = item[0] if key in path_keys: config[section][key] = os.path.join( os.path.dirname(cfg), config[section][key]) config[section][key] = os.path.normpath( config[section][key]) except configparser.Error as err: msg = "Can't parse config at '" + cfg + "'. " + err.message raise PreconditionError(msg) # Setup special lookup function for getting values def getvalue(getter, section): """Creates function to lookup a specific value in a specific section with a specific getter. Args: getter (function): getter function to perform a single lookup section (str): The section that contains the key Returns: function: A function that can lookup keys in the config """ def lookup(key, fallback=None): """Looks up a value in a specific section for a specific type. Args: key (str): The name of the value that will be looked up fallback: A fallback value Returns: The value of the key """ installedfile_section = "Installed." + installed_filename installedfile_section += "." + section value = getter(section, key, fallback=fallback) return getter(installedfile_section, key, fallback=value) return lookup # Get arguments getstr = getvalue(config.get, "Arguments") getbool = getvalue(config.getboolean, "Arguments") DUISTRATEGY = getbool("dui", DUISTRATEGY) FORCE = getbool("force", FORCE) MAKEDIRS = getbool("makedirs", MAKEDIRS) SKIPAFTER = getbool("skipafter", SKIPAFTER) SKIPBEFORE = getbool("skipbefore", SKIPBEFORE) SKIPEVENTS = getbool("skipevents", SKIPEVENTS) SKIPROOT = getbool("skiproot", SKIPROOT) SUPERFORCE = getbool("superforce", SUPERFORCE) LOGGINGLEVEL = getstr("logginglevel", LOGGINGLEVEL).upper() LOGFILE = getstr("logfile", LOGFILE) # Get settings getstr = getvalue(config.get, "Settings") getbool = getvalue(config.getboolean, "Settings") getint = getvalue(config.getint, "Settings") ASKROOT = getbool("askroot", ASKROOT) SHELL = getstr("shell", SHELL) SHELL_ARGS = getstr("shellArgs", SHELL_ARGS) SHELL_TIMEOUT = getint("shellTimeout", SHELL_TIMEOUT) DECRYPT_PWD = getstr("decryptPwd", DECRYPT_PWD) BACKUP_EXTENSION = getstr("backupExtension", BACKUP_EXTENSION) TAG_SEPARATOR = getstr("tagSeparator", TAG_SEPARATOR) HASH_SEPARATOR = getstr("hashSeparator", HASH_SEPARATOR) PROFILE_FILES = getstr("profileFiles") TARGET_FILES = getstr("targetFiles") DATA_DIR = normpath(getstr("dataDir", DATA_DIR)) COLOR = getbool("color", COLOR) SMART_CD = getbool("smartShellCWD", SMART_CD) # Setup internal values INSTALLED_FILE = os.path.join(DATA_DIR, "installed/%s.json") INSTALLED_FILE_BACKUP = INSTALLED_FILE + "." + BACKUP_EXTENSION if not COLOR: C_OK = C_WARNING = C_FAIL = ENDC = BOLD = C_HIGHLIGHT = NOBOLD = '' C_DEBUG = '' # Get command options getstr = getvalue(config.get, "Defaults") getbool = getvalue(config.getboolean, "Defaults") getint = getvalue(config.getint, "Defaults") DEFAULTS = { "extension": getstr("extension", DEFAULTS["extension"]), "name": getstr("name", DEFAULTS["name"]), "optional": getbool("optional", DEFAULTS["optional"]), "owner": getstr("owner", DEFAULTS["owner"]), "permission": getint("permission", DEFAULTS["permission"]), "prefix": getstr("prefix", DEFAULTS["prefix"]), "replace": getstr("replace", DEFAULTS["replace"]), "replace_pattern": getstr("replace_pattern", DEFAULTS["replace_pattern"]), "suffix": getstr("suffix", DEFAULTS["suffix"]), "secure": getbool("secure", DEFAULTS["secure"]), "tags": next(csv.reader([getstr("tags", "")])) } DIR_DEFAULT = getstr("directory", DIR_DEFAULT) # Insert installed-file into constants INSTALLED_FILE = INSTALLED_FILE % installed_filename INSTALLED_FILE_BACKUP = INSTALLED_FILE_BACKUP % installed_filename # Check if TARGET_FILES and PROFILE_FILES were set by the user if not TARGET_FILES or TARGET_FILES == "</path/to/your/dotfiles/>": raise UserError("No directory for your dotfiles specified.") if not PROFILE_FILES or PROFILE_FILES == "</path/to/your/profiles/>": raise UserError("No directory for your profiles specified.")
NOBOLD = '\033[22m' """Bash color code to stop bold text.""" # Loaders for config and installed-section ############################################################################### # Search paths for config files CFG_FILES = [] """A list with all configuration files that will be used to set constants. All settings of all configuration files will be used. If a specific setting is set in more than one configuration file, the setting from the configuration file with higher index will be prefered. """ CONFIG_SEARCH_PATHS = [ "/etc/uberdot", os.path.join(get_user_env_var('XDG_CONFIG_HOME', normpath('~/.config')), "uberdot"), DATA_DIR ] """A list of paths that will be used to search for configuration files. """ def loadconfig(config_file, installed_filename="default"): """Loads constants from the config files. This will load all configs from :const:`CFG_FILES` or ``config_file`` if provided. The name of the installed-file will be used to load installed-file specific values. Args: config_file (str): Absolute path to the config file to use. If None, the configs from :const:`CFG_FILES` will be loaded.