diff --git a/deluge/common.py b/deluge/common.py index 5910b2d40..65dea2ca9 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -13,8 +13,10 @@ import base64 import gettext import locale import logging +import numbers import os import platform +import re import subprocess import sys import time @@ -465,6 +467,82 @@ def fdate(seconds, date_only=False, precision_secs=False): return time.strftime("%x %H:%M", time.localtime(seconds)) +def tokenize(text): + """ + Tokenize a text into numbers and strings. + + Args: + text (str): The text to tokenize (a string). + + Returns: + list: A list of strings and/or numbers. + + This function is used to implement robust tokenization of user input + It automatically coerces integer and floating point numbers, ignores + whitespace and knows how to separate numbers from strings even without + whitespace. + """ + tokenized_input = [] + for token in re.split(r'(\d+(?:\.\d+)?)', text): + token = token.strip() + if re.match(r'\d+\.\d+', token): + tokenized_input.append(float(token)) + elif token.isdigit(): + tokenized_input.append(int(token)) + elif token: + tokenized_input.append(token) + return tokenized_input + + +size_units = (dict(prefix='b', divider=1, singular='byte', plural='bytes'), + dict(prefix='KiB', divider=1024**1), + dict(prefix='MiB', divider=1024**2), + dict(prefix='GiB', divider=1024**3), + dict(prefix='TiB', divider=1024**4), + dict(prefix='PiB', divider=1024**5), + dict(prefix='KB', divider=1000**1), + dict(prefix='MB', divider=1000**2), + dict(prefix='GB', divider=1000**3), + dict(prefix='TB', divider=1000**4), + dict(prefix='PB', divider=1000**5), + dict(prefix='m', divider=1000**2)) + + +class InvalidSize(Exception): + pass + + +def parse_human_size(size): + """ + Parse a human readable data size and return the number of bytes. + + Args: + size (str): The human readable file size to parse (a string). + + Returns: + int: The corresponding size in bytes. + + Raises: + InvalidSize: when the input can't be parsed. + + """ + tokens = tokenize(size) + if tokens and isinstance(tokens[0], numbers.Number): + # If the input contains only a number, it's assumed to be the number of bytes. + if len(tokens) == 1: + return int(tokens[0]) + # Otherwise we expect to find two tokens: A number and a unit. + if len(tokens) == 2 and isinstance(tokens[1], basestring): + normalized_unit = tokens[1].lower() + # Try to match the first letter of the unit. + for unit in size_units: + if normalized_unit.startswith(unit['prefix'].lower()): + return int(tokens[0] * unit['divider']) + # We failed to parse the size specification. + msg = "Failed to parse size! (input %r was tokenized as %r)" + raise InvalidSize(msg % (size, tokens)) + + def is_url(url): """ A simple test to check if the URL is valid diff --git a/deluge/log.py b/deluge/log.py index 9d3b77469..5c34dc81f 100644 --- a/deluge/log.py +++ b/deluge/log.py @@ -107,15 +107,19 @@ levels = { } -def setup_logger(level="error", filename=None, filemode="w"): +def setup_logger(level="error", filename=None, filemode="w", logrotate=None): """ Sets up the basic logger and if `:param:filename` is set, then it will log to that file instead of stdout. - :param level: str, the level to log - :param filename: str, the file to log to + Args: + level (str): The log level to use (Default: "error") + filename (str, optional): The log filename. Default is None meaning log + to terminal + filemode (str): The filemode to use when opening the log file + logrotate (int, optional): The size of the logfile in bytes when enabling + log rotation (Default is None meaning disabled) """ - if logging.getLoggerClass() is not Logging: logging.setLoggerClass(Logging) logging.addLevelName(5, "TRACE") @@ -125,19 +129,16 @@ def setup_logger(level="error", filename=None, filemode="w"): root_logger = logging.getLogger() - if filename and filemode == "a": + if filename and logrotate: handler = logging.handlers.RotatingFileHandler( - filename, filemode, - maxBytes=50 * 1024 * 1024, # 50 Mb - backupCount=3, - encoding="utf-8", - delay=0 + filename, maxBytes=logrotate, + backupCount=5, encoding="utf-8" ) elif filename and filemode == "w": handler = getattr( logging.handlers, "WatchedFileHandler", logging.FileHandler)( - filename, filemode, "utf-8", delay=0 - ) + filename, mode=filemode, encoding="utf-8" + ) else: handler = logging.StreamHandler(stream=sys.stdout) diff --git a/deluge/tests/test_common.py b/deluge/tests/test_common.py index 3b4c0f710..fc6e97971 100644 --- a/deluge/tests/test_common.py +++ b/deluge/tests/test_common.py @@ -74,3 +74,21 @@ class CommonTestCase(unittest.TestCase): self.failUnless(VersionSplit("1.4.0") > VersionSplit("1.4.0.dev123")) self.failUnless(VersionSplit("1.4.0.dev1") < VersionSplit("1.4.0")) self.failUnless(VersionSplit("1.4.0a1") < VersionSplit("1.4.0")) + + def test_parse_human_size(self): + from deluge.common import parse_human_size + sizes = [("1", 1), + ("10 bytes", 10), + ("2048 bytes", 2048), + ("1MiB", 2**(10 * 2)), + ("1 MiB", 2**(10 * 2)), + ("1 GiB", 2**(10 * 3)), + ("1 GiB", 2**(10 * 3)), + ("1M", 10**6), + ("1MB", 10**6), + ("1 GB", 10**9), + ("1 TB", 10**12)] + + for human_size, byte_size in sizes: + parsed = parse_human_size(human_size) + self.assertEquals(parsed, byte_size, "Mismatch when converting '%s'" % human_size) diff --git a/deluge/ui/baseargparser.py b/deluge/ui/baseargparser.py index a85184ddb..fa5793a08 100644 --- a/deluge/ui/baseargparser.py +++ b/deluge/ui/baseargparser.py @@ -14,21 +14,21 @@ import platform import sys import textwrap -import deluge.common import deluge.configmanager import deluge.log +from deluge import common from deluge.log import setup_logger def get_version(): - version_str = "%s\n" % (deluge.common.get_version()) + version_str = "%s\n" % (common.get_version()) try: from deluge._libtorrent import lt version_str += "libtorrent: %s\n" % lt.version except ImportError: pass version_str += "Python: %s\n" % platform.python_version() - version_str += "OS: %s %s\n" % (platform.system(), " ".join(deluge.common.get_os_version())) + version_str += "OS: %s %s\n" % (platform.system(), " ".join(common.get_os_version())) return version_str @@ -93,6 +93,7 @@ class BaseArgParser(argparse.ArgumentParser): kwargs["formatter_class"] = lambda prog: DelugeTextHelpFormatter(prog, max_help_position=33, width=90) super(BaseArgParser, self).__init__(*args, add_help=False, **kwargs) + self.common_setup = False self.group = self.add_argument_group('Common Options') self.group.add_argument('--version', action='version', version='%(prog)s ' + get_version(), help="Show program's version number and exit") @@ -104,37 +105,43 @@ class BaseArgParser(argparse.ArgumentParser): help="Set the log level: %s" % ", ".join(deluge.log.levels)) self.group.add_argument("-q", "--quiet", action="store_true", default=False, help="Sets the log level to 'none', this is the same as `-L none`") - self.group.add_argument("-r", "--rotate-logs", action="store_true", default=False, - help="Rotate logfiles.") + self.group.add_argument("--log-rotate", action="store", nargs="?", const="50M", + help="Enable logfile rotation (optional max file size, default: %(const)s)." + "Log file rotate count is 5") self.group.add_argument("--profile", metavar="", action="store", nargs="?", default=False, help="Profile %(prog)s with cProfile. Prints results to stdout" - "unless a filename is specififed.") + "unless a filename is specififed") self.group.add_argument("-h", "--help", action=HelpAction, help='Show this help message and exit') def parse_args(self, *args): options, remaining = super(BaseArgParser, self).parse_known_args(*args) options.remaining = remaining - # Setup the logger - if options.quiet: - options.loglevel = "none" - if options.loglevel: - options.loglevel = options.loglevel.lower() + if not self.common_setup: + self.common_setup = True + # Setup the logger + if options.quiet: + options.loglevel = "none" + if options.loglevel: + options.loglevel = options.loglevel.lower() - logfile_mode = 'w' - if options.rotate_logs: - logfile_mode = 'a' + logfile_mode = 'w' + logrotate = options.log_rotate + if options.log_rotate: + logfile_mode = 'a' + logrotate = common.parse_human_size(options.log_rotate) - # Setup the logger - setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode) + # Setup the logger + setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode, + logrotate=logrotate) - if options.config: - if not deluge.configmanager.set_config_dir(options.config): - log = logging.getLogger(__name__) - log.error("There was an error setting the config dir! Exiting..") - sys.exit(1) - else: - if not os.path.exists(deluge.common.get_default_config_dir()): - os.makedirs(deluge.common.get_default_config_dir()) + if options.config: + if not deluge.configmanager.set_config_dir(options.config): + log = logging.getLogger(__name__) + log.error("There was an error setting the config dir! Exiting..") + sys.exit(1) + else: + if not os.path.exists(common.get_default_config_dir()): + os.makedirs(common.get_default_config_dir()) return options diff --git a/deluge/ui/console/console.py b/deluge/ui/console/console.py index 955a8809b..d0ef89740 100644 --- a/deluge/ui/console/console.py +++ b/deluge/ui/console/console.py @@ -24,6 +24,7 @@ log = logging.getLogger(__name__) # defined in setup.py # + def load_commands(command_dir): def get_command(name):