summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbendikro <bro.devel+deluge@gmail.com>2016-01-15 22:08:44 +0100
committerbendikro <bro.devel+deluge@gmail.com>2016-04-18 09:36:13 +0200
commit6300f9154acf04bf9bb7c3db44603fd1954efe37 (patch)
tree7b4309bf419b92767d58cdc65a258f08f6405494
parentc90af1ce6c01e8948f0ffd388c49292efe75a211 (diff)
downloaddeluge-6300f9154acf04bf9bb7c3db44603fd1954efe37.tar.gz
deluge-6300f9154acf04bf9bb7c3db44603fd1954efe37.tar.bz2
deluge-6300f9154acf04bf9bb7c3db44603fd1954efe37.zip
[#1949] [UI] Allow setting max size for rotating log file
-rw-r--r--deluge/common.py78
-rw-r--r--deluge/log.py25
-rw-r--r--deluge/tests/test_common.py18
-rw-r--r--deluge/ui/baseargparser.py61
-rw-r--r--deluge/ui/console/console.py1
5 files changed, 144 insertions, 39 deletions
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="<results file>", 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()
-
- logfile_mode = 'w'
- if options.rotate_logs:
- logfile_mode = 'a'
-
- # Setup the logger
- setup_logger(level=options.loglevel, filename=options.logfile, filemode=logfile_mode)
-
- 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 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'
+ 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,
+ 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(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):