]> arthur.barton.de Git - bup.git/blob - main.py
Update base_version to 0.34~ for 0.34 development
[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, chain_ex, wrap_main
46 from bup.helpers import atoi, columnate, debug1, log, 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 def force_tty():
152     if fix_stdout or fix_stderr:
153         amt = (fix_stdout and 1 or 0) + (fix_stderr and 2 or 0)
154         os.environ['BUP_FORCE_TTY'] = str(amt)
155
156
157 sep_rx = re.compile(r'([\r\n])')
158
159 def print_clean_line(dest, content, width, sep=None):
160     """Write some or all of content, followed by sep, to the dest fd after
161     padding the content with enough spaces to fill the current
162     terminal width or truncating it to the terminal width if sep is a
163     carriage return."""
164     global sep_rx
165     assert sep in ('\r', '\n', None)
166     if not content:
167         if sep:
168             os.write(dest, sep)
169         return
170     for x in content:
171         assert not sep_rx.match(x)
172     content = ''.join(content)
173     if sep == '\r' and len(content) > width:
174         content = content[width:]
175     os.write(dest, content)
176     if len(content) < width:
177         os.write(dest, ' ' * (width - len(content)))
178     if sep:
179         os.write(dest, sep)
180
181 def filter_output(src_out, src_err, dest_out, dest_err):
182     """Transfer data from src_out to dest_out and src_err to dest_err via
183     print_clean_line until src_out and src_err close."""
184     global sep_rx
185     assert not isinstance(src_out, bool)
186     assert not isinstance(src_err, bool)
187     assert not isinstance(dest_out, bool)
188     assert not isinstance(dest_err, bool)
189     assert src_out is not None or src_err is not None
190     assert (src_out is None) == (dest_out is None)
191     assert (src_err is None) == (dest_err is None)
192     pending = {}
193     pending_ex = None
194     try:
195         fds = tuple([x for x in (src_out, src_err) if x is not None])
196         while fds:
197             ready_fds, _, _ = select.select(fds, [], [])
198             width = tty_width()
199             for fd in ready_fds:
200                 buf = os.read(fd, 4096)
201                 dest = dest_out if fd == src_out else dest_err
202                 if not buf:
203                     fds = tuple([x for x in fds if x is not fd])
204                     print_clean_line(dest, pending.pop(fd, []), width)
205                 else:
206                     split = sep_rx.split(buf)
207                     if len(split) > 2:
208                         while len(split) > 1:
209                             content, sep = split[:2]
210                             split = split[2:]
211                             print_clean_line(dest,
212                                              pending.pop(fd, []) + [content],
213                                              width,
214                                              sep)
215                     else:
216                         assert(len(split) == 1)
217                         pending.setdefault(fd, []).extend(split)
218     except BaseException as ex:
219         pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
220     try:
221         # Try to finish each of the streams
222         for fd, pending_items in compat.items(pending):
223             dest = dest_out if fd == src_out else dest_err
224             try:
225                 print_clean_line(dest, pending_items, width)
226             except (EnvironmentError, EOFError) as ex:
227                 pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
228     except BaseException as ex:
229         pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
230     if pending_ex:
231         raise pending_ex
232
233 def run_subcmd(subcmd):
234
235     c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd
236     if not (fix_stdout or fix_stderr):
237         os.execvp(c[0], c)
238
239     p = None
240     try:
241         p = subprocess.Popen(c,
242                              stdout=PIPE if fix_stdout else sys.stdout,
243                              stderr=PIPE if fix_stderr else sys.stderr,
244                              preexec_fn=force_tty,
245                              bufsize=4096,
246                              close_fds=True)
247         # Assume p will receive these signals and quit, which will
248         # then cause us to quit.
249         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
250             signal.signal(sig, signal.SIG_IGN)
251
252         filter_output(fix_stdout and p.stdout.fileno() or None,
253                       fix_stderr and p.stderr.fileno() or None,
254                       fix_stdout and sys.stdout.fileno() or None,
255                       fix_stderr and sys.stderr.fileno() or None)
256         return p.wait()
257     except BaseException as ex:
258         add_ex_tb(ex)
259         try:
260             if p and p.poll() == None:
261                 os.kill(p.pid, signal.SIGTERM)
262                 p.wait()
263         except BaseException as kill_ex:
264             raise chain_ex(add_ex_tb(kill_ex), ex)
265         raise ex
266         
267 wrap_main(lambda : run_subcmd(subcmd))