From b686174b07bd56af4e5ffaa23c24f27f417fc305 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 19 Feb 2023 16:05:52 +0100 Subject: Merging upstream version 2.1.1 (Closes: #1026291). Signed-off-by: Daniel Baumann --- deluge/config.py | 221 +++++++++++++++++++++++++++---------------------------- 1 file changed, 110 insertions(+), 111 deletions(-) (limited to 'deluge/config.py') diff --git a/deluge/config.py b/deluge/config.py index c852996..c5cb312 100644 --- a/deluge/config.py +++ b/deluge/config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright (C) 2008 Andrew Resch # @@ -39,78 +38,66 @@ this can only be done for the 'config file version' and not for the 'format' version as this will be done internally. """ -from __future__ import unicode_literals - import json import logging import os +import pickle import shutil from codecs import getwriter -from io import open from tempfile import NamedTemporaryFile -import six.moves.cPickle as pickle # noqa: N813 - from deluge.common import JSON_FORMAT, get_default_config_dir log = logging.getLogger(__name__) -callLater = None # noqa: N816 Necessary for the config tests - -def prop(func): - """Function decorator for defining property attributes - The decorated function is expected to return a dictionary - containing one or more of the following pairs: - - fget - function for getting attribute value - fset - function for setting attribute value - fdel - function for deleting attribute - - This can be conveniently constructed by the locals() builtin - function; see: - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183 - """ - return property(doc=func.__doc__, **func()) - - -def find_json_objects(s): - """Find json objects in a string. +def find_json_objects(text, decoder=json.JSONDecoder()): + """Find json objects in text. Args: - s (str): the string to find json objects in + text (str): The text to find json objects within. Returns: list: A list of tuples containing start and end locations of json - objects in string `s`. e.g. [(start, end), ...] + objects in the text. e.g. [(start, end), ...] + """ objects = [] - opens = 0 - start = s.find('{') - offset = start - - if start < 0: - return [] - - quoted = False - for index, c in enumerate(s[offset:]): - if c == '"': - quoted = not quoted - elif quoted: - continue - elif c == '{': - opens += 1 - elif c == '}': - opens -= 1 - if opens == 0: - objects.append((start, index + offset + 1)) - start = index + offset + 1 + offset = 0 + while True: + try: + start = text.index('{', offset) + except ValueError: + break + + try: + __, index = decoder.raw_decode(text[start:]) + except json.decoder.JSONDecodeError: + offset = start + 1 + else: + offset = start + index + objects.append((start, offset)) return objects -class Config(object): +def cast_to_existing_type(value, old_value): + """Attempt to convert new value type to match old value type""" + types_match = isinstance(old_value, (type(None), type(value))) + if value is not None and not types_match: + old_type = type(old_value) + # Skip convert to bytes since requires knowledge of encoding and value should + # be unicode anyway. + if old_type is bytes: + return value + + return old_type(value) + + return value + + +class Config: """This class is used to access/create/modify config files. Args: @@ -120,13 +107,23 @@ class Config(object): file_version (int): The file format for the default config values when creating a fresh config. This value should be increased whenever a new migration function is setup to convert old config files. (default: 1) + log_mask_funcs (dict): A dict of key:function, used to mask sensitive + key values (e.g. passwords) when logging is enabled. """ - def __init__(self, filename, defaults=None, config_dir=None, file_version=1): + def __init__( + self, + filename, + defaults=None, + config_dir=None, + file_version=1, + log_mask_funcs=None, + ): self.__config = {} self.__set_functions = {} self.__change_callbacks = [] + self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {} # These hold the version numbers and they will be set when loaded self.__version = {'format': 1, 'file': file_version} @@ -137,7 +134,7 @@ class Config(object): if defaults: for key, value in defaults.items(): - self.set_item(key, value) + self.set_item(key, value, default=True) # Load the config from file in the config_dir if config_dir: @@ -147,6 +144,12 @@ class Config(object): self.load() + def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase + """Wrapper around reactor.callLater for test purpose.""" + from twisted.internet import reactor + + return reactor.callLater(period, func, *args, **kwargs) + def __contains__(self, item): return item in self.__config @@ -155,7 +158,7 @@ class Config(object): return self.set_item(key, value) - def set_item(self, key, value): + def set_item(self, key, value, default=False): """Sets item 'key' to 'value' in the config dictionary. Does not allow changing the item's type unless it is None. @@ -167,6 +170,8 @@ class Config(object): key (str): Item to change to change. value (any): The value to change item to, must be same type as what is currently in the config. + default (optional, bool): When setting a default value skip func or save + callbacks. Raises: ValueError: Raised when the type of value is not the same as what is @@ -179,61 +184,54 @@ class Config(object): 5 """ - if key not in self.__config: - self.__config[key] = value - log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value)) - return - - if self.__config[key] == value: - return + if isinstance(value, bytes): + value = value.decode() - # Change the value type if it is not None and does not match. - type_match = isinstance(self.__config[key], (type(None), type(value))) - if value is not None and not type_match: + if key in self.__config: try: - oldtype = type(self.__config[key]) - # Don't convert to bytes as requires encoding and value will - # be decoded anyway. - if oldtype is not bytes: - value = oldtype(value) + value = cast_to_existing_type(value, self.__config[key]) except ValueError: log.warning('Value Type "%s" invalid for key: %s', type(value), key) raise + else: + if self.__config[key] == value: + return - if isinstance(value, bytes): - value = value.decode('utf8') - - log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value)) + if log.isEnabledFor(logging.DEBUG): + if key in self.__log_mask_funcs: + value = self.__log_mask_funcs[key](value) + log.debug( + 'Setting key "%s" to: %s (of type: %s)', + key, + value, + type(value), + ) self.__config[key] = value - global callLater - if callLater is None: - # Must import here and not at the top or it will throw ReactorAlreadyInstalledError - from twisted.internet.reactor import ( - callLater, - ) # pylint: disable=redefined-outer-name + # Skip save or func callbacks if setting default value for keys + if default: + return + # Run the set_function for this key if any - try: - for func in self.__set_functions[key]: - callLater(0, func, key, value) - except KeyError: - pass + for func in self.__set_functions.get(key, []): + self.callLater(0, func, key, value) + try: def do_change_callbacks(key, value): for func in self.__change_callbacks: func(key, value) - callLater(0, do_change_callbacks, key, value) + self.callLater(0, do_change_callbacks, key, value) except Exception: pass # We set the save_timer for 5 seconds if not already set if not self._save_timer or not self._save_timer.active(): - self._save_timer = callLater(5, self.save) + self._save_timer = self.callLater(5, self.save) def __getitem__(self, key): - """See get_item """ + """See get_item""" return self.get_item(key) def get_item(self, key): @@ -306,16 +304,9 @@ class Config(object): del self.__config[key] - global callLater - if callLater is None: - # Must import here and not at the top or it will throw ReactorAlreadyInstalledError - from twisted.internet.reactor import ( - callLater, - ) # pylint: disable=redefined-outer-name - # We set the save_timer for 5 seconds if not already set if not self._save_timer or not self._save_timer.active(): - self._save_timer = callLater(5, self.save) + self._save_timer = self.callLater(5, self.save) def register_change_callback(self, callback): """Registers a callback function for any changed value. @@ -361,7 +352,6 @@ class Config(object): # Run the function now if apply_now is set if apply_now: function(key, self.__config[key]) - return def apply_all(self): """Calls all set functions. @@ -404,9 +394,9 @@ class Config(object): filename = self.__config_file try: - with open(filename, 'r', encoding='utf8') as _file: + with open(filename, encoding='utf8') as _file: data = _file.read() - except IOError as ex: + except OSError as ex: log.warning('Unable to open config file %s: %s', filename, ex) return @@ -436,12 +426,24 @@ class Config(object): log.exception(ex) log.warning('Unable to load config file: %s', filename) + if not log.isEnabledFor(logging.DEBUG): + return + + config = self.__config + if self.__log_mask_funcs: + config = { + key: self.__log_mask_funcs[key](config[key]) + if key in self.__log_mask_funcs + else config[key] + for key in config + } + log.debug( 'Config %s version: %s.%s loaded: %s', filename, self.__version['format'], self.__version['file'], - self.__config, + config, ) def save(self, filename=None): @@ -459,7 +461,7 @@ class Config(object): # Check to see if the current config differs from the one on disk # We will only write a new config file if there is a difference try: - with open(filename, 'r', encoding='utf8') as _file: + with open(filename, encoding='utf8') as _file: data = _file.read() objects = find_json_objects(data) start, end = objects[0] @@ -471,7 +473,7 @@ class Config(object): if self._save_timer and self._save_timer.active(): self._save_timer.cancel() return True - except (IOError, IndexError) as ex: + except (OSError, IndexError) as ex: log.warning('Unable to open config file: %s because: %s', filename, ex) # Save the new config and make sure it's written to disk @@ -485,7 +487,7 @@ class Config(object): json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT) _file.flush() os.fsync(_file.fileno()) - except IOError as ex: + except OSError as ex: log.error('Error writing new config file: %s', ex) return False @@ -496,7 +498,7 @@ class Config(object): try: log.debug('Backing up old config file to %s.bak', filename) shutil.move(filename, filename + '.bak') - except IOError as ex: + except OSError as ex: log.warning('Unable to backup old config: %s', ex) # The new config file has been written successfully, so let's move it over @@ -504,7 +506,7 @@ class Config(object): try: log.debug('Moving new config file %s to %s', filename_tmp, filename) shutil.move(filename_tmp, filename) - except IOError as ex: + except OSError as ex: log.error('Error moving new config file: %s', ex) return False else: @@ -556,14 +558,11 @@ class Config(object): def config_file(self): return self.__config_file - @prop - def config(): # pylint: disable=no-method-argument + @property + def config(self): """The config dictionary""" + return self.__config - def fget(self): - return self.__config - - def fdel(self): - return self.save() - - return locals() + @config.deleter + def config(self): + return self.save() -- cgit v1.2.3