From 8453468459b3977aceabb7e6ff14add704f74f2d Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Thu, 13 Jan 2022 22:47:25 +0100 Subject: [PATCH 01/14] add csv export for #16 --- catcli/catcli.py | 22 +++++++++++++++++++- catcli/logger.py | 23 +++++++++++++++++---- catcli/noder.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/catcli/catcli.py b/catcli/catcli.py index 431c195..05451d6 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -45,6 +45,7 @@ Usage: {1} rename [--catalog=] [-BCfV] {1} edit [--catalog=] [-BCfV] {1} graph [--catalog=] [-BCV] [] + {1} export [--catalog=] [-BH] [--format=] [] {1} help {1} --help {1} --version @@ -57,7 +58,9 @@ Options: -b --script Output script to manage found file(s) [default: False]. -C --no-color Do not output colors [default: False]. -c --hash Calculate md5 hash [default: False]. - -d --directory Only directory (default: False). + -d --directory Only directory [default: False]. + -F --format= Export format [default: csv]. + -H --header Export with header [default: False]. -f --force Do not ask when updating the catalog [default: False]. -l --lpath= Path where changes are logged [default: ] -n --no-subsize Do not store size of directories [default: False]. @@ -202,6 +205,21 @@ def cmd_rename(args, noder, catalog, top): return top +def cmd_export(args, noder, catalog, top): + path = args[''] + node = top + if path: + node = noder.get_node(top, path) + + header = args['--header'] + + fmt = args['--format'] + if fmt == 'csv': + noder.to_csv(node, with_header=header) + else: + Logger.err('Format not supported: {}'.format(fmt)) + + def cmd_edit(args, noder, catalog, top): storage = args[''] storages = list(x.name for x in top.children) @@ -276,6 +294,8 @@ def main(): cmd_rename(args, noder, catalog, top) elif args['edit']: cmd_edit(args, noder, catalog, top) + elif args['export']: + cmd_export(args, noder, catalog, top) return True diff --git a/catcli/logger.py b/catcli/logger.py index 8500a97..1f1dbef 100644 --- a/catcli/logger.py +++ b/catcli/logger.py @@ -48,13 +48,15 @@ class Logger: if attr: end = ' {}({}){}'.format(Logger.GRAY, attr, Logger.RESET) s = '{}{}{}{}:'.format(pre, Logger.UND, Logger.STORAGE, Logger.RESET) - s += ' {}{}{}{}\n'.format(Logger.PURPLE, name, Logger.RESET, end) + s += ' {}{}{}{}\n'.format(Logger.PURPLE, + _fix_badchars(name), + Logger.RESET, end) s += ' {}{}{}'.format(Logger.GRAY, args, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def file(pre, name, attr): '''print a file node''' - s = '{}{}'.format(pre, name) + s = '{}{}'.format(pre, _fix_badchars(name)) s += ' {}[{}]{}'.format(Logger.GRAY, attr, Logger.RESET) sys.stdout.write('{}\n'.format(s)) @@ -67,12 +69,14 @@ class Logger: end.append(' '.join(['{}:{}'.format(x, y) for x, y in attr])) if end: end = ' [{}]'.format(', '.join(end)) - s = '{}{}{}{}'.format(pre, Logger.BLUE, name, Logger.RESET) + s = '{}{}{}{}'.format(pre, Logger.BLUE, + _fix_badchars(name), Logger.RESET) s += '{}{}{}'.format(Logger.GRAY, end, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def arc(pre, name, archive): - s = '{}{}{}{}'.format(pre, Logger.YELLOW, name, Logger.RESET) + s = '{}{}{}{}'.format(pre, Logger.YELLOW, + _fix_badchars(name), Logger.RESET) s += ' {}[{}:{}]{}'.format(Logger.GRAY, Logger.ARCHIVE, archive, Logger.RESET) sys.stdout.write('{}\n'.format(s)) @@ -82,34 +86,45 @@ class Logger: ###################################################################### def out(string): '''to stdout no color''' + string = _fix_badchars(string) sys.stdout.write('{}\n'.format(string)) def debug(string): '''to stderr no color''' + string = _fix_badchars(string) sys.stderr.write('[DBG] {}\n'.format(string)) def info(string): '''to stdout in color''' + string = _fix_badchars(string) s = '{}{}{}'.format(Logger.MAGENTA, string, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def err(string): '''to stderr in RED''' + string = _fix_badchars(string) s = '{}{}{}'.format(Logger.RED, string, Logger.RESET) sys.stderr.write('{}\n'.format(s)) def progr(string): '''print progress''' + string = _fix_badchars(string) sys.stderr.write('{}\r'.format(string)) sys.stderr.flush() def bold(string): '''make it bold''' + string = _fix_badchars(string) return '{}{}{}'.format(Logger.BOLD, string, Logger.RESET) def flog(path, string, append=True): + string = _fix_badchars(string) mode = 'w' if append: mode = 'a' with open(path, mode) as f: f.write(string) + + +def _fix_badchars(line): + return line.encode('utf-8', 'ignore').decode('utf-8') diff --git a/catcli/noder.py b/catcli/noder.py index 8b793b4..396c77d 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -35,6 +35,7 @@ class Noder: TYPE_ARC = 'arc' TYPE_STORAGE = 'storage' TYPE_META = 'meta' + CSV_HEADER = 'name,type,path,size,md5' def __init__(self, debug=False, sortsize=False, arc=False): ''' @@ -280,6 +281,46 @@ class Noder: ############################################################### # printing ############################################################### + def _node_to_csv(self, node, sep=','): + ''' + print a node to csv + @node: the node to consider + ''' + if not node: + return '' + if node.type == self.TYPE_TOP: + return '' + if node.type == self.TYPE_STORAGE: + return '' + + out = [] + + # node name + out.append(node.name) + + # node type + out.append(node.type) + + # node full path + parents = self._get_parents(node) + storage = self._get_storage(node) + fullpath = os.path.join(storage.name, parents) + out.append(fullpath) + + # size + if node.size: + out.append(utils.human(node.size)) + else: + out.append('') + + # md5 if any + if node.md5: + out.append(node.md5) + else: + out.append('') + + return sep.join(['"' + o + '"' for o in out]) + def _print_node(self, node, pre='', withpath=False, withdepth=False, withstorage=False, recalcparent=False): @@ -376,6 +417,17 @@ class Noder: for pre, fill, node in rend: self._print_node(node, pre=pre, withdepth=True) + def to_csv(self, node, with_header=False): + '''print the tree to csv''' + if with_header: + Logger.out(self.CSV_HEADER) + + rend = anytree.RenderTree(node, childiter=self._sort_tree) + for _, _, node in rend: + line = self._node_to_csv(node) + if len(line) > 0: + Logger.out(line) + def to_dot(self, node, path='tree.dot'): '''export to dot for graphing''' anytree.exporter.DotExporter(node).to_dotfile(path) From 38bc83d3b947743541ba640eaff3f4fcb680736c Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 14 Jan 2022 10:25:04 +0100 Subject: [PATCH 02/14] adding indexed at and maccess to csv --- catcli/catcli.py | 4 ++-- catcli/logger.py | 5 +++++ catcli/noder.py | 10 +++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/catcli/catcli.py b/catcli/catcli.py index 05451d6..85fea0a 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -238,8 +238,8 @@ def cmd_edit(args, noder, catalog, top): def banner(): - Logger.out(BANNER) - Logger.out("") + Logger.out_err(BANNER) + Logger.out_err("") def main(): diff --git a/catcli/logger.py b/catcli/logger.py index 1f1dbef..d9d1e3a 100644 --- a/catcli/logger.py +++ b/catcli/logger.py @@ -89,6 +89,11 @@ class Logger: string = _fix_badchars(string) sys.stdout.write('{}\n'.format(string)) + def out_err(string): + '''to stderr no color''' + string = _fix_badchars(string) + sys.stderr.write('{}\n'.format(string)) + def debug(string): '''to stderr no color''' string = _fix_badchars(string) diff --git a/catcli/noder.py b/catcli/noder.py index 396c77d..c969909 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -35,7 +35,7 @@ class Noder: TYPE_ARC = 'arc' TYPE_STORAGE = 'storage' TYPE_META = 'meta' - CSV_HEADER = 'name,type,path,size,md5' + CSV_HEADER = 'name,type,path,size,indexed_at,maccess,md5' def __init__(self, debug=False, sortsize=False, arc=False): ''' @@ -313,6 +313,12 @@ class Noder: else: out.append('') + # indexed date/time + out.append(utils.epoch_to_str(storage.ts)) + + # maccess + out.append(utils.epoch_to_str(node.maccess)) + # md5 if any if node.md5: out.append(node.md5) @@ -551,6 +557,8 @@ class Noder: def _get_storage(self, node): '''recursively traverse up to find storage''' + if node.type == self.TYPE_STORAGE: + return node return node.ancestors[1] def _has_attr(self, node, attr): From f181f33753c5d29164af1cc2db6d9e94da20bd60 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 14 Jan 2022 10:39:24 +0100 Subject: [PATCH 03/14] refactor logging --- catcli/logger.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/catcli/logger.py b/catcli/logger.py index d9d1e3a..52d2943 100644 --- a/catcli/logger.py +++ b/catcli/logger.py @@ -39,6 +39,9 @@ class Logger: Logger.BOLD = '' Logger.UND = '' + def fix_badchars(line): + return line.encode('utf-8', 'ignore').decode('utf-8') + ###################################################################### # node specific output ###################################################################### @@ -49,14 +52,14 @@ class Logger: end = ' {}({}){}'.format(Logger.GRAY, attr, Logger.RESET) s = '{}{}{}{}:'.format(pre, Logger.UND, Logger.STORAGE, Logger.RESET) s += ' {}{}{}{}\n'.format(Logger.PURPLE, - _fix_badchars(name), + Logger.fix_badchars(name), Logger.RESET, end) s += ' {}{}{}'.format(Logger.GRAY, args, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def file(pre, name, attr): '''print a file node''' - s = '{}{}'.format(pre, _fix_badchars(name)) + s = '{}{}'.format(pre, Logger.fix_badchars(name)) s += ' {}[{}]{}'.format(Logger.GRAY, attr, Logger.RESET) sys.stdout.write('{}\n'.format(s)) @@ -70,13 +73,13 @@ class Logger: if end: end = ' [{}]'.format(', '.join(end)) s = '{}{}{}{}'.format(pre, Logger.BLUE, - _fix_badchars(name), Logger.RESET) + Logger.fix_badchars(name), Logger.RESET) s += '{}{}{}'.format(Logger.GRAY, end, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def arc(pre, name, archive): s = '{}{}{}{}'.format(pre, Logger.YELLOW, - _fix_badchars(name), Logger.RESET) + Logger.fix_badchars(name), Logger.RESET) s += ' {}[{}:{}]{}'.format(Logger.GRAY, Logger.ARCHIVE, archive, Logger.RESET) sys.stdout.write('{}\n'.format(s)) @@ -86,44 +89,44 @@ class Logger: ###################################################################### def out(string): '''to stdout no color''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) sys.stdout.write('{}\n'.format(string)) def out_err(string): '''to stderr no color''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) sys.stderr.write('{}\n'.format(string)) def debug(string): '''to stderr no color''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) sys.stderr.write('[DBG] {}\n'.format(string)) def info(string): '''to stdout in color''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) s = '{}{}{}'.format(Logger.MAGENTA, string, Logger.RESET) sys.stdout.write('{}\n'.format(s)) def err(string): '''to stderr in RED''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) s = '{}{}{}'.format(Logger.RED, string, Logger.RESET) sys.stderr.write('{}\n'.format(s)) def progr(string): '''print progress''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) sys.stderr.write('{}\r'.format(string)) sys.stderr.flush() def bold(string): '''make it bold''' - string = _fix_badchars(string) + string = Logger.fix_badchars(string) return '{}{}{}'.format(Logger.BOLD, string, Logger.RESET) def flog(path, string, append=True): - string = _fix_badchars(string) + string = Logger.fix_badchars(string) mode = 'w' if append: mode = 'a' @@ -131,5 +134,3 @@ class Logger: f.write(string) -def _fix_badchars(line): - return line.encode('utf-8', 'ignore').decode('utf-8') From cef572d7ebb323e28efa30ab7da9a252a2c0ffc6 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 14 Jan 2022 10:39:36 +0100 Subject: [PATCH 04/14] refactor storage args output --- catcli/noder.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/catcli/noder.py b/catcli/noder.py index c969909..4a97684 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -387,28 +387,28 @@ class Noder: nbchildren = len(node.children) freepercent = '{:.1f}%'.format( node.free * 100 / node.total - ).ljust(6) + ) # get the date dt = '' if self._has_attr(node, 'ts'): - dt = 'date: ' - dt += '{}'.format(utils.epoch_to_str(node.ts)).ljust(11) + dt = 'date:' + dt += '{}'.format(utils.epoch_to_str(node.ts)) ds = '' # the children size sz = self._rec_size(node, store=False) sz = utils.human(sz) - ds = 'totsize:' + '{}'.format(sz).ljust(7) + ds = 'totsize:' + '{}'.format(sz) # format the output name = '{}'.format(node.name) args = [ - 'nbfiles:' + '{}'.format(nbchildren).ljust(6), + 'nbfiles:' + '{}'.format(nbchildren), ds, 'free:{}'.format(freepercent), - 'du:' + '{}/{}'.format(hf, ht).ljust(14), + 'du:' + '{}/{}'.format(hf, ht), dt] Logger.storage(pre, - name.ljust(20), - '{}'.format(','.join(args)), + name, + '{}'.format(' | '.join(args)), node.attr) elif node.type == self.TYPE_ARC: # archive node From a7a7626b1361e2eaa3e9cd2c4523be8db0c03f5a Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 14 Jan 2022 10:39:59 +0100 Subject: [PATCH 05/14] linting --- catcli/logger.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/catcli/logger.py b/catcli/logger.py index 52d2943..9d7ab92 100644 --- a/catcli/logger.py +++ b/catcli/logger.py @@ -132,5 +132,3 @@ class Logger: mode = 'a' with open(path, mode) as f: f.write(string) - - From f5b20c023f74be2899ab63553e5446b8e3a5e6b3 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Fri, 14 Jan 2022 10:43:52 +0100 Subject: [PATCH 06/14] add CSV doc --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 2fe2854..cc79e3c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Features: * Store md5 hash of files * Ability to update the catalog * Tag your different storages with additional information + * export catalog to CSV @@ -74,6 +75,7 @@ See the [examples](#examples) for an overview of the available features. * [Catalog graph](#catalog-graph) * [Edit storage](#edit-storage) * [Update catalog](#update-catalog) + * [Export catalog](#export-catalog) * [Examples](#examples) * [Contribution](#contribution) @@ -206,6 +208,21 @@ Updates are based on the access time of each of the files and on the hash checksum if present (catalog was indexed with `-c --hash` and `update` is called with the switch `-c --hash`). +## Export catalog + +The catalog can be exported to CSV using the `export` command. +Fields are separated by a comma (`,`) and are quoted with double quotes (`"`). + +Each line format is `name,type,path,size,indexed_at,maccess,md5`. + +* **name**: the entry name +* **type**: the entry type (file, directory) +* **path**: the entry path +* **size**: the entry size +* **indexed_at**: when this entry was indexed +* **maccess**: the entry modification date/time +* **md5**: the entry checksum (if any) + # Examples ## Simple example From 1b2c52fb3e174b0b96dbf80b783d7862f6360c41 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 14:41:33 +0100 Subject: [PATCH 07/14] add csv format to ls and find --- catcli/catcli.py | 28 ++++++++++++++------- catcli/noder.py | 65 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/catcli/catcli.py b/catcli/catcli.py index 85fea0a..66a24ef 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -36,10 +36,10 @@ USAGE = """ {0} Usage: - {1} ls [--catalog=] [-aBCrVS] [] + {1} ls [--catalog=] [--format=] [-aBCrVS] [] {1} index [--catalog=] [--meta=...] [-aBCcfnV] {1} update [--catalog=] [-aBCcfnV] [--lpath=] - {1} find [--catalog=] [-aBCbdVP] [--path=] + {1} find [--catalog=] [--format=] [-aBCbdVP] [--path=] {1} rm [--catalog=] [-BCfV] {1} tree [--catalog=] [-aBCVS] [] {1} rename [--catalog=] [-BCfV] @@ -59,7 +59,7 @@ Options: -C --no-color Do not output colors [default: False]. -c --hash Calculate md5 hash [default: False]. -d --directory Only directory [default: False]. - -F --format= Export format [default: csv]. + -F --format= Export format [default: native]. -H --header Export with header [default: False]. -f --force Do not ask when updating the catalog [default: False]. -l --lpath= Path where changes are logged [default: ] @@ -71,7 +71,7 @@ Options: -V --verbose Be verbose [default: False]. -v --version Show version. -h --help Show this screen. -""".format(BANNER, NAME, CATALOGPATH) +""".format(BANNER, NAME, CATALOGPATH) # nopep8 def cmd_index(args, noder, catalog, top): @@ -146,7 +146,9 @@ def cmd_ls(args, noder, top): path += SEPARATOR if not path.endswith(WILD): path += WILD - found = noder.walk(top, path, rec=args['--recursive']) + found = noder.walk(top, path, + rec=args['--recursive'], + fmt=args['--format']) if not found: Logger.err('\"{}\": nothing found'.format(args[''])) return found @@ -168,9 +170,10 @@ def cmd_find(args, noder, top): fromtree = args['--parent'] directory = args['--directory'] startpath = args['--path'] + fmt = args['--format'] return noder.find_name(top, args[''], script=args['--script'], startpath=startpath, directory=directory, - parentfromtree=fromtree) + parentfromtree=fromtree, fmt=fmt) def cmd_tree(args, noder, top): @@ -214,10 +217,11 @@ def cmd_export(args, noder, catalog, top): header = args['--header'] fmt = args['--format'] - if fmt == 'csv': + if fmt == 'native': + # equivalent to tree + noder.print_tree(node) + elif fmt == 'csv': noder.to_csv(node, with_header=header) - else: - Logger.err('Format not supported: {}'.format(fmt)) def cmd_edit(args, noder, catalog, top): @@ -249,6 +253,12 @@ def main(): print(USAGE) return True + # check format + fmt = args['--format'] + if fmt != 'native' and fmt != 'csv': + Logger.err('bad format: {}'.format(fmt)) + return False + if args['--verbose']: print(args) diff --git a/catcli/noder.py b/catcli/noder.py index 4a97684..c14ad0f 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -325,7 +325,9 @@ class Noder: else: out.append('') - return sep.join(['"' + o + '"' for o in out]) + line = sep.join(['"' + o + '"' for o in out]) + if len(line) > 0: + Logger.out(line) def _print_node(self, node, pre='', withpath=False, withdepth=False, withstorage=False, @@ -430,9 +432,7 @@ class Noder: rend = anytree.RenderTree(node, childiter=self._sort_tree) for _, _, node in rend: - line = self._node_to_csv(node) - if len(line) > 0: - Logger.out(line) + self._node_to_csv(node) def to_dot(self, node, path='tree.dot'): '''export to dot for graphing''' @@ -445,8 +445,16 @@ class Noder: ############################################################### def find_name(self, root, key, script=False, directory=False, - startpath=None, parentfromtree=False): - '''find files based on their names''' + startpath=None, parentfromtree=False, + fmt='native'): + ''' + find files based on their names + @script: output script + @directory: only search for directories + @startpath: node to start with + @parentfromtree: get path from parent instead of stored relpath + @fmt: output format + ''' self._debug('searching for \"{}\"'.format(key)) start = root if startpath: @@ -461,16 +469,26 @@ class Noder: if directory and f.type != self.TYPE_DIR: # ignore non directory continue - self._print_node(f, withpath=True, withdepth=True, - withstorage=True, recalcparent=parentfromtree) + + # print the node + if fmt == 'native': + self._print_node(f, withpath=True, + withdepth=True, + withstorage=True, + recalcparent=parentfromtree) + elif fmt == 'csv': + self._node_to_csv(f) + if parentfromtree: paths.append(self._get_parents(f)) else: paths.append(f.relpath) + if script: tmp = ['${source}/' + x for x in paths] cmd = 'op=file; source=/media/mnt; $op {}'.format(' '.join(tmp)) Logger.info(cmd) + return found def _find_name(self, node): @@ -482,7 +500,7 @@ class Noder: ############################################################### # climbing ############################################################### - def walk(self, root, path, rec=False): + def walk(self, root, path, rec=False, fmt='native'): '''walk the tree for ls based on names''' self._debug('walking path: \"{}\"'.format(path)) r = anytree.resolver.Resolver('name') @@ -490,15 +508,36 @@ class Noder: try: found = r.glob(root, path) if len(found) < 1: + # nothing found return [] + if rec: - self.print_tree(found[0].parent) + # print the entire tree + if fmt == 'native': + self.print_tree(found[0].parent) + elif fmt == 'csv': + self.to_csv(found[0].parent) return found + + # sort found nodes found = sorted(found, key=self._sort, reverse=self.sortsize) - self._print_node(found[0].parent, - withpath=False, withdepth=True) + + # print the parent + if fmt == 'native': + self._print_node(found[0].parent, + withpath=False, withdepth=True) + elif fmt == 'csv': + self._node_to_csv(found[0].parent) + + # print all found nodes for f in found: - self._print_node(f, withpath=False, pre='- ', withdepth=True) + if fmt == 'native': + self._print_node(f, withpath=False, + pre='- ', + withdepth=True) + elif fmt == 'csv': + self._node_to_csv(f) + except anytree.resolver.ChildResolverError: pass return found From 47602fe32d8e449dcf2af341f386896f2843daf2 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 14:41:45 +0100 Subject: [PATCH 08/14] fix tests --- tests-requirements.txt | 2 +- tests.sh | 6 ++++-- tests/test_find.py | 3 ++- tests/test_ls.py | 3 ++- tests/test_rm.py | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests-requirements.txt b/tests-requirements.txt index 2795de2..151a565 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,5 +1,5 @@ pycodestyle; python_version >= '3.0' pyflakes; python_version >= '3.0' -nose; python_version >= '3.0' +nose-py3; python_version >= '3.0' coverage; python_version >= '3.0' coveralls; python_version >= '3.0' diff --git a/tests.sh b/tests.sh index 178ba7e..4513102 100755 --- a/tests.sh +++ b/tests.sh @@ -13,8 +13,10 @@ pycodestyle tests/ pyflakes catcli/ pyflakes tests/ -PYTHONPATH=catcli python3 -m nose -s --with-coverage --cover-package=catcli -#PYTHONPATH=catcli python3 -m nose -s +nosebin="nosetests" + +PYTHONPATH=catcli ${nosebin} -s --with-coverage --cover-package=catcli +#PYTHONPATH=catcli ${nosebin} -s for t in ${cur}/tests-ng/*; do echo "running test \"`basename ${t}`\"" diff --git a/tests/test_find.py b/tests/test_find.py index cb5a584..5daff0a 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -24,7 +24,8 @@ class TestFind(unittest.TestCase): # create fake args args = {'': '7544G', '--script': True, '--verbose': True, '--parent': False, - '--directory': False, '--path': None} + '--directory': False, '--path': None, + '--format': 'native'} # try to find something found = cmd_find(args, noder, top) diff --git a/tests/test_ls.py b/tests/test_ls.py index 7b2061a..5446531 100644 --- a/tests/test_ls.py +++ b/tests/test_ls.py @@ -25,7 +25,8 @@ class TestWalking(unittest.TestCase): # create fake args args = {'': '', '--recursive': False, - '--verbose': True} + '--verbose': True, + '--format': 'native'} # list root args[''] = '' diff --git a/tests/test_rm.py b/tests/test_rm.py index 81f96ef..356bc62 100644 --- a/tests/test_rm.py +++ b/tests/test_rm.py @@ -25,7 +25,8 @@ class TestRm(unittest.TestCase): # create fake args dict args = {'': '', '--recursive': False, - '--verbose': True} + '--verbose': True, + '--format': 'native'} # list files and make sure there are children args[''] = '' From ee8260b524870b863814f933d9601dd861a9ae0e Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 14:44:51 +0100 Subject: [PATCH 09/14] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc79e3c..2f2e4a1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Features: * Store md5 hash of files * Ability to update the catalog * Tag your different storages with additional information - * export catalog to CSV + * print results and export catalog to CSV From 1ac6f01b6cd73cb88faf06f9ca534a1d05665b4f Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 14:45:02 +0100 Subject: [PATCH 10/14] add print_supported_formats to list available formats --- catcli/catcli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/catcli/catcli.py b/catcli/catcli.py index 66a24ef..eb6b41e 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -46,6 +46,7 @@ Usage: {1} edit [--catalog=] [-BCfV] {1} graph [--catalog=] [-BCV] [] {1} export [--catalog=] [-BH] [--format=] [] + {1} print_supported_formats {1} help {1} --help {1} --version @@ -306,6 +307,9 @@ def main(): cmd_edit(args, noder, catalog, top) elif args['export']: cmd_export(args, noder, catalog, top) + elif args['print_supported_formats']: + print('"native": native format') + print('"csv" : CSV format') return True From a3ebd2d5a4b070a7467f0804aea3c9c77f101e50 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 14:46:48 +0100 Subject: [PATCH 11/14] revert to nose --- tests-requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests-requirements.txt b/tests-requirements.txt index 151a565..82bfba8 100644 --- a/tests-requirements.txt +++ b/tests-requirements.txt @@ -1,5 +1,6 @@ pycodestyle; python_version >= '3.0' pyflakes; python_version >= '3.0' -nose-py3; python_version >= '3.0' +#nose-py3; python_version >= '3.0' +nose; python_version >= '3.0' coverage; python_version >= '3.0' coveralls; python_version >= '3.0' From ecd15e66bd6319b39bec4362f5b25d5af00b4633 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 15:29:58 +0100 Subject: [PATCH 12/14] refactor and replace export with tree --- README.md | 6 ++-- catcli/catcli.py | 34 ++++++-------------- catcli/noder.py | 77 ++++++++++++++++++++++------------------------ tests/test_tree.py | 6 +++- 4 files changed, 54 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 2f2e4a1..9d303d1 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ See the [examples](#examples) for an overview of the available features. * [Catalog graph](#catalog-graph) * [Edit storage](#edit-storage) * [Update catalog](#update-catalog) - * [Export catalog](#export-catalog) + * [CSV format](#csv-format) * [Examples](#examples) * [Contribution](#contribution) @@ -208,9 +208,9 @@ Updates are based on the access time of each of the files and on the hash checksum if present (catalog was indexed with `-c --hash` and `update` is called with the switch `-c --hash`). -## Export catalog +## CSV format -The catalog can be exported to CSV using the `export` command. +Results can be printed to CSV using `--format=csv`. Fields are separated by a comma (`,`) and are quoted with double quotes (`"`). Each line format is `name,type,path,size,indexed_at,maccess,md5`. diff --git a/catcli/catcli.py b/catcli/catcli.py index eb6b41e..8ba27f7 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -37,15 +37,14 @@ USAGE = """ Usage: {1} ls [--catalog=] [--format=] [-aBCrVS] [] + {1} find [--catalog=] [--format=] [-aBCbdVP] [--path=] + {1} tree [--catalog=] [--format=] [-aBCVS] [] {1} index [--catalog=] [--meta=...] [-aBCcfnV] {1} update [--catalog=] [-aBCcfnV] [--lpath=] - {1} find [--catalog=] [--format=] [-aBCbdVP] [--path=] {1} rm [--catalog=] [-BCfV] - {1} tree [--catalog=] [-aBCVS] [] {1} rename [--catalog=] [-BCfV] {1} edit [--catalog=] [-BCfV] {1} graph [--catalog=] [-BCV] [] - {1} export [--catalog=] [-BH] [--format=] [] {1} print_supported_formats {1} help {1} --help @@ -60,8 +59,7 @@ Options: -C --no-color Do not output colors [default: False]. -c --hash Calculate md5 hash [default: False]. -d --directory Only directory [default: False]. - -F --format= Export format [default: native]. - -H --header Export with header [default: False]. + -F --format= Print format, see command \"print_supported_formats\" [default: native]. -f --force Do not ask when updating the catalog [default: False]. -l --lpath= Path where changes are logged [default: ] -n --no-subsize Do not store size of directories [default: False]. @@ -179,11 +177,16 @@ def cmd_find(args, noder, top): def cmd_tree(args, noder, top): path = args[''] + fmt = args['--format'] + + # find node to start with node = top if path: node = noder.get_node(top, path) + if node: - noder.print_tree(node) + # print the tree + noder.print_tree(node, fmt=fmt) def cmd_graph(args, noder, top): @@ -209,22 +212,6 @@ def cmd_rename(args, noder, catalog, top): return top -def cmd_export(args, noder, catalog, top): - path = args[''] - node = top - if path: - node = noder.get_node(top, path) - - header = args['--header'] - - fmt = args['--format'] - if fmt == 'native': - # equivalent to tree - noder.print_tree(node) - elif fmt == 'csv': - noder.to_csv(node, with_header=header) - - def cmd_edit(args, noder, catalog, top): storage = args[''] storages = list(x.name for x in top.children) @@ -305,11 +292,10 @@ def main(): cmd_rename(args, noder, catalog, top) elif args['edit']: cmd_edit(args, noder, catalog, top) - elif args['export']: - cmd_export(args, noder, catalog, top) elif args['print_supported_formats']: print('"native": native format') print('"csv" : CSV format') + print(' {}'.format(noder.CSV_HEADER)) return True diff --git a/catcli/noder.py b/catcli/noder.py index c14ad0f..f54e425 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -290,40 +290,38 @@ class Noder: return '' if node.type == self.TYPE_TOP: return '' - if node.type == self.TYPE_STORAGE: - return '' out = [] - - # node name - out.append(node.name) - - # node type - out.append(node.type) - - # node full path - parents = self._get_parents(node) - storage = self._get_storage(node) - fullpath = os.path.join(storage.name, parents) - out.append(fullpath) - - # size - if node.size: - out.append(utils.human(node.size)) + if node.type == self.TYPE_STORAGE: + # handle storage + out.append(node.name) + out.append(node.type) + out.append('') # full path + # size + sz = self._rec_size(node, store=False) + out.append(utils.human(sz)) + out.append(utils.epoch_to_str(node.ts)) + out.append('') # maccess + out.append('') # md5 else: - out.append('') + out.append(node.name) + out.append(node.type) - # indexed date/time - out.append(utils.epoch_to_str(storage.ts)) + # node full path + parents = self._get_parents(node) + storage = self._get_storage(node) + fullpath = os.path.join(storage.name, parents) + out.append(fullpath) - # maccess - out.append(utils.epoch_to_str(node.maccess)) + out.append(utils.human(node.size)) + out.append(utils.epoch_to_str(storage.ts)) + out.append(utils.epoch_to_str(node.maccess)) - # md5 if any - if node.md5: - out.append(node.md5) - else: - out.append('') + # md5 if any + if node.md5: + out.append(node.md5) + else: + out.append('') line = sep.join(['"' + o + '"' for o in out]) if len(line) > 0: @@ -419,17 +417,17 @@ class Noder: else: Logger.err('bad node encountered: {}'.format(node)) - def print_tree(self, node, style=anytree.ContRoundStyle()): + def print_tree(self, node, style=anytree.ContRoundStyle(), fmt='native'): '''print the tree similar to unix tool "tree"''' - rend = anytree.RenderTree(node, childiter=self._sort_tree) - for pre, fill, node in rend: - self._print_node(node, pre=pre, withdepth=True) - - def to_csv(self, node, with_header=False): + if fmt == 'native': + rend = anytree.RenderTree(node, childiter=self._sort_tree) + for pre, fill, node in rend: + self._print_node(node, pre=pre, withdepth=True) + elif fmt == 'csv': + self._to_csv(node) + + def _to_csv(self, node, with_header=True): '''print the tree to csv''' - if with_header: - Logger.out(self.CSV_HEADER) - rend = anytree.RenderTree(node, childiter=self._sort_tree) for _, _, node in rend: self._node_to_csv(node) @@ -513,10 +511,7 @@ class Noder: if rec: # print the entire tree - if fmt == 'native': - self.print_tree(found[0].parent) - elif fmt == 'csv': - self.to_csv(found[0].parent) + self.print_tree(found[0].parent, fmt=fmt) return found # sort found nodes diff --git a/tests/test_tree.py b/tests/test_tree.py index 66bb618..320ce23 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -24,7 +24,11 @@ class TestTree(unittest.TestCase): noder = Noder() # create fake args dict - args = {'': path, '--verbose': True} + args = { + '': path, + '--verbose': True, + '--format': 'native', + } # print tree and wait for any errors cmd_tree(args, noder, top) From 93f9cf560d1fb66c41b0b17ee043066093dca7a0 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Sat, 15 Jan 2022 15:34:02 +0100 Subject: [PATCH 13/14] refactor print_supported_formats --- catcli/catcli.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/catcli/catcli.py b/catcli/catcli.py index 8ba27f7..2f2f6ab 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -241,6 +241,12 @@ def main(): print(USAGE) return True + if args['print_supported_formats']: + print('"native": native format') + print('"csv" : CSV format') + print(' {}'.format(Noder.CSV_HEADER)) + return True + # check format fmt = args['--format'] if fmt != 'native' and fmt != 'csv': @@ -292,10 +298,6 @@ def main(): cmd_rename(args, noder, catalog, top) elif args['edit']: cmd_edit(args, noder, catalog, top) - elif args['print_supported_formats']: - print('"native": native format') - print('"csv" : CSV format') - print(' {}'.format(noder.CSV_HEADER)) return True From 56d40c12cc246eab3df30a54b8ed808a40742e88 Mon Sep 17 00:00:00 2001 From: deadc0de6 Date: Mon, 17 Jan 2022 22:31:37 +0100 Subject: [PATCH 14/14] adding header switch for tree --- catcli/catcli.py | 6 ++++-- catcli/noder.py | 25 ++++++++++++++++++++----- tests/test_tree.py | 1 + 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/catcli/catcli.py b/catcli/catcli.py index 2f2f6ab..ac39a23 100755 --- a/catcli/catcli.py +++ b/catcli/catcli.py @@ -38,7 +38,7 @@ USAGE = """ Usage: {1} ls [--catalog=] [--format=] [-aBCrVS] [] {1} find [--catalog=] [--format=] [-aBCbdVP] [--path=] - {1} tree [--catalog=] [--format=] [-aBCVS] [] + {1} tree [--catalog=] [--format=] [-aBCVSH] [] {1} index [--catalog=] [--meta=...] [-aBCcfnV] {1} update [--catalog=] [-aBCcfnV] [--lpath=] {1} rm [--catalog=] [-BCfV] @@ -61,6 +61,7 @@ Options: -d --directory Only directory [default: False]. -F --format= Print format, see command \"print_supported_formats\" [default: native]. -f --force Do not ask when updating the catalog [default: False]. + -H --header Print header on CSV format [default: False]. -l --lpath= Path where changes are logged [default: ] -n --no-subsize Do not store size of directories [default: False]. -P --parent Ignore stored relpath [default: True]. @@ -178,6 +179,7 @@ def cmd_find(args, noder, top): def cmd_tree(args, noder, top): path = args[''] fmt = args['--format'] + hdr = args['--header'] # find node to start with node = top @@ -186,7 +188,7 @@ def cmd_tree(args, noder, top): if node: # print the tree - noder.print_tree(node, fmt=fmt) + noder.print_tree(node, fmt=fmt, header=hdr) def cmd_graph(args, noder, top): diff --git a/catcli/noder.py b/catcli/noder.py index f54e425..0997b49 100644 --- a/catcli/noder.py +++ b/catcli/noder.py @@ -417,18 +417,27 @@ class Noder: else: Logger.err('bad node encountered: {}'.format(node)) - def print_tree(self, node, style=anytree.ContRoundStyle(), fmt='native'): - '''print the tree similar to unix tool "tree"''' + def print_tree(self, node, style=anytree.ContRoundStyle(), + fmt='native', header=False): + ''' + print the tree similar to unix tool "tree" + @node: start node + @style: when fmt=native, defines the tree style + @fmt: output format + @header: when fmt=csv, print the header + ''' if fmt == 'native': rend = anytree.RenderTree(node, childiter=self._sort_tree) for pre, fill, node in rend: self._print_node(node, pre=pre, withdepth=True) elif fmt == 'csv': - self._to_csv(node) + self._to_csv(node, with_header=header) - def _to_csv(self, node, with_header=True): + def _to_csv(self, node, with_header=False): '''print the tree to csv''' rend = anytree.RenderTree(node, childiter=self._sort_tree) + if with_header: + Logger.out(self.CSV_HEADER) for _, _, node in rend: self._node_to_csv(node) @@ -499,8 +508,14 @@ class Noder: # climbing ############################################################### def walk(self, root, path, rec=False, fmt='native'): - '''walk the tree for ls based on names''' + ''' + walk the tree for ls based on names + @root: start node + @rec: recursive walk + @fmt: output format + ''' self._debug('walking path: \"{}\"'.format(path)) + r = anytree.resolver.Resolver('name') found = [] try: diff --git a/tests/test_tree.py b/tests/test_tree.py index 320ce23..46cfe4f 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -28,6 +28,7 @@ class TestTree(unittest.TestCase): '': path, '--verbose': True, '--format': 'native', + '--header': False, } # print tree and wait for any errors