diff --git a/README.md b/README.md index 9edc047..8e63220 100644 --- a/README.md +++ b/README.md @@ -74,26 +74,29 @@ chkbit will Run `chkbit PATH` to verify only. ``` -usage: chkbit [-h] [-u] [--show-ignored-only] [--algo ALGO] [-f] [-s] [--index-name NAME] [--ignore-name NAME] [-w N] [--plain] [-q] [-v] [PATH ...] +usage: chkbit [-h] [-u] [--show-ignored-only] [--algo ALGO] [-f] [-s] [-l FILE] [--log-verbose] [--index-name NAME] [--ignore-name NAME] [-w N] [--plain] [-q] [-v] [PATH ...] Checks the data integrity of your files. See https://github.com/laktak/chkbit-py positional arguments: - PATH directories to check + PATH directories to check options: - -h, --help show this help message and exit - -u, --update update indices (without this chkbit will verify files in readonly mode) - --show-ignored-only only show ignored files - --algo ALGO hash algorithm: md5, sha512, blake3 (default: blake3) - -f, --force force update of damaged items - -s, --skip-symlinks do not follow symlinks - --index-name NAME filename where chkbit stores its hashes (default: .chkbit) - --ignore-name NAME filename that chkbit reads its ignore list from (default: .chkbitignore) - -w N, --workers N number of workers to use (default: 5) - --plain show plain status instead of being fancy - -q, --quiet quiet, don't show progress/information - -v, --verbose verbose output + -h, --help show this help message and exit + -u, --update update indices (without this chkbit will verify files in readonly mode) + --show-ignored-only only show ignored files + --algo ALGO hash algorithm: md5, sha512, blake3 (default: blake3) + -f, --force force update of damaged items + -s, --skip-symlinks do not follow symlinks + -l FILE, --log-file FILE + write to a logfile if specified + --log-verbose verbose logging + --index-name NAME filename where chkbit stores its hashes (default: .chkbit) + --ignore-name NAME filename that chkbit reads its ignore list from (default: .chkbitignore) + -w N, --workers N number of workers to use (default: 5) + --plain show plain status instead of being fancy + -q, --quiet quiet, don't show progress/information + -v, --verbose verbose output .chkbitignore rules: each line should contain exactly one name diff --git a/chkbit/status.py b/chkbit/status.py index be9de0b..4a6d156 100644 --- a/chkbit/status.py +++ b/chkbit/status.py @@ -1,4 +1,6 @@ +from __future__ import annotations from enum import Enum +import logging class Status(Enum): @@ -11,3 +13,16 @@ class Status(Enum): IGNORE = "ign" INTERNALEXCEPTION = "EXC" UPDATE_INDEX = "iup" + + @staticmethod + def get_level(status: Status): + if status == Status.INTERNALEXCEPTION: + return logging.CRITICAL + elif status in [Status.ERR_DMG, Status.ERR_IDX]: + return logging.ERROR + if status == Status.WARN_OLD: + return logging.WARNING + elif status in [Status.NEW, Status.UPDATE, Status.OK, Status.IGNORE]: + return logging.INFO + else: + return logging.DEBUG diff --git a/chkbit_cli/main.py b/chkbit_cli/main.py index 9dd2092..8720e07 100644 --- a/chkbit_cli/main.py +++ b/chkbit_cli/main.py @@ -1,4 +1,5 @@ import argparse +import logging import os import queue import shutil @@ -50,12 +51,16 @@ class Main: self.num_new = 0 self.num_upd = 0 self.verbose = False + self.log = logging.getLogger("") + self.log_verbose = False self.progress = Progress.Fancy self.total = 0 self.term_width = shutil.get_terminal_size()[0] max_stat = int((self.term_width - 70) / 2) self.fps = RateCalc(timedelta(seconds=1), max_stat=max_stat) self.bps = RateCalc(timedelta(seconds=1), max_stat=max_stat) + # disable + self.log.setLevel(logging.CRITICAL + 1) def _log(self, stat: Status, path: str): if stat == Status.UPDATE_INDEX: @@ -73,8 +78,18 @@ class Main: elif stat == Status.NEW: self.num_new += 1 + lvl = Status.get_level(stat) + if self.log_verbose or not stat in [Status.OK, Status.IGNORE]: + self.log.log(lvl, f"{stat.value} {path}") + if self.verbose or not stat in [Status.OK, Status.IGNORE]: - CLI.printline(stat.value, " ", path) + CLI.printline( + CLI_ALERT_FG if lvl >= logging.WARNING else "", + stat.value, + " ", + path, + CLI.style.reset, + ) def _res_worker(self, context: Context): last = datetime.now() @@ -182,10 +197,9 @@ class Main: iunit2 = lambda x, u1, u2: f"{x} {u2 if x!=1 else u1}" if self.progress != Progress.Quiet: - cprint( - CLI_OK_FG, - f"Processed {iunit(self.total, 'file')}{' in readonly mode' if not context.update else ''}.", - ) + status = f"Processed {iunit(self.total, 'file')}{' in readonly mode' if not context.update else ''}." + cprint(CLI_OK_FG, status) + self.log.info(status) if self.progress == Progress.Fancy and self.total > 0: elapsed = datetime.now() - self.fps.start @@ -219,13 +233,14 @@ class Main: for err in self.dmg_list: print(err, file=sys.stderr) n = len(self.dmg_list) - eprint( - CLI_ALERT_FG, - f"error: detected {iunit(n, 'file')} with damage!", - ) + status = f"error: detected {iunit(n, 'file')} with damage!" + self.log.error(status) + eprint(CLI_ALERT_FG, status) if self.err_list: - eprint(CLI_ALERT_FG, "chkbit ran into errors:") + status = "chkbit ran into errors" + self.log.error(status + "!") + eprint(CLI_ALERT_FG, status + ":") for err in self.err_list: print(err, file=sys.stderr) @@ -270,6 +285,18 @@ class Main: "-s", "--skip-symlinks", action="store_true", help="do not follow symlinks" ) + parser.add_argument( + "-l", + "--log-file", + metavar="FILE", + type=str, + help="write to a logfile if specified", + ) + + parser.add_argument( + "--log-verbose", action="store_true", help="verbose logging" + ) + parser.add_argument( "--index-name", metavar="NAME", @@ -315,6 +342,18 @@ class Main: args = parser.parse_args() self.verbose = args.verbose or args.show_ignored_only + if args.log_file: + self.log_verbose = args.log_verbose + self.log.setLevel(logging.INFO) + fh = logging.FileHandler(args.log_file) + fh.setFormatter( + logging.Formatter( + "%(asctime)s %(levelname).4s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + self.log.addHandler(fh) + if args.quiet: self.progress = Progress.Quiet elif not sys.stdout.isatty(): @@ -323,6 +362,7 @@ class Main: self.progress = Progress.Plain if args.paths: + self.log.info(f"chkbit {', '.join(args.paths)}") context = self.process(args) if context and not context.show_ignored_only: self.print_result(context)