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