1"""Common code for listing files from a bup repository."""
2
3from __future__ import absolute_import
4from binascii import hexlify
5from itertools import chain
6from stat import S_ISDIR, S_ISLNK
7import copy, locale, os.path, stat, sys
8
9from bup import metadata, options, vfs, xstat
10from bup.compat import argv_bytes
11from bup.io import path_msg
12from bup.options import Options
13from bup.repo import LocalRepo, RemoteRepo
14from bup.helpers import columnate, istty1, last, log
15
16def item_hash(item, tree_for_commit):
17    """If the item is a Commit, return its commit oid, otherwise return
18    the item's oid, if it has one.
19
20    """
21    if tree_for_commit and isinstance(item, vfs.Commit):
22        return item.coid
23    return getattr(item, 'oid', None)
24
25def item_info(item, name,
26              show_hash = False,
27              commit_hash=False,
28              long_fmt = False,
29              classification = None,
30              numeric_ids = False,
31              human_readable = False):
32    """Return bytes containing the information to display for the VFS
33    item.  Classification may be "all", "type", or None.
34
35    """
36    result = b''
37    if show_hash:
38        oid = item_hash(item, commit_hash)
39        result += b'%s ' % (hexlify(oid) if oid
40                            else b'0000000000000000000000000000000000000000')
41    if long_fmt:
42        meta = item.meta.copy()
43        meta.path = name
44        # FIXME: need some way to track fake vs real meta items?
45        result += metadata.summary_bytes(meta,
46                                         numeric_ids=numeric_ids,
47                                         classification=classification,
48                                         human_readable=human_readable)
49    else:
50        result += name
51        if classification:
52            cls = xstat.classification_str(vfs.item_mode(item),
53                                           classification == 'all')
54            result += cls.encode('ascii')
55    return result
56
57
58optspec = """
59bup ls [-r host:path] [-l] [-d] [-F] [-a] [-A] [-s] [-n] [path...]
60--
61r,remote=   remote repository path
62s,hash   show hash for each file
63commit-hash show commit hash instead of tree for commits (implies -s)
64a,all    show hidden files
65A,almost-all    show hidden files except . and ..
66l        use a detailed, long listing format
67d,directory show directories, not contents; don't follow symlinks
68F,classify append type indicator: dir/ sym@ fifo| sock= exec*
69file-type append type indicator: dir/ sym@ fifo| sock=
70human-readable    print human readable file sizes (i.e. 3.9K, 4.7M)
71n,numeric-ids list numeric IDs (user, group, etc.) rather than names
72"""
73
74def opts_from_cmdline(args, onabort=None):
75    """Parse ls command line arguments and return a dictionary of ls
76    options, agumented with "classification", "long_listing",
77    "paths", and "show_hidden".
78
79    """
80    if onabort:
81        opt, flags, extra = Options(optspec, onabort=onabort).parse(args)
82    else:
83        opt, flags, extra = Options(optspec).parse(args)
84
85    opt.paths = [argv_bytes(x) for x in extra] or (b'/',)
86    opt.long_listing = opt.l
87    opt.classification = None
88    opt.show_hidden = None
89    for flag in flags:
90        option, parameter = flag
91        if option in ('-F', '--classify'):
92            opt.classification = 'all'
93        elif option == '--file-type':
94            opt.classification = 'type'
95        elif option in ('-a', '--all'):
96            opt.show_hidden = 'all'
97        elif option in ('-A', '--almost-all'):
98            opt.show_hidden = 'almost'
99    return opt
100
101def within_repo(repo, opt, out):
102
103    if opt.commit_hash:
104        opt.hash = True
105
106    def item_line(item, name):
107        return item_info(item, name,
108                         show_hash=opt.hash,
109                         commit_hash=opt.commit_hash,
110                         long_fmt=opt.long_listing,
111                         classification=opt.classification,
112                         numeric_ids=opt.numeric_ids,
113                         human_readable=opt.human_readable)
114
115    ret = 0
116    pending = []
117    for path in opt.paths:
118        try:
119            if opt.directory:
120                resolved = vfs.resolve(repo, path, follow=False)
121            else:
122                resolved = vfs.try_resolve(repo, path)
123
124            leaf_name, leaf_item = resolved[-1]
125            if not leaf_item:
126                log('error: cannot access %r in %r\n'
127                    % ('/'.join(path_msg(name) for name, item in resolved),
128                       path_msg(path)))
129                ret = 1
130                continue
131            if not opt.directory and S_ISDIR(vfs.item_mode(leaf_item)):
132                items = vfs.contents(repo, leaf_item)
133                if opt.show_hidden == 'all':
134                    # Match non-bup "ls -a ... /".
135                    parent = resolved[-2] if len(resolved) > 1 else resolved[0]
136                    items = chain(items, ((b'..', parent[1]),))
137                for sub_name, sub_item in sorted(items, key=lambda x: x[0]):
138                    if opt.show_hidden != 'all' and sub_name == b'.':
139                        continue
140                    if sub_name.startswith(b'.') and \
141                       opt.show_hidden not in ('almost', 'all'):
142                        continue
143                    if opt.l:
144                        sub_item = vfs.ensure_item_has_metadata(repo, sub_item,
145                                                                include_size=True)
146                    else:
147                        sub_item = vfs.augment_item_meta(repo, sub_item,
148                                                         include_size=True)
149                    line = item_line(sub_item, sub_name)
150                    if not opt.long_listing and istty1:
151                        pending.append(line)
152                    else:
153                        out.write(line)
154                        out.write(b'\n')
155            else:
156                leaf_item = vfs.augment_item_meta(repo, leaf_item,
157                                                  include_size=True)
158                line = item_line(leaf_item, os.path.normpath(path))
159                if not opt.long_listing and istty1:
160                    pending.append(line)
161                else:
162                    out.write(line)
163                    out.write(b'\n')
164        except vfs.IOError as ex:
165            log('bup: %s\n' % ex)
166            ret = 1
167
168    if pending:
169        out.write(columnate(pending, b''))
170
171    return ret
172
173def via_cmdline(args, out=None, onabort=None):
174    """Write a listing of a file or directory in the bup repository to out.
175
176    When a long listing is not requested and stdout is attached to a
177    tty, the output is formatted in columns. When not attached to tty
178    (for example when the output is piped to another command), one
179    file is listed per line.
180
181    """
182    assert out
183    opt = opts_from_cmdline(args, onabort=onabort)
184    repo = RemoteRepo(argv_bytes(opt.remote)) if opt.remote else LocalRepo()
185    return within_repo(repo, opt, out)
186