• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

deadc0de6 / catcli / 4399176145

pending completion
4399176145

Pull #30

github

GitHub
Merge ee2cf80d9 into 7590ad02c
Pull Request #30: Fuse

474 of 474 new or added lines in 13 files covered. (100.0%)

1304 of 1691 relevant lines covered (77.11%)

3.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.09
/catcli/catcli.py
1
#!/usr/bin/env python3
2
# author: deadc0de6
3

4
"""
5✔
5
author: deadc0de6 (https://github.com/deadc0de6)
6
Copyright (c) 2017, deadc0de6
7

8
Catcli command line interface
9
"""
10

11
import sys
5✔
12
import os
5✔
13
import datetime
5✔
14
from typing import Dict, Any, List
5✔
15
from docopt import docopt
5✔
16

17
# local imports
18
from catcli.version import __version__ as VERSION
5✔
19
from catcli.nodes import NodeTop, NodeAny
5✔
20
from catcli.logger import Logger
5✔
21
from catcli.colors import Colors
5✔
22
from catcli.catalog import Catalog
5✔
23
from catcli.walker import Walker
5✔
24
from catcli.noder import Noder
5✔
25
from catcli.utils import ask, edit, path_to_search_all
5✔
26
from catcli.fuser import Fuser
5✔
27
from catcli.exceptions import BadFormatException, CatcliException
5✔
28

29
NAME = 'catcli'
5✔
30
CUR = os.path.dirname(os.path.abspath(__file__))
5✔
31
CATALOGPATH = f'{NAME}.catalog'
5✔
32
GRAPHPATH = f'/tmp/{NAME}.dot'
5✔
33
FORMATS = ['native', 'csv', 'csv-with-header', 'fzf-native', 'fzf-csv']
5✔
34

35
BANNER = f""" +-+-+-+-+-+-+
5✔
36
 |c|a|t|c|l|i|
37
 +-+-+-+-+-+-+ v{VERSION}"""
38

39
USAGE = f"""
5✔
40
{BANNER}
41

42
Usage:
43
    {NAME} ls     [--catalog=<path>] [--format=<fmt>] [-aBCrVSs] [<path>]
44
    {NAME} find   [--catalog=<path>] [--format=<fmt>]
45
                  [-aBCbdVsP] [--path=<path>] [<term>]
46
    {NAME} index  [--catalog=<path>] [--meta=<meta>...]
47
                  [-aBCcfnV] <name> <path>
48
    {NAME} update [--catalog=<path>] [-aBCcfnV] [--lpath=<path>] <name> <path>
49
    {NAME} mount  [--catalog=<path>] [-V] <mountpoint>
50
    {NAME} rm     [--catalog=<path>] [-BCfV] <storage>
51
    {NAME} rename [--catalog=<path>] [-BCfV] <storage> <name>
52
    {NAME} edit   [--catalog=<path>] [-BCfV] <storage>
53
    {NAME} graph  [--catalog=<path>] [-BCV] [<path>]
54
    {NAME} print_supported_formats
55
    {NAME} help
56
    {NAME} --help
57
    {NAME} --version
58

59
Options:
60
    --catalog=<path>    Path to the catalog [default: {CATALOGPATH}].
61
    --meta=<meta>       Additional attribute to store [default: ].
62
    -a --archive        Handle archive file [default: False].
63
    -B --no-banner      Do not display the banner [default: False].
64
    -b --script         Output script to manage found file(s) [default: False].
65
    -C --no-color       Do not output colors [default: False].
66
    -c --hash           Calculate md5 hash [default: False].
67
    -d --directory      Only directory [default: False].
68
    -F --format=<fmt>   see \"print_supported_formats\" [default: native].
69
    -f --force          Do not ask when updating the catalog [default: False].
70
    -l --lpath=<path>   Path where changes are logged [default: ]
71
    -n --no-subsize     Do not store size of directories [default: False].
72
    -P --parent         Ignore stored relpath [default: True].
73
    -p --path=<path>    Start path.
74
    -r --recursive      Recursive [default: False].
75
    -s --raw-size       Print raw size [default: False].
76
    -S --sortsize       Sort by size, largest first [default: False].
77
    -V --verbose        Be verbose [default: False].
78
    -v --version        Show version.
79
    -h --help           Show this screen.
80
"""  # nopep8
81

82

83
def cmd_mount(args: Dict[str, Any],
5✔
84
              top: NodeTop,
85
              noder: Noder) -> None:
86
    """mount action"""
87
    mountpoint = args['<mountpoint>']
×
88
    debug = args['--verbose']
×
89
    Fuser(mountpoint, top, noder,
×
90
          debug=debug)
91

92

93
def cmd_index(args: Dict[str, Any],
5✔
94
              noder: Noder,
95
              catalog: Catalog,
96
              top: NodeTop) -> None:
97
    """index action"""
98
    path = args['<path>']
5✔
99
    name = args['<name>']
5✔
100
    usehash = args['--hash']
5✔
101
    debug = args['--verbose']
5✔
102
    subsize = not args['--no-subsize']
5✔
103
    if not os.path.exists(path):
5✔
104
        Logger.err(f'\"{path}\" does not exist')
×
105
        return
×
106
    if name in noder.get_storage_names(top):
5✔
107
        try:
×
108
            if not ask(f'Overwrite storage \"{name}\"'):
×
109
                Logger.err(f'storage named \"{name}\" already exist')
×
110
                return
×
111
        except KeyboardInterrupt:
×
112
            Logger.err('aborted')
×
113
            return
×
114
        node = noder.get_storage_node(top, name)
×
115
        node.parent = None
×
116

117
    start = datetime.datetime.now()
5✔
118
    walker = Walker(noder, usehash=usehash, debug=debug)
5✔
119
    attr = args['--meta']
5✔
120
    root = noder.new_storage_node(name, path, top, attr)
5✔
121
    _, cnt = walker.index(path, root, name)
5✔
122
    if subsize:
5✔
123
        noder.rec_size(root, store=True)
5✔
124
    stop = datetime.datetime.now()
5✔
125
    diff = stop - start
5✔
126
    Logger.info(f'Indexed {cnt} file(s) in {diff}')
5✔
127
    if cnt > 0:
5✔
128
        catalog.save(top)
5✔
129

130

131
def cmd_update(args: Dict[str, Any],
5✔
132
               noder: Noder,
133
               catalog: Catalog,
134
               top: NodeTop) -> None:
135
    """update action"""
136
    path = args['<path>']
5✔
137
    name = args['<name>']
5✔
138
    usehash = args['--hash']
5✔
139
    logpath = args['--lpath']
5✔
140
    debug = args['--verbose']
5✔
141
    subsize = not args['--no-subsize']
5✔
142
    if not os.path.exists(path):
5✔
143
        Logger.err(f'\"{path}\" does not exist')
×
144
        return
×
145
    root = noder.get_storage_node(top, name, newpath=path)
5✔
146
    if not root:
5✔
147
        Logger.err(f'storage named \"{name}\" does not exist')
×
148
        return
×
149
    start = datetime.datetime.now()
5✔
150
    walker = Walker(noder, usehash=usehash, debug=debug,
5✔
151
                    logpath=logpath)
152
    cnt = walker.reindex(path, root, top)
5✔
153
    if subsize:
5✔
154
        noder.rec_size(root, store=True)
5✔
155
    stop = datetime.datetime.now()
5✔
156
    diff = stop - start
5✔
157
    Logger.info(f'updated {cnt} file(s) in {diff}')
5✔
158
    if cnt > 0:
5✔
159
        catalog.save(top)
5✔
160

161

162
def cmd_ls(args: Dict[str, Any],
5✔
163
           noder: Noder,
164
           top: NodeTop) -> List[NodeAny]:
165
    """ls action"""
166
    path = path_to_search_all(args['<path>'])
5✔
167
    fmt = args['--format']
5✔
168
    if fmt.startswith('fzf'):
5✔
169
        raise BadFormatException('fzf is not supported in ls, use find')
×
170
    found = noder.list(top,
5✔
171
                       path,
172
                       rec=args['--recursive'],
173
                       fmt=fmt,
174
                       raw=args['--raw-size'])
175
    if not found:
5✔
176
        path = args['<path>']
5✔
177
        Logger.err(f'\"{path}\": nothing found')
5✔
178
    return found
5✔
179

180

181
def cmd_rm(args: Dict[str, Any],
5✔
182
           noder: Noder,
183
           catalog: Catalog,
184
           top: NodeTop) -> NodeTop:
185
    """rm action"""
186
    name = args['<storage>']
5✔
187
    node = noder.get_storage_node(top, name)
5✔
188
    if node:
5✔
189
        node.parent = None
5✔
190
        if catalog.save(top):
5✔
191
            Logger.info(f'Storage \"{name}\" removed')
5✔
192
    else:
193
        Logger.err(f'Storage named \"{name}\" does not exist')
5✔
194
    return top
5✔
195

196

197
def cmd_find(args: Dict[str, Any],
5✔
198
             noder: Noder,
199
             top: NodeTop) -> List[NodeAny]:
200
    """find action"""
201
    fromtree = args['--parent']
5✔
202
    directory = args['--directory']
5✔
203
    startpath = args['--path']
5✔
204
    fmt = args['--format']
5✔
205
    raw = args['--raw-size']
5✔
206
    script = args['--script']
5✔
207
    search_for = args['<term>']
5✔
208
    found = noder.find_name(top, search_for,
5✔
209
                            script=script,
210
                            startnode=startpath,
211
                            only_dir=directory,
212
                            parentfromtree=fromtree,
213
                            fmt=fmt, raw=raw)
214
    return found
5✔
215

216

217
def cmd_graph(args: Dict[str, Any],
5✔
218
              noder: Noder,
219
              top: NodeTop) -> None:
220
    """graph action"""
221
    path = args['<path>']
5✔
222
    if not path:
5✔
223
        path = GRAPHPATH
×
224
    cmd = noder.to_dot(top, path)
5✔
225
    Logger.info(f'create graph with \"{cmd}\" (you need graphviz)')
5✔
226

227

228
def cmd_rename(args: Dict[str, Any],
5✔
229
               catalog: Catalog,
230
               top: NodeTop) -> None:
231
    """rename action"""
232
    storage = args['<storage>']
×
233
    new = args['<name>']
×
234
    storages = list(x.name for x in top.children)
×
235
    if storage in storages:
×
236
        node = next(filter(lambda x: x.name == storage, top.children))
×
237
        node.name = new
×
238
        if catalog.save(top):
×
239
            msg = f'Storage \"{storage}\" renamed to \"{new}\"'
×
240
            Logger.info(msg)
×
241
    else:
242
        Logger.err(f'Storage named \"{storage}\" does not exist')
×
243

244

245
def cmd_edit(args: Dict[str, Any],
5✔
246
             noder: Noder,
247
             catalog: Catalog,
248
             top: NodeTop) -> None:
249
    """edit action"""
250
    storage = args['<storage>']
×
251
    storages = list(x.name for x in top.children)
×
252
    if storage in storages:
×
253
        node = next(filter(lambda x: x.name == storage, top.children))
×
254
        attr = node.attr
×
255
        if not attr:
×
256
            attr = ''
×
257
        new = edit(attr)
×
258
        node.attr = noder.attrs_to_string(new)
×
259
        if catalog.save(top):
×
260
            Logger.info(f'Storage \"{storage}\" edited')
×
261
    else:
262
        Logger.err(f'Storage named \"{storage}\" does not exist')
×
263

264

265
def banner() -> None:
5✔
266
    """print banner"""
267
    Logger.stderr_nocolor(BANNER)
×
268
    Logger.stderr_nocolor("")
×
269

270

271
def print_supported_formats() -> None:
5✔
272
    """print all supported formats to stdout"""
273
    print('"native"     : native format')
×
274
    print('"csv"        : CSV format')
×
275
    print(f'               {Noder.CSV_HEADER}')
×
276
    print('"fzf-native" : fzf to native (only valid for find)')
×
277
    print('"fzf-csv"    : fzf to csv (only valid for find)')
×
278

279

280
def main() -> bool:
5✔
281
    """entry point"""
282
    args = docopt(USAGE, version=VERSION)
5✔
283

284
    if args['help'] or args['--help']:
5✔
285
        print(USAGE)
×
286
        return True
×
287

288
    if args['print_supported_formats']:
5✔
289
        print_supported_formats()
×
290
        return True
×
291

292
    # check format
293
    fmt = args['--format']
5✔
294
    if fmt not in FORMATS:
5✔
295
        Logger.err(f'bad format: {fmt}')
×
296
        print_supported_formats()
×
297
        return False
×
298

299
    if args['--verbose']:
5✔
300
        print(args)
×
301

302
    # print banner
303
    if not args['--no-banner']:
5✔
304
        banner()
×
305

306
    # set colors
307
    if args['--no-color']:
5✔
308
        Colors.no_color()
×
309

310
    # init noder
311
    noder = Noder(debug=args['--verbose'], sortsize=args['--sortsize'],
5✔
312
                  arc=args['--archive'])
313
    # init catalog
314
    catalog_path = args['--catalog']
5✔
315
    catalog = Catalog(catalog_path, debug=args['--verbose'],
5✔
316
                      force=args['--force'])
317
    # init top node
318
    top = catalog.restore()
5✔
319
    if not top:
5✔
320
        top = noder.new_top_node()
5✔
321

322
    # handle the meta node
323
    meta = noder.update_metanode(top)
5✔
324
    catalog.set_metanode(meta)
5✔
325

326
    # parse command
327
    try:
5✔
328
        if args['index']:
5✔
329
            cmd_index(args, noder, catalog, top)
5✔
330
        if args['update']:
5✔
331
            if not catalog.exists():
5✔
332
                Logger.err(f'no such catalog: {catalog_path}')
×
333
                return False
×
334
            cmd_update(args, noder, catalog, top)
5✔
335
        elif args['find']:
5✔
336
            if not catalog.exists():
×
337
                Logger.err(f'no such catalog: {catalog_path}')
×
338
                return False
×
339
            cmd_find(args, noder, top)
×
340
        elif args['ls']:
5✔
341
            if not catalog.exists():
5✔
342
                Logger.err(f'no such catalog: {catalog_path}')
×
343
                return False
×
344
            cmd_ls(args, noder, top)
5✔
345
        elif args['mount']:
5✔
346
            if not catalog.exists():
×
347
                Logger.err(f'no such catalog: {catalog_path}')
×
348
                return False
×
349
            cmd_mount(args, top, noder)
×
350
        elif args['rm']:
5✔
351
            if not catalog.exists():
×
352
                Logger.err(f'no such catalog: {catalog_path}')
×
353
                return False
×
354
            cmd_rm(args, noder, catalog, top)
×
355
        elif args['graph']:
5✔
356
            if not catalog.exists():
×
357
                Logger.err(f'no such catalog: {catalog_path}')
×
358
                return False
×
359
            cmd_graph(args, noder, top)
×
360
        elif args['rename']:
5✔
361
            if not catalog.exists():
×
362
                Logger.err(f'no such catalog: {catalog_path}')
×
363
                return False
×
364
            cmd_rename(args, catalog, top)
×
365
        elif args['edit']:
5✔
366
            if not catalog.exists():
×
367
                Logger.err(f'no such catalog: {catalog_path}')
×
368
                return False
×
369
            cmd_edit(args, noder, catalog, top)
×
370
    except CatcliException as exc:
×
371
        Logger.stderr_nocolor('ERROR ' + str(exc))
×
372
        return False
×
373

374
    return True
5✔
375

376

377
if __name__ == '__main__':
5✔
378
    if main():
5✔
379
        sys.exit(0)
5✔
380
    sys.exit(1)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc