]> arthur.barton.de Git - bup.git/blob - lib/cmd/server-cmd.py
prune-older-cmd: copy to bup.cmd.prune_older
[bup.git] / lib / cmd / server-cmd.py
1 #!/bin/sh
2 """": # -*-python-*-
3 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
4 export "BUP_ARGV_0"="$0"
5 arg_i=1
6 for arg in "$@"; do
7     export "BUP_ARGV_${arg_i}"="$arg"
8     shift
9     arg_i=$((arg_i + 1))
10 done
11 # Here to end of preamble replaced during install
12 bup_python="$(dirname "$0")/../../config/bin/python" || exit $?
13 exec "$bup_python" "$0"
14 """
15 # end of bup preamble
16
17 from __future__ import absolute_import
18
19 # Intentionally replace the dirname "$0" that python prepends
20 import os, sys
21 sys.path[0] = os.path.dirname(os.path.realpath(__file__)) + '/..'
22
23 from binascii import hexlify, unhexlify
24 import struct, subprocess
25
26 from bup import compat, options, git, vfs, vint
27 from bup.compat import environ, hexstr
28 from bup.git import MissingObject
29 from bup.helpers import (Conn, debug1, debug2, linereader, lines_until_sentinel,
30                          log)
31 from bup.io import byte_stream, path_msg
32 from bup.repo import LocalRepo
33
34
35 suspended_w = None
36 dumb_server_mode = False
37 repo = None
38
39 def do_help(conn, junk):
40     conn.write(b'Commands:\n    %s\n' % b'\n    '.join(sorted(commands)))
41     conn.ok()
42
43
44 def _set_mode():
45     global dumb_server_mode
46     dumb_server_mode = os.path.exists(git.repo(b'bup-dumb-server'))
47     debug1('bup server: serving in %s mode\n' 
48            % (dumb_server_mode and 'dumb' or 'smart'))
49
50
51 def _init_session(reinit_with_new_repopath=None):
52     global repo
53     if reinit_with_new_repopath is None and git.repodir:
54         if not repo:
55             repo = LocalRepo()
56         return
57     git.check_repo_or_die(reinit_with_new_repopath)
58     if repo:
59         repo.close()
60     repo = LocalRepo()
61     # OK. we now know the path is a proper repository. Record this path in the
62     # environment so that subprocesses inherit it and know where to operate.
63     environ[b'BUP_DIR'] = git.repodir
64     debug1('bup server: bupdir is %s\n' % path_msg(git.repodir))
65     _set_mode()
66
67
68 def init_dir(conn, arg):
69     git.init_repo(arg)
70     debug1('bup server: bupdir initialized: %s\n' % path_msg(git.repodir))
71     _init_session(arg)
72     conn.ok()
73
74
75 def set_dir(conn, arg):
76     _init_session(arg)
77     conn.ok()
78
79     
80 def list_indexes(conn, junk):
81     _init_session()
82     suffix = b''
83     if dumb_server_mode:
84         suffix = b' load'
85     for f in os.listdir(git.repo(b'objects/pack')):
86         if f.endswith(b'.idx'):
87             conn.write(b'%s%s\n' % (f, suffix))
88     conn.ok()
89
90
91 def send_index(conn, name):
92     _init_session()
93     assert name.find(b'/') < 0
94     assert name.endswith(b'.idx')
95     idx = git.open_idx(git.repo(b'objects/pack/%s' % name))
96     conn.write(struct.pack('!I', len(idx.map)))
97     conn.write(idx.map)
98     conn.ok()
99
100
101 def receive_objects_v2(conn, junk):
102     global suspended_w
103     _init_session()
104     suggested = set()
105     if suspended_w:
106         w = suspended_w
107         suspended_w = None
108     else:
109         if dumb_server_mode:
110             w = git.PackWriter(objcache_maker=None)
111         else:
112             w = git.PackWriter()
113     while 1:
114         ns = conn.read(4)
115         if not ns:
116             w.abort()
117             raise Exception('object read: expected length header, got EOF\n')
118         n = struct.unpack('!I', ns)[0]
119         #debug2('expecting %d bytes\n' % n)
120         if not n:
121             debug1('bup server: received %d object%s.\n' 
122                 % (w.count, w.count!=1 and "s" or ''))
123             fullpath = w.close(run_midx=not dumb_server_mode)
124             if fullpath:
125                 (dir, name) = os.path.split(fullpath)
126                 conn.write(b'%s.idx\n' % name)
127             conn.ok()
128             return
129         elif n == 0xffffffff:
130             debug2('bup server: receive-objects suspended.\n')
131             suspended_w = w
132             conn.ok()
133             return
134             
135         shar = conn.read(20)
136         crcr = struct.unpack('!I', conn.read(4))[0]
137         n -= 20 + 4
138         buf = conn.read(n)  # object sizes in bup are reasonably small
139         #debug2('read %d bytes\n' % n)
140         _check(w, n, len(buf), 'object read: expected %d bytes, got %d\n')
141         if not dumb_server_mode:
142             oldpack = w.exists(shar, want_source=True)
143             if oldpack:
144                 assert(not oldpack == True)
145                 assert(oldpack.endswith(b'.idx'))
146                 (dir,name) = os.path.split(oldpack)
147                 if not (name in suggested):
148                     debug1("bup server: suggesting index %s\n"
149                            % git.shorten_hash(name).decode('ascii'))
150                     debug1("bup server:   because of object %s\n"
151                            % hexstr(shar))
152                     conn.write(b'index %s\n' % name)
153                     suggested.add(name)
154                 continue
155         nw, crc = w._raw_write((buf,), sha=shar)
156         _check(w, crcr, crc, 'object read: expected crc %d, got %d\n')
157     # NOTREACHED
158     
159
160 def _check(w, expected, actual, msg):
161     if expected != actual:
162         w.abort()
163         raise Exception(msg % (expected, actual))
164
165
166 def read_ref(conn, refname):
167     _init_session()
168     r = git.read_ref(refname)
169     conn.write(b'%s\n' % hexlify(r) if r else b'')
170     conn.ok()
171
172
173 def update_ref(conn, refname):
174     _init_session()
175     newval = conn.readline().strip()
176     oldval = conn.readline().strip()
177     git.update_ref(refname, unhexlify(newval), unhexlify(oldval))
178     conn.ok()
179
180 def join(conn, id):
181     _init_session()
182     try:
183         for blob in git.cp().join(id):
184             conn.write(struct.pack('!I', len(blob)))
185             conn.write(blob)
186     except KeyError as e:
187         log('server: error: %s\n' % e)
188         conn.write(b'\0\0\0\0')
189         conn.error(e)
190     else:
191         conn.write(b'\0\0\0\0')
192         conn.ok()
193
194 def cat_batch(conn, dummy):
195     _init_session()
196     cat_pipe = git.cp()
197     # For now, avoid potential deadlock by just reading them all
198     for ref in tuple(lines_until_sentinel(conn, b'\n', Exception)):
199         ref = ref[:-1]
200         it = cat_pipe.get(ref)
201         info = next(it)
202         if not info[0]:
203             conn.write(b'missing\n')
204             continue
205         conn.write(b'%s %s %d\n' % info)
206         for buf in it:
207             conn.write(buf)
208     conn.ok()
209
210 def refs(conn, args):
211     limit_to_heads, limit_to_tags = args.split()
212     assert limit_to_heads in (b'0', b'1')
213     assert limit_to_tags in (b'0', b'1')
214     limit_to_heads = int(limit_to_heads)
215     limit_to_tags = int(limit_to_tags)
216     _init_session()
217     patterns = tuple(x[:-1] for x in lines_until_sentinel(conn, b'\n', Exception))
218     for name, oid in git.list_refs(patterns=patterns,
219                                    limit_to_heads=limit_to_heads,
220                                    limit_to_tags=limit_to_tags):
221         assert b'\n' not in name
222         conn.write(b'%s %s\n' % (hexlify(oid), name))
223     conn.write(b'\n')
224     conn.ok()
225
226 def rev_list(conn, _):
227     _init_session()
228     count = conn.readline()
229     if not count:
230         raise Exception('Unexpected EOF while reading rev-list count')
231     assert count == b'\n'
232     count = None
233     fmt = conn.readline()
234     if not fmt:
235         raise Exception('Unexpected EOF while reading rev-list format')
236     fmt = None if fmt == b'\n' else fmt[:-1]
237     refs = tuple(x[:-1] for x in lines_until_sentinel(conn, b'\n', Exception))
238     args = git.rev_list_invocation(refs, format=fmt)
239     p = subprocess.Popen(args, env=git._gitenv(git.repodir),
240                          stdout=subprocess.PIPE)
241     while True:
242         out = p.stdout.read(64 * 1024)
243         if not out:
244             break
245         conn.write(out)
246     conn.write(b'\n')
247     rv = p.wait()  # not fatal
248     if rv:
249         msg = 'git rev-list returned error %d' % rv
250         conn.error(msg)
251         raise GitError(msg)
252     conn.ok()
253
254 def resolve(conn, args):
255     _init_session()
256     (flags,) = args.split()
257     flags = int(flags)
258     want_meta = bool(flags & 1)
259     follow = bool(flags & 2)
260     have_parent = bool(flags & 4)
261     parent = vfs.read_resolution(conn) if have_parent else None
262     path = vint.read_bvec(conn)
263     if not len(path):
264         raise Exception('Empty resolve path')
265     try:
266         res = list(vfs.resolve(repo, path, parent=parent, want_meta=want_meta,
267                                follow=follow))
268     except vfs.IOError as ex:
269         res = ex
270     if isinstance(res, vfs.IOError):
271         conn.write(b'\x00')  # error
272         vfs.write_ioerror(conn, res)
273     else:
274         conn.write(b'\x01')  # success
275         vfs.write_resolution(conn, res)
276     conn.ok()
277
278 optspec = """
279 bup server
280 """
281 o = options.Options(optspec)
282 (opt, flags, extra) = o.parse(compat.argv[1:])
283
284 if extra:
285     o.fatal('no arguments expected')
286
287 debug2('bup server: reading from stdin.\n')
288
289 commands = {
290     b'quit': None,
291     b'help': do_help,
292     b'init-dir': init_dir,
293     b'set-dir': set_dir,
294     b'list-indexes': list_indexes,
295     b'send-index': send_index,
296     b'receive-objects-v2': receive_objects_v2,
297     b'read-ref': read_ref,
298     b'update-ref': update_ref,
299     b'join': join,
300     b'cat': join,  # apocryphal alias
301     b'cat-batch' : cat_batch,
302     b'refs': refs,
303     b'rev-list': rev_list,
304     b'resolve': resolve
305 }
306
307 # FIXME: this protocol is totally lame and not at all future-proof.
308 # (Especially since we abort completely as soon as *anything* bad happens)
309 sys.stdout.flush()
310 conn = Conn(byte_stream(sys.stdin), byte_stream(sys.stdout))
311 lr = linereader(conn)
312 for _line in lr:
313     line = _line.strip()
314     if not line:
315         continue
316     debug1('bup server: command: %r\n' % line)
317     words = line.split(b' ', 1)
318     cmd = words[0]
319     rest = len(words)>1 and words[1] or b''
320     if cmd == b'quit':
321         break
322     else:
323         cmd = commands.get(cmd)
324         if cmd:
325             cmd(conn, rest)
326         else:
327             raise Exception('unknown server command: %r\n' % line)
328
329 debug1('bup server: done\n')