]> 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                     while len(split) > 1:
208                         content, sep = split[:2]
209                         split = split[2:]
210                         print_clean_line(dest,
211                                          pending.pop(fd, []) + [content],
212                                          width,
213                                          sep)
214                     assert(len(split) == 1)
215                     if split[0]:
216                         pending.setdefault(fd, []).extend(split)
217     except BaseException as ex:
218         pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
219     try:
220         # Try to finish each of the streams
221         for fd, pending_items in compat.items(pending):
222             dest = dest_out if fd == src_out else dest_err
223             try:
224                 print_clean_line(dest, pending_items, width)
225             except (EnvironmentError, EOFError) as ex:
226                 pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
227     except BaseException as ex:
228         pending_ex = chain_ex(add_ex_tb(ex), pending_ex)
229     if pending_ex:
230         raise pending_ex
231
232 def run_subcmd(subcmd):
233
234     c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd
235     if not (fix_stdout or fix_stderr):
236         os.execvp(c[0], c)
237
238     p = None
239     try:
240         p = subprocess.Popen(c,
241                              stdout=PIPE if fix_stdout else sys.stdout,
242                              stderr=PIPE if fix_stderr else sys.stderr,
243                              preexec_fn=force_tty,
244                              bufsize=4096,
245                              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 chain_ex(add_ex_tb(kill_ex), ex)
264         raise ex
265         
266 wrap_main(lambda : run_subcmd(subcmd))