]> arthur.barton.de Git - bup.git/blob - cmd/bup
tests: fuse: use stat instead of relying on "ls -l" output
[bup.git] / cmd / bup
1 #!/bin/sh
2 """": # -*-python-*- # -*-python-*-
3 set -e
4 top="$(pwd)"
5 cmdpath="$0"
6 # loop because macos doesn't have recursive readlink/realpath utils
7 while test -L "$cmdpath"; do
8     link="$(readlink "$cmdpath")"
9     cd "$(dirname "$cmdpath")"
10     cmdpath="$link"
11 done
12 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
13 cd "$top"
14 exec "$script_home/bup-python" "$0" ${1+"$@"}
15 """
16 # end of bup preamble
17
18 from __future__ import absolute_import, print_function
19 import errno, re, sys, os, subprocess, signal, getopt
20
21 if sys.version_info[0] != 2 \
22    and not os.environ.get('BUP_ALLOW_UNEXPECTED_PYTHON_VERSION') == 'true':
23     print('error: bup may crash with python versions other than 2, or eat your data',
24           file=sys.stderr)
25     sys.exit(2)
26
27 from subprocess import PIPE
28 from sys import stderr, stdout
29 import select
30
31 from bup import compat, path, helpers
32 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
33 from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width
34
35 cmdpath = path.cmddir()
36
37 def usage(msg=""):
38     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
39         '<command> [options...]\n\n')
40     common = dict(
41         ftp = 'Browse backup sets using an ftp-like client',
42         fsck = 'Check backup sets for damage and add redundancy information',
43         fuse = 'Mount your backup sets as a filesystem',
44         help = 'Print detailed help for the given command',
45         index = 'Create or display the index of files to back up',
46         on = 'Backup a remote machine to the local one',
47         restore = 'Extract files from a backup set',
48         save = 'Save files into a backup set (note: run "bup index" first)',
49         tag = 'Tag commits for easier access',
50         web = 'Launch a web server to examine backup sets',
51     )
52
53     log('Common commands:\n')
54     for cmd,synopsis in sorted(common.items()):
55         log('    %-10s %s\n' % (cmd, synopsis))
56     log('\n')
57     
58     log('Other available commands:\n')
59     cmds = []
60     for c in sorted(os.listdir(cmdpath)):
61         if c.startswith('bup-') and c.find('.') < 0:
62             cname = c[4:]
63             if cname not in common:
64                 cmds.append(c[4:])
65     log(columnate(cmds, '    '))
66     log('\n')
67     
68     log("See 'bup help COMMAND' for more information on " +
69         "a specific command.\n")
70     if msg:
71         log("\n%s\n" % msg)
72     sys.exit(99)
73
74
75 if len(sys.argv) < 2:
76     usage()
77
78 # Handle global options.
79 try:
80     optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=']
81     global_args, subcmd = getopt.getopt(sys.argv[1:], '?VDd:', optspec)
82 except getopt.GetoptError as ex:
83     usage('error: %s' % ex.msg)
84
85 help_requested = None
86 do_profile = False
87
88 for opt in global_args:
89     if opt[0] in ['-?', '--help']:
90         help_requested = True
91     elif opt[0] in ['-V', '--version']:
92         subcmd = ['version']
93     elif opt[0] in ['-D', '--debug']:
94         helpers.buglvl += 1
95         os.environ['BUP_DEBUG'] = str(helpers.buglvl)
96     elif opt[0] in ['--profile']:
97         do_profile = True
98     elif opt[0] in ['-d', '--bup-dir']:
99         os.environ['BUP_DIR'] = opt[1]
100     else:
101         usage('error: unexpected option "%s"' % opt[0])
102
103 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
104 if 'BUP_DIR' in os.environ:
105     os.environ['BUP_DIR'] = os.path.abspath(os.environ['BUP_DIR'])
106
107 if len(subcmd) == 0:
108     if help_requested:
109         subcmd = ['help']
110     else:
111         usage()
112
113 if help_requested and subcmd[0] != 'help':
114     subcmd = ['help'] + subcmd
115
116 if len(subcmd) > 1 and subcmd[1] == '--help' and subcmd[0] != 'help':
117     subcmd = ['help', subcmd[0]] + subcmd[2:]
118
119 subcmd_name = subcmd[0]
120 if not subcmd_name:
121     usage()
122
123 def subpath(s):
124     return os.path.join(cmdpath, 'bup-%s' % s)
125
126 subcmd[0] = subpath(subcmd_name)
127 if not os.path.exists(subcmd[0]):
128     usage('error: unknown command "%s"' % subcmd_name)
129
130 already_fixed = atoi(os.environ.get('BUP_FORCE_TTY'))
131 if subcmd_name in ['mux', 'ftp', 'help']:
132     already_fixed = True
133 fix_stdout = not already_fixed and os.isatty(1)
134 fix_stderr = not already_fixed and os.isatty(2)
135
136 if fix_stdout or fix_stderr:
137     tty_env = merge_dict(os.environ,
138                          {'BUP_FORCE_TTY': str((fix_stdout and 1 or 0)
139                                                + (fix_stderr and 2 or 0))})
140 else:
141     tty_env = os.environ
142
143
144 sep_rx = re.compile(br'([\r\n])')
145
146 def print_clean_line(dest, content, width, sep=None):
147     """Write some or all of content, followed by sep, to the dest fd after
148     padding the content with enough spaces to fill the current
149     terminal width or truncating it to the terminal width if sep is a
150     carriage return."""
151     global sep_rx
152     assert sep in (b'\r', b'\n', None)
153     if not content:
154         if sep:
155             os.write(dest, sep)
156         return
157     for x in content:
158         assert not sep_rx.match(x)
159     content = b''.join(content)
160     if sep == b'\r' and len(content) > width:
161         content = content[width:]
162     os.write(dest, content)
163     if len(content) < width:
164         os.write(dest, b' ' * (width - len(content)))
165     if sep:
166         os.write(dest, sep)
167
168 def filter_output(src_out, src_err, dest_out, dest_err):
169     """Transfer data from src_out to dest_out and src_err to dest_err via
170     print_clean_line until src_out and src_err close."""
171     global sep_rx
172     assert not isinstance(src_out, bool)
173     assert not isinstance(src_err, bool)
174     assert not isinstance(dest_out, bool)
175     assert not isinstance(dest_err, bool)
176     assert src_out is not None or src_err is not None
177     assert (src_out is None) == (dest_out is None)
178     assert (src_err is None) == (dest_err is None)
179     pending = {}
180     pending_ex = None
181     try:
182         fds = tuple([x for x in (src_out, src_err) if x is not None])
183         while fds:
184             ready_fds, _, _ = select.select(fds, [], [])
185             width = tty_width()
186             for fd in ready_fds:
187                 buf = os.read(fd, 4096)
188                 dest = dest_out if fd == src_out else dest_err
189                 if not buf:
190                     fds = tuple([x for x in fds if x is not fd])
191                     print_clean_line(dest, pending.pop(fd, []), width)
192                 else:
193                     split = sep_rx.split(buf)
194                     while len(split) > 1:
195                         content, sep = split[:2]
196                         split = split[2:]
197                         print_clean_line(dest,
198                                          pending.pop(fd, []) + [content],
199                                          width,
200                                          sep)
201                     assert(len(split) == 1)
202                     if split[0]:
203                         pending.setdefault(fd, []).extend(split)
204     except BaseException as ex:
205         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
206     try:
207         # Try to finish each of the streams
208         for fd, pending_items in compat.items(pending):
209             dest = dest_out if fd == src_out else dest_err
210             try:
211                 print_clean_line(dest, pending_items, width)
212             except (EnvironmentError, EOFError) as ex:
213                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
214     except BaseException as ex:
215         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
216     if pending_ex:
217         raise pending_ex
218
219 def run_subcmd(subcmd):
220
221     c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd
222     if not (fix_stdout or fix_stderr):
223         os.execvp(c[0], c)
224
225     p = None
226     try:
227         p = subprocess.Popen(c,
228                              stdout=PIPE if fix_stdout else sys.stdout,
229                              stderr=PIPE if fix_stderr else sys.stderr,
230                              env=tty_env, bufsize=4096, close_fds=True)
231         # Assume p will receive these signals and quit, which will
232         # then cause us to quit.
233         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
234             signal.signal(sig, signal.SIG_IGN)
235
236         filter_output(fix_stdout and p.stdout.fileno() or None,
237                       fix_stderr and p.stderr.fileno() or None,
238                       fix_stdout and sys.stdout.fileno() or None,
239                       fix_stderr and sys.stderr.fileno() or None)
240         return p.wait()
241     except BaseException as ex:
242         add_ex_tb(ex)
243         try:
244             if p and p.poll() == None:
245                 os.kill(p.pid, signal.SIGTERM)
246                 p.wait()
247         except BaseException as kill_ex:
248             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
249         raise ex
250         
251 wrap_main(lambda : run_subcmd(subcmd))