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