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