##// END OF EJS Templates
httppeer: move size computation later in _callstream...
Augie Fackler -
r28484:da6f713a default
parent child Browse files
Show More
@@ -1,292 +1,290 b''
1 1 # httppeer.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 of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import httplib
13 13 import os
14 14 import socket
15 15 import tempfile
16 16 import urllib
17 17 import urllib2
18 18 import zlib
19 19
20 20 from .i18n import _
21 21 from .node import nullid
22 22 from . import (
23 23 changegroup,
24 24 error,
25 25 httpconnection,
26 26 statichttprepo,
27 27 url,
28 28 util,
29 29 wireproto,
30 30 )
31 31
32 32 def zgenerator(f):
33 33 zd = zlib.decompressobj()
34 34 try:
35 35 for chunk in util.filechunkiter(f):
36 36 while chunk:
37 37 yield zd.decompress(chunk, 2**18)
38 38 chunk = zd.unconsumed_tail
39 39 except httplib.HTTPException:
40 40 raise IOError(None, _('connection ended unexpectedly'))
41 41 yield zd.flush()
42 42
43 43 class httppeer(wireproto.wirepeer):
44 44 def __init__(self, ui, path):
45 45 self.path = path
46 46 self.caps = None
47 47 self.handler = None
48 48 self.urlopener = None
49 49 self.requestbuilder = None
50 50 u = util.url(path)
51 51 if u.query or u.fragment:
52 52 raise error.Abort(_('unsupported URL component: "%s"') %
53 53 (u.query or u.fragment))
54 54
55 55 # urllib cannot handle URLs with embedded user or passwd
56 56 self._url, authinfo = u.authinfo()
57 57
58 58 self.ui = ui
59 59 self.ui.debug('using %s\n' % self._url)
60 60
61 61 self.urlopener = url.opener(ui, authinfo)
62 62 self.requestbuilder = urllib2.Request
63 63
64 64 def __del__(self):
65 65 if self.urlopener:
66 66 for h in self.urlopener.handlers:
67 67 h.close()
68 68 getattr(h, "close_all", lambda : None)()
69 69
70 70 def url(self):
71 71 return self.path
72 72
73 73 # look up capabilities only when needed
74 74
75 75 def _fetchcaps(self):
76 76 self.caps = set(self._call('capabilities').split())
77 77
78 78 def _capabilities(self):
79 79 if self.caps is None:
80 80 try:
81 81 self._fetchcaps()
82 82 except error.RepoError:
83 83 self.caps = set()
84 84 self.ui.debug('capabilities: %s\n' %
85 85 (' '.join(self.caps or ['none'])))
86 86 return self.caps
87 87
88 88 def lock(self):
89 89 raise error.Abort(_('operation not supported over http'))
90 90
91 91 def _callstream(self, cmd, **args):
92 92 if cmd == 'pushkey':
93 93 args['data'] = ''
94 94 data = args.pop('data', None)
95 size = 0
96 if util.safehasattr(data, 'length'):
97 size = data.length
98 elif data is not None:
99 size = len(data)
100 95 headers = args.pop('headers', {})
101 if data is not None and 'Content-Type' not in headers:
102 headers['Content-Type'] = 'application/mercurial-0.1'
103
104
105 if size and self.ui.configbool('ui', 'usehttp2', False):
106 headers['Expect'] = '100-Continue'
107 headers['X-HgHttp2'] = '1'
108 96
109 97 self.ui.debug("sending %s command\n" % cmd)
110 98 q = [('cmd', cmd)]
111 99 headersize = 0
112 100 if len(args) > 0:
113 101 httpheader = self.capable('httpheader')
114 102 if httpheader:
115 103 headersize = int(httpheader.split(',', 1)[0])
116 104 if headersize > 0:
117 105 # The headers can typically carry more data than the URL.
118 106 encargs = urllib.urlencode(sorted(args.items()))
119 107 headerfmt = 'X-HgArg-%s'
120 108 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
121 109 headernum = 0
122 110 for i in xrange(0, len(encargs), contentlen):
123 111 headernum += 1
124 112 header = headerfmt % str(headernum)
125 113 headers[header] = encargs[i:i + contentlen]
126 114 varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)]
127 115 headers['Vary'] = ','.join(varyheaders)
128 116 else:
129 117 q += sorted(args.items())
130 118 qs = '?%s' % urllib.urlencode(q)
131 119 cu = "%s%s" % (self._url, qs)
120 size = 0
121 if util.safehasattr(data, 'length'):
122 size = data.length
123 elif data is not None:
124 size = len(data)
125 if size and self.ui.configbool('ui', 'usehttp2', False):
126 headers['Expect'] = '100-Continue'
127 headers['X-HgHttp2'] = '1'
128 if data is not None and 'Content-Type' not in headers:
129 headers['Content-Type'] = 'application/mercurial-0.1'
132 130 req = self.requestbuilder(cu, data, headers)
133 131 if data is not None:
134 132 self.ui.debug("sending %s bytes\n" % size)
135 133 req.add_unredirected_header('Content-Length', '%d' % size)
136 134 try:
137 135 resp = self.urlopener.open(req)
138 136 except urllib2.HTTPError as inst:
139 137 if inst.code == 401:
140 138 raise error.Abort(_('authorization failed'))
141 139 raise
142 140 except httplib.HTTPException as inst:
143 141 self.ui.debug('http error while sending %s command\n' % cmd)
144 142 self.ui.traceback()
145 143 raise IOError(None, inst)
146 144 except IndexError:
147 145 # this only happens with Python 2.3, later versions raise URLError
148 146 raise error.Abort(_('http error, possibly caused by proxy setting'))
149 147 # record the url we got redirected to
150 148 resp_url = resp.geturl()
151 149 if resp_url.endswith(qs):
152 150 resp_url = resp_url[:-len(qs)]
153 151 if self._url.rstrip('/') != resp_url.rstrip('/'):
154 152 if not self.ui.quiet:
155 153 self.ui.warn(_('real URL is %s\n') % resp_url)
156 154 self._url = resp_url
157 155 try:
158 156 proto = resp.getheader('content-type')
159 157 except AttributeError:
160 158 proto = resp.headers.get('content-type', '')
161 159
162 160 safeurl = util.hidepassword(self._url)
163 161 if proto.startswith('application/hg-error'):
164 162 raise error.OutOfBandError(resp.read())
165 163 # accept old "text/plain" and "application/hg-changegroup" for now
166 164 if not (proto.startswith('application/mercurial-') or
167 165 (proto.startswith('text/plain')
168 166 and not resp.headers.get('content-length')) or
169 167 proto.startswith('application/hg-changegroup')):
170 168 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
171 169 raise error.RepoError(
172 170 _("'%s' does not appear to be an hg repository:\n"
173 171 "---%%<--- (%s)\n%s\n---%%<---\n")
174 172 % (safeurl, proto or 'no content-type', resp.read(1024)))
175 173
176 174 if proto.startswith('application/mercurial-'):
177 175 try:
178 176 version = proto.split('-', 1)[1]
179 177 version_info = tuple([int(n) for n in version.split('.')])
180 178 except ValueError:
181 179 raise error.RepoError(_("'%s' sent a broken Content-Type "
182 180 "header (%s)") % (safeurl, proto))
183 181 if version_info > (0, 1):
184 182 raise error.RepoError(_("'%s' uses newer protocol %s") %
185 183 (safeurl, version))
186 184
187 185 return resp
188 186
189 187 def _call(self, cmd, **args):
190 188 fp = self._callstream(cmd, **args)
191 189 try:
192 190 return fp.read()
193 191 finally:
194 192 # if using keepalive, allow connection to be reused
195 193 fp.close()
196 194
197 195 def _callpush(self, cmd, cg, **args):
198 196 # have to stream bundle to a temp file because we do not have
199 197 # http 1.1 chunked transfer.
200 198
201 199 types = self.capable('unbundle')
202 200 try:
203 201 types = types.split(',')
204 202 except AttributeError:
205 203 # servers older than d1b16a746db6 will send 'unbundle' as a
206 204 # boolean capability. They only support headerless/uncompressed
207 205 # bundles.
208 206 types = [""]
209 207 for x in types:
210 208 if x in changegroup.bundletypes:
211 209 type = x
212 210 break
213 211
214 212 tempname = changegroup.writebundle(self.ui, cg, None, type)
215 213 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
216 214 headers = {'Content-Type': 'application/mercurial-0.1'}
217 215
218 216 try:
219 217 r = self._call(cmd, data=fp, headers=headers, **args)
220 218 vals = r.split('\n', 1)
221 219 if len(vals) < 2:
222 220 raise error.ResponseError(_("unexpected response:"), r)
223 221 return vals
224 222 except socket.error as err:
225 223 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
226 224 raise error.Abort(_('push failed: %s') % err.args[1])
227 225 raise error.Abort(err.args[1])
228 226 finally:
229 227 fp.close()
230 228 os.unlink(tempname)
231 229
232 230 def _calltwowaystream(self, cmd, fp, **args):
233 231 fh = None
234 232 fp_ = None
235 233 filename = None
236 234 try:
237 235 # dump bundle to disk
238 236 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
239 237 fh = os.fdopen(fd, "wb")
240 238 d = fp.read(4096)
241 239 while d:
242 240 fh.write(d)
243 241 d = fp.read(4096)
244 242 fh.close()
245 243 # start http push
246 244 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
247 245 headers = {'Content-Type': 'application/mercurial-0.1'}
248 246 return self._callstream(cmd, data=fp_, headers=headers, **args)
249 247 finally:
250 248 if fp_ is not None:
251 249 fp_.close()
252 250 if fh is not None:
253 251 fh.close()
254 252 os.unlink(filename)
255 253
256 254 def _callcompressable(self, cmd, **args):
257 255 stream = self._callstream(cmd, **args)
258 256 return util.chunkbuffer(zgenerator(stream))
259 257
260 258 def _abort(self, exception):
261 259 raise exception
262 260
263 261 class httpspeer(httppeer):
264 262 def __init__(self, ui, path):
265 263 if not url.has_https:
266 264 raise error.Abort(_('Python support for SSL and HTTPS '
267 265 'is not installed'))
268 266 httppeer.__init__(self, ui, path)
269 267
270 268 def instance(ui, path, create):
271 269 if create:
272 270 raise error.Abort(_('cannot create new http repository'))
273 271 try:
274 272 if path.startswith('https:'):
275 273 inst = httpspeer(ui, path)
276 274 else:
277 275 inst = httppeer(ui, path)
278 276 try:
279 277 # Try to do useful work when checking compatibility.
280 278 # Usually saves a roundtrip since we want the caps anyway.
281 279 inst._fetchcaps()
282 280 except error.RepoError:
283 281 # No luck, try older compatibility check.
284 282 inst.between([(nullid, nullid)])
285 283 return inst
286 284 except error.RepoError as httpexception:
287 285 try:
288 286 r = statichttprepo.instance(ui, "static-" + path, create)
289 287 ui.note('(falling back to static-http)\n')
290 288 return r
291 289 except error.RepoError:
292 290 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now