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