##// END OF EJS Templates
protocol: avoid sending outrageously large between requests
Matt Mackall -
r7342:1dcd2cc6 default
parent child Browse files
Show More
@@ -1,235 +1,238 b''
1 1 # httprepo.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from node import bin, hex, nullid
10 10 from i18n import _
11 11 import repo, os, urllib, urllib2, urlparse, zlib, util, httplib
12 12 import errno, socket, changegroup, statichttprepo
13 13 import url
14 14
15 15 def zgenerator(f):
16 16 zd = zlib.decompressobj()
17 17 try:
18 18 for chunk in util.filechunkiter(f):
19 19 yield zd.decompress(chunk)
20 20 except httplib.HTTPException:
21 21 raise IOError(None, _('connection ended unexpectedly'))
22 22 yield zd.flush()
23 23
24 24 class httprepository(repo.repository):
25 25 def __init__(self, ui, path):
26 26 self.path = path
27 27 self.caps = None
28 28 self.handler = None
29 29 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
30 30 if query or frag:
31 31 raise util.Abort(_('unsupported URL component: "%s"') %
32 32 (query or frag))
33 33
34 34 # urllib cannot handle URLs with embedded user or passwd
35 35 self._url, authinfo = url.getauthinfo(path)
36 36
37 37 self.ui = ui
38 38 self.ui.debug(_('using %s\n') % self._url)
39 39
40 40 self.urlopener = url.opener(ui, authinfo)
41 41
42 42 def url(self):
43 43 return self.path
44 44
45 45 # look up capabilities only when needed
46 46
47 47 def get_caps(self):
48 48 if self.caps is None:
49 49 try:
50 50 self.caps = util.set(self.do_read('capabilities').split())
51 51 except repo.RepoError:
52 52 self.caps = util.set()
53 53 self.ui.debug(_('capabilities: %s\n') %
54 54 (' '.join(self.caps or ['none'])))
55 55 return self.caps
56 56
57 57 capabilities = property(get_caps)
58 58
59 59 def lock(self):
60 60 raise util.Abort(_('operation not supported over http'))
61 61
62 62 def do_cmd(self, cmd, **args):
63 63 data = args.pop('data', None)
64 64 headers = args.pop('headers', {})
65 65 self.ui.debug(_("sending %s command\n") % cmd)
66 66 q = {"cmd": cmd}
67 67 q.update(args)
68 68 qs = '?%s' % urllib.urlencode(q)
69 69 cu = "%s%s" % (self._url, qs)
70 70 try:
71 71 if data:
72 72 self.ui.debug(_("sending %s bytes\n") % len(data))
73 73 resp = self.urlopener.open(urllib2.Request(cu, data, headers))
74 74 except urllib2.HTTPError, inst:
75 75 if inst.code == 401:
76 76 raise util.Abort(_('authorization failed'))
77 77 raise
78 78 except httplib.HTTPException, inst:
79 79 self.ui.debug(_('http error while sending %s command\n') % cmd)
80 80 self.ui.print_exc()
81 81 raise IOError(None, inst)
82 82 except IndexError:
83 83 # this only happens with Python 2.3, later versions raise URLError
84 84 raise util.Abort(_('http error, possibly caused by proxy setting'))
85 85 # record the url we got redirected to
86 86 resp_url = resp.geturl()
87 87 if resp_url.endswith(qs):
88 88 resp_url = resp_url[:-len(qs)]
89 89 if self._url != resp_url:
90 90 self.ui.status(_('real URL is %s\n') % resp_url)
91 91 self._url = resp_url
92 92 try:
93 93 proto = resp.getheader('content-type')
94 94 except AttributeError:
95 95 proto = resp.headers['content-type']
96 96
97 97 # accept old "text/plain" and "application/hg-changegroup" for now
98 98 if not (proto.startswith('application/mercurial-') or
99 99 proto.startswith('text/plain') or
100 100 proto.startswith('application/hg-changegroup')):
101 101 self.ui.debug(_("Requested URL: '%s'\n") % cu)
102 102 raise repo.RepoError(_("'%s' does not appear to be an hg repository")
103 103 % self._url)
104 104
105 105 if proto.startswith('application/mercurial-'):
106 106 try:
107 107 version = proto.split('-', 1)[1]
108 108 version_info = tuple([int(n) for n in version.split('.')])
109 109 except ValueError:
110 110 raise repo.RepoError(_("'%s' sent a broken Content-Type "
111 111 "header (%s)") % (self._url, proto))
112 112 if version_info > (0, 1):
113 113 raise repo.RepoError(_("'%s' uses newer protocol %s") %
114 114 (self._url, version))
115 115
116 116 return resp
117 117
118 118 def do_read(self, cmd, **args):
119 119 fp = self.do_cmd(cmd, **args)
120 120 try:
121 121 return fp.read()
122 122 finally:
123 123 # if using keepalive, allow connection to be reused
124 124 fp.close()
125 125
126 126 def lookup(self, key):
127 127 self.requirecap('lookup', _('look up remote revision'))
128 128 d = self.do_cmd("lookup", key = key).read()
129 129 success, data = d[:-1].split(' ', 1)
130 130 if int(success):
131 131 return bin(data)
132 132 raise repo.RepoError(data)
133 133
134 134 def heads(self):
135 135 d = self.do_read("heads")
136 136 try:
137 137 return map(bin, d[:-1].split(" "))
138 138 except:
139 139 raise util.UnexpectedOutput(_("unexpected response:"), d)
140 140
141 141 def branches(self, nodes):
142 142 n = " ".join(map(hex, nodes))
143 143 d = self.do_read("branches", nodes=n)
144 144 try:
145 145 br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ]
146 146 return br
147 147 except:
148 148 raise util.UnexpectedOutput(_("unexpected response:"), d)
149 149
150 150 def between(self, pairs):
151 n = " ".join(["-".join(map(hex, p)) for p in pairs])
152 d = self.do_read("between", pairs=n)
153 try:
154 p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
155 return p
156 except:
157 raise util.UnexpectedOutput(_("unexpected response:"), d)
151 batch = 8 # avoid giant requests
152 r = []
153 for i in xrange(0, len(pairs), batch):
154 n = " ".join(["-".join(map(hex, p)) for p in pairs[i:i + batch]])
155 d = self.do_read("between", pairs=n)
156 try:
157 r += [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ]
158 except:
159 raise util.UnexpectedOutput(_("unexpected response:"), d)
160 return r
158 161
159 162 def changegroup(self, nodes, kind):
160 163 n = " ".join(map(hex, nodes))
161 164 f = self.do_cmd("changegroup", roots=n)
162 165 return util.chunkbuffer(zgenerator(f))
163 166
164 167 def changegroupsubset(self, bases, heads, source):
165 168 self.requirecap('changegroupsubset', _('look up remote changes'))
166 169 baselst = " ".join([hex(n) for n in bases])
167 170 headlst = " ".join([hex(n) for n in heads])
168 171 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
169 172 return util.chunkbuffer(zgenerator(f))
170 173
171 174 def unbundle(self, cg, heads, source):
172 175 # have to stream bundle to a temp file because we do not have
173 176 # http 1.1 chunked transfer.
174 177
175 178 type = ""
176 179 types = self.capable('unbundle')
177 180 # servers older than d1b16a746db6 will send 'unbundle' as a
178 181 # boolean capability
179 182 try:
180 183 types = types.split(',')
181 184 except AttributeError:
182 185 types = [""]
183 186 if types:
184 187 for x in types:
185 188 if x in changegroup.bundletypes:
186 189 type = x
187 190 break
188 191
189 192 tempname = changegroup.writebundle(cg, None, type)
190 193 fp = url.httpsendfile(tempname, "rb")
191 194 try:
192 195 try:
193 196 resp = self.do_read(
194 197 'unbundle', data=fp,
195 198 headers={'Content-Type': 'application/octet-stream'},
196 199 heads=' '.join(map(hex, heads)))
197 200 resp_code, output = resp.split('\n', 1)
198 201 try:
199 202 ret = int(resp_code)
200 203 except ValueError, err:
201 204 raise util.UnexpectedOutput(
202 205 _('push failed (unexpected response):'), resp)
203 206 self.ui.write(output)
204 207 return ret
205 208 except socket.error, err:
206 209 if err[0] in (errno.ECONNRESET, errno.EPIPE):
207 210 raise util.Abort(_('push failed: %s') % err[1])
208 211 raise util.Abort(err[1])
209 212 finally:
210 213 fp.close()
211 214 os.unlink(tempname)
212 215
213 216 def stream_out(self):
214 217 return self.do_cmd('stream_out')
215 218
216 219 class httpsrepository(httprepository):
217 220 def __init__(self, ui, path):
218 221 if not url.has_https:
219 222 raise util.Abort(_('Python support for SSL and HTTPS '
220 223 'is not installed'))
221 224 httprepository.__init__(self, ui, path)
222 225
223 226 def instance(ui, path, create):
224 227 if create:
225 228 raise util.Abort(_('cannot create new http repository'))
226 229 try:
227 230 if path.startswith('https:'):
228 231 inst = httpsrepository(ui, path)
229 232 else:
230 233 inst = httprepository(ui, path)
231 234 inst.between([(nullid, nullid)])
232 235 return inst
233 236 except repo.RepoError:
234 237 ui.note('(falling back to static-http)\n')
235 238 return statichttprepo.instance(ui, "static-" + path, create)
General Comments 0
You need to be logged in to leave comments. Login now