##// END OF EJS Templates
httppeer: move url opening in its own method...
Boris Feld -
r35715:5a7906ed default
parent child Browse files
Show More
@@ -1,473 +1,476 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import tempfile
16 import tempfile
17
17
18 from .i18n import _
18 from .i18n import _
19 from .node import nullid
19 from .node import nullid
20 from . import (
20 from . import (
21 bundle2,
21 bundle2,
22 error,
22 error,
23 httpconnection,
23 httpconnection,
24 pycompat,
24 pycompat,
25 statichttprepo,
25 statichttprepo,
26 url,
26 url,
27 util,
27 util,
28 wireproto,
28 wireproto,
29 )
29 )
30
30
31 httplib = util.httplib
31 httplib = util.httplib
32 urlerr = util.urlerr
32 urlerr = util.urlerr
33 urlreq = util.urlreq
33 urlreq = util.urlreq
34
34
35 def encodevalueinheaders(value, header, limit):
35 def encodevalueinheaders(value, header, limit):
36 """Encode a string value into multiple HTTP headers.
36 """Encode a string value into multiple HTTP headers.
37
37
38 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``value`` will be encoded into 1 or more HTTP headers with the names
39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
40 name + value will be at most ``limit`` bytes long.
40 name + value will be at most ``limit`` bytes long.
41
41
42 Returns an iterable of 2-tuples consisting of header names and
42 Returns an iterable of 2-tuples consisting of header names and
43 values as native strings.
43 values as native strings.
44 """
44 """
45 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
45 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
46 # not bytes. This function always takes bytes in as arguments.
46 # not bytes. This function always takes bytes in as arguments.
47 fmt = pycompat.strurl(header) + r'-%s'
47 fmt = pycompat.strurl(header) + r'-%s'
48 # Note: it is *NOT* a bug that the last bit here is a bytestring
48 # Note: it is *NOT* a bug that the last bit here is a bytestring
49 # and not a unicode: we're just getting the encoded length anyway,
49 # and not a unicode: we're just getting the encoded length anyway,
50 # and using an r-string to make it portable between Python 2 and 3
50 # and using an r-string to make it portable between Python 2 and 3
51 # doesn't work because then the \r is a literal backslash-r
51 # doesn't work because then the \r is a literal backslash-r
52 # instead of a carriage return.
52 # instead of a carriage return.
53 valuelen = limit - len(fmt % r'000') - len(': \r\n')
53 valuelen = limit - len(fmt % r'000') - len(': \r\n')
54 result = []
54 result = []
55
55
56 n = 0
56 n = 0
57 for i in xrange(0, len(value), valuelen):
57 for i in xrange(0, len(value), valuelen):
58 n += 1
58 n += 1
59 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
59 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
60
60
61 return result
61 return result
62
62
63 def _wraphttpresponse(resp):
63 def _wraphttpresponse(resp):
64 """Wrap an HTTPResponse with common error handlers.
64 """Wrap an HTTPResponse with common error handlers.
65
65
66 This ensures that any I/O from any consumer raises the appropriate
66 This ensures that any I/O from any consumer raises the appropriate
67 error and messaging.
67 error and messaging.
68 """
68 """
69 origread = resp.read
69 origread = resp.read
70
70
71 class readerproxy(resp.__class__):
71 class readerproxy(resp.__class__):
72 def read(self, size=None):
72 def read(self, size=None):
73 try:
73 try:
74 return origread(size)
74 return origread(size)
75 except httplib.IncompleteRead as e:
75 except httplib.IncompleteRead as e:
76 # e.expected is an integer if length known or None otherwise.
76 # e.expected is an integer if length known or None otherwise.
77 if e.expected:
77 if e.expected:
78 msg = _('HTTP request error (incomplete response; '
78 msg = _('HTTP request error (incomplete response; '
79 'expected %d bytes got %d)') % (e.expected,
79 'expected %d bytes got %d)') % (e.expected,
80 len(e.partial))
80 len(e.partial))
81 else:
81 else:
82 msg = _('HTTP request error (incomplete response)')
82 msg = _('HTTP request error (incomplete response)')
83
83
84 raise error.PeerTransportError(
84 raise error.PeerTransportError(
85 msg,
85 msg,
86 hint=_('this may be an intermittent network failure; '
86 hint=_('this may be an intermittent network failure; '
87 'if the error persists, consider contacting the '
87 'if the error persists, consider contacting the '
88 'network or server operator'))
88 'network or server operator'))
89 except httplib.HTTPException as e:
89 except httplib.HTTPException as e:
90 raise error.PeerTransportError(
90 raise error.PeerTransportError(
91 _('HTTP request error (%s)') % e,
91 _('HTTP request error (%s)') % e,
92 hint=_('this may be an intermittent network failure; '
92 hint=_('this may be an intermittent network failure; '
93 'if the error persists, consider contacting the '
93 'if the error persists, consider contacting the '
94 'network or server operator'))
94 'network or server operator'))
95
95
96 resp.__class__ = readerproxy
96 resp.__class__ = readerproxy
97
97
98 class _multifile(object):
98 class _multifile(object):
99 def __init__(self, *fileobjs):
99 def __init__(self, *fileobjs):
100 for f in fileobjs:
100 for f in fileobjs:
101 if not util.safehasattr(f, 'length'):
101 if not util.safehasattr(f, 'length'):
102 raise ValueError(
102 raise ValueError(
103 '_multifile only supports file objects that '
103 '_multifile only supports file objects that '
104 'have a length but this one does not:', type(f), f)
104 'have a length but this one does not:', type(f), f)
105 self._fileobjs = fileobjs
105 self._fileobjs = fileobjs
106 self._index = 0
106 self._index = 0
107
107
108 @property
108 @property
109 def length(self):
109 def length(self):
110 return sum(f.length for f in self._fileobjs)
110 return sum(f.length for f in self._fileobjs)
111
111
112 def read(self, amt=None):
112 def read(self, amt=None):
113 if amt <= 0:
113 if amt <= 0:
114 return ''.join(f.read() for f in self._fileobjs)
114 return ''.join(f.read() for f in self._fileobjs)
115 parts = []
115 parts = []
116 while amt and self._index < len(self._fileobjs):
116 while amt and self._index < len(self._fileobjs):
117 parts.append(self._fileobjs[self._index].read(amt))
117 parts.append(self._fileobjs[self._index].read(amt))
118 got = len(parts[-1])
118 got = len(parts[-1])
119 if got < amt:
119 if got < amt:
120 self._index += 1
120 self._index += 1
121 amt -= got
121 amt -= got
122 return ''.join(parts)
122 return ''.join(parts)
123
123
124 def seek(self, offset, whence=os.SEEK_SET):
124 def seek(self, offset, whence=os.SEEK_SET):
125 if whence != os.SEEK_SET:
125 if whence != os.SEEK_SET:
126 raise NotImplementedError(
126 raise NotImplementedError(
127 '_multifile does not support anything other'
127 '_multifile does not support anything other'
128 ' than os.SEEK_SET for whence on seek()')
128 ' than os.SEEK_SET for whence on seek()')
129 if offset != 0:
129 if offset != 0:
130 raise NotImplementedError(
130 raise NotImplementedError(
131 '_multifile only supports seeking to start, but that '
131 '_multifile only supports seeking to start, but that '
132 'could be fixed if you need it')
132 'could be fixed if you need it')
133 for f in self._fileobjs:
133 for f in self._fileobjs:
134 f.seek(0)
134 f.seek(0)
135 self._index = 0
135 self._index = 0
136
136
137 class httppeer(wireproto.wirepeer):
137 class httppeer(wireproto.wirepeer):
138 def __init__(self, ui, path):
138 def __init__(self, ui, path):
139 self._path = path
139 self._path = path
140 self._caps = None
140 self._caps = None
141 self._urlopener = None
141 self._urlopener = None
142 self._requestbuilder = None
142 self._requestbuilder = None
143 u = util.url(path)
143 u = util.url(path)
144 if u.query or u.fragment:
144 if u.query or u.fragment:
145 raise error.Abort(_('unsupported URL component: "%s"') %
145 raise error.Abort(_('unsupported URL component: "%s"') %
146 (u.query or u.fragment))
146 (u.query or u.fragment))
147
147
148 # urllib cannot handle URLs with embedded user or passwd
148 # urllib cannot handle URLs with embedded user or passwd
149 self._url, authinfo = u.authinfo()
149 self._url, authinfo = u.authinfo()
150
150
151 self._ui = ui
151 self._ui = ui
152 ui.debug('using %s\n' % self._url)
152 ui.debug('using %s\n' % self._url)
153
153
154 self._urlopener = url.opener(ui, authinfo)
154 self._urlopener = url.opener(ui, authinfo)
155 self._requestbuilder = urlreq.request
155 self._requestbuilder = urlreq.request
156
156
157 def __del__(self):
157 def __del__(self):
158 urlopener = getattr(self, '_urlopener', None)
158 urlopener = getattr(self, '_urlopener', None)
159 if urlopener:
159 if urlopener:
160 for h in urlopener.handlers:
160 for h in urlopener.handlers:
161 h.close()
161 h.close()
162 getattr(h, "close_all", lambda: None)()
162 getattr(h, "close_all", lambda: None)()
163
163
164 def _openurl(self, req):
165 return self._urlopener.open(req)
166
164 # Begin of _basepeer interface.
167 # Begin of _basepeer interface.
165
168
166 @util.propertycache
169 @util.propertycache
167 def ui(self):
170 def ui(self):
168 return self._ui
171 return self._ui
169
172
170 def url(self):
173 def url(self):
171 return self._path
174 return self._path
172
175
173 def local(self):
176 def local(self):
174 return None
177 return None
175
178
176 def peer(self):
179 def peer(self):
177 return self
180 return self
178
181
179 def canpush(self):
182 def canpush(self):
180 return True
183 return True
181
184
182 def close(self):
185 def close(self):
183 pass
186 pass
184
187
185 # End of _basepeer interface.
188 # End of _basepeer interface.
186
189
187 # Begin of _basewirepeer interface.
190 # Begin of _basewirepeer interface.
188
191
189 def capabilities(self):
192 def capabilities(self):
190 if self._caps is None:
193 if self._caps is None:
191 try:
194 try:
192 self._fetchcaps()
195 self._fetchcaps()
193 except error.RepoError:
196 except error.RepoError:
194 self._caps = set()
197 self._caps = set()
195 self.ui.debug('capabilities: %s\n' %
198 self.ui.debug('capabilities: %s\n' %
196 (' '.join(self._caps or ['none'])))
199 (' '.join(self._caps or ['none'])))
197 return self._caps
200 return self._caps
198
201
199 # End of _basewirepeer interface.
202 # End of _basewirepeer interface.
200
203
201 # look up capabilities only when needed
204 # look up capabilities only when needed
202
205
203 def _fetchcaps(self):
206 def _fetchcaps(self):
204 self._caps = set(self._call('capabilities').split())
207 self._caps = set(self._call('capabilities').split())
205
208
206 def _callstream(self, cmd, _compressible=False, **args):
209 def _callstream(self, cmd, _compressible=False, **args):
207 args = pycompat.byteskwargs(args)
210 args = pycompat.byteskwargs(args)
208 if cmd == 'pushkey':
211 if cmd == 'pushkey':
209 args['data'] = ''
212 args['data'] = ''
210 data = args.pop('data', None)
213 data = args.pop('data', None)
211 headers = args.pop('headers', {})
214 headers = args.pop('headers', {})
212
215
213 self.ui.debug("sending %s command\n" % cmd)
216 self.ui.debug("sending %s command\n" % cmd)
214 q = [('cmd', cmd)]
217 q = [('cmd', cmd)]
215 headersize = 0
218 headersize = 0
216 varyheaders = []
219 varyheaders = []
217 # Important: don't use self.capable() here or else you end up
220 # Important: don't use self.capable() here or else you end up
218 # with infinite recursion when trying to look up capabilities
221 # with infinite recursion when trying to look up capabilities
219 # for the first time.
222 # for the first time.
220 postargsok = self._caps is not None and 'httppostargs' in self._caps
223 postargsok = self._caps is not None and 'httppostargs' in self._caps
221 if postargsok and args:
224 if postargsok and args:
222 strargs = urlreq.urlencode(sorted(args.items()))
225 strargs = urlreq.urlencode(sorted(args.items()))
223 if not data:
226 if not data:
224 data = strargs
227 data = strargs
225 else:
228 else:
226 if isinstance(data, bytes):
229 if isinstance(data, bytes):
227 i = io.BytesIO(data)
230 i = io.BytesIO(data)
228 i.length = len(data)
231 i.length = len(data)
229 data = i
232 data = i
230 argsio = io.BytesIO(strargs)
233 argsio = io.BytesIO(strargs)
231 argsio.length = len(strargs)
234 argsio.length = len(strargs)
232 data = _multifile(argsio, data)
235 data = _multifile(argsio, data)
233 headers[r'X-HgArgs-Post'] = len(strargs)
236 headers[r'X-HgArgs-Post'] = len(strargs)
234 else:
237 else:
235 if len(args) > 0:
238 if len(args) > 0:
236 httpheader = self.capable('httpheader')
239 httpheader = self.capable('httpheader')
237 if httpheader:
240 if httpheader:
238 headersize = int(httpheader.split(',', 1)[0])
241 headersize = int(httpheader.split(',', 1)[0])
239 if headersize > 0:
242 if headersize > 0:
240 # The headers can typically carry more data than the URL.
243 # The headers can typically carry more data than the URL.
241 encargs = urlreq.urlencode(sorted(args.items()))
244 encargs = urlreq.urlencode(sorted(args.items()))
242 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
245 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
243 headersize):
246 headersize):
244 headers[header] = value
247 headers[header] = value
245 varyheaders.append(header)
248 varyheaders.append(header)
246 else:
249 else:
247 q += sorted(args.items())
250 q += sorted(args.items())
248 qs = '?%s' % urlreq.urlencode(q)
251 qs = '?%s' % urlreq.urlencode(q)
249 cu = "%s%s" % (self._url, qs)
252 cu = "%s%s" % (self._url, qs)
250 size = 0
253 size = 0
251 if util.safehasattr(data, 'length'):
254 if util.safehasattr(data, 'length'):
252 size = data.length
255 size = data.length
253 elif data is not None:
256 elif data is not None:
254 size = len(data)
257 size = len(data)
255 if size and self.ui.configbool('ui', 'usehttp2'):
258 if size and self.ui.configbool('ui', 'usehttp2'):
256 headers[r'Expect'] = r'100-Continue'
259 headers[r'Expect'] = r'100-Continue'
257 headers[r'X-HgHttp2'] = r'1'
260 headers[r'X-HgHttp2'] = r'1'
258 if data is not None and r'Content-Type' not in headers:
261 if data is not None and r'Content-Type' not in headers:
259 headers[r'Content-Type'] = r'application/mercurial-0.1'
262 headers[r'Content-Type'] = r'application/mercurial-0.1'
260
263
261 # Tell the server we accept application/mercurial-0.2 and multiple
264 # Tell the server we accept application/mercurial-0.2 and multiple
262 # compression formats if the server is capable of emitting those
265 # compression formats if the server is capable of emitting those
263 # payloads.
266 # payloads.
264 protoparams = []
267 protoparams = []
265
268
266 mediatypes = set()
269 mediatypes = set()
267 if self._caps is not None:
270 if self._caps is not None:
268 mt = self.capable('httpmediatype')
271 mt = self.capable('httpmediatype')
269 if mt:
272 if mt:
270 protoparams.append('0.1')
273 protoparams.append('0.1')
271 mediatypes = set(mt.split(','))
274 mediatypes = set(mt.split(','))
272
275
273 if '0.2tx' in mediatypes:
276 if '0.2tx' in mediatypes:
274 protoparams.append('0.2')
277 protoparams.append('0.2')
275
278
276 if '0.2tx' in mediatypes and self.capable('compression'):
279 if '0.2tx' in mediatypes and self.capable('compression'):
277 # We /could/ compare supported compression formats and prune
280 # We /could/ compare supported compression formats and prune
278 # non-mutually supported or error if nothing is mutually supported.
281 # non-mutually supported or error if nothing is mutually supported.
279 # For now, send the full list to the server and have it error.
282 # For now, send the full list to the server and have it error.
280 comps = [e.wireprotosupport().name for e in
283 comps = [e.wireprotosupport().name for e in
281 util.compengines.supportedwireengines(util.CLIENTROLE)]
284 util.compengines.supportedwireengines(util.CLIENTROLE)]
282 protoparams.append('comp=%s' % ','.join(comps))
285 protoparams.append('comp=%s' % ','.join(comps))
283
286
284 if protoparams:
287 if protoparams:
285 protoheaders = encodevalueinheaders(' '.join(protoparams),
288 protoheaders = encodevalueinheaders(' '.join(protoparams),
286 'X-HgProto',
289 'X-HgProto',
287 headersize or 1024)
290 headersize or 1024)
288 for header, value in protoheaders:
291 for header, value in protoheaders:
289 headers[header] = value
292 headers[header] = value
290 varyheaders.append(header)
293 varyheaders.append(header)
291
294
292 if varyheaders:
295 if varyheaders:
293 headers[r'Vary'] = r','.join(varyheaders)
296 headers[r'Vary'] = r','.join(varyheaders)
294
297
295 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
298 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
296
299
297 if data is not None:
300 if data is not None:
298 self.ui.debug("sending %s bytes\n" % size)
301 self.ui.debug("sending %s bytes\n" % size)
299 req.add_unredirected_header('Content-Length', '%d' % size)
302 req.add_unredirected_header('Content-Length', '%d' % size)
300 try:
303 try:
301 resp = self._urlopener.open(req)
304 resp = self._openurl(req)
302 except urlerr.httperror as inst:
305 except urlerr.httperror as inst:
303 if inst.code == 401:
306 if inst.code == 401:
304 raise error.Abort(_('authorization failed'))
307 raise error.Abort(_('authorization failed'))
305 raise
308 raise
306 except httplib.HTTPException as inst:
309 except httplib.HTTPException as inst:
307 self.ui.debug('http error while sending %s command\n' % cmd)
310 self.ui.debug('http error while sending %s command\n' % cmd)
308 self.ui.traceback()
311 self.ui.traceback()
309 raise IOError(None, inst)
312 raise IOError(None, inst)
310
313
311 # Insert error handlers for common I/O failures.
314 # Insert error handlers for common I/O failures.
312 _wraphttpresponse(resp)
315 _wraphttpresponse(resp)
313
316
314 # record the url we got redirected to
317 # record the url we got redirected to
315 resp_url = pycompat.bytesurl(resp.geturl())
318 resp_url = pycompat.bytesurl(resp.geturl())
316 if resp_url.endswith(qs):
319 if resp_url.endswith(qs):
317 resp_url = resp_url[:-len(qs)]
320 resp_url = resp_url[:-len(qs)]
318 if self._url.rstrip('/') != resp_url.rstrip('/'):
321 if self._url.rstrip('/') != resp_url.rstrip('/'):
319 if not self.ui.quiet:
322 if not self.ui.quiet:
320 self.ui.warn(_('real URL is %s\n') % resp_url)
323 self.ui.warn(_('real URL is %s\n') % resp_url)
321 self._url = resp_url
324 self._url = resp_url
322 try:
325 try:
323 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
326 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
324 except AttributeError:
327 except AttributeError:
325 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
328 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
326
329
327 safeurl = util.hidepassword(self._url)
330 safeurl = util.hidepassword(self._url)
328 if proto.startswith('application/hg-error'):
331 if proto.startswith('application/hg-error'):
329 raise error.OutOfBandError(resp.read())
332 raise error.OutOfBandError(resp.read())
330 # accept old "text/plain" and "application/hg-changegroup" for now
333 # accept old "text/plain" and "application/hg-changegroup" for now
331 if not (proto.startswith('application/mercurial-') or
334 if not (proto.startswith('application/mercurial-') or
332 (proto.startswith('text/plain')
335 (proto.startswith('text/plain')
333 and not resp.headers.get('content-length')) or
336 and not resp.headers.get('content-length')) or
334 proto.startswith('application/hg-changegroup')):
337 proto.startswith('application/hg-changegroup')):
335 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
338 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
336 raise error.RepoError(
339 raise error.RepoError(
337 _("'%s' does not appear to be an hg repository:\n"
340 _("'%s' does not appear to be an hg repository:\n"
338 "---%%<--- (%s)\n%s\n---%%<---\n")
341 "---%%<--- (%s)\n%s\n---%%<---\n")
339 % (safeurl, proto or 'no content-type', resp.read(1024)))
342 % (safeurl, proto or 'no content-type', resp.read(1024)))
340
343
341 if proto.startswith('application/mercurial-'):
344 if proto.startswith('application/mercurial-'):
342 try:
345 try:
343 version = proto.split('-', 1)[1]
346 version = proto.split('-', 1)[1]
344 version_info = tuple([int(n) for n in version.split('.')])
347 version_info = tuple([int(n) for n in version.split('.')])
345 except ValueError:
348 except ValueError:
346 raise error.RepoError(_("'%s' sent a broken Content-Type "
349 raise error.RepoError(_("'%s' sent a broken Content-Type "
347 "header (%s)") % (safeurl, proto))
350 "header (%s)") % (safeurl, proto))
348
351
349 # TODO consider switching to a decompression reader that uses
352 # TODO consider switching to a decompression reader that uses
350 # generators.
353 # generators.
351 if version_info == (0, 1):
354 if version_info == (0, 1):
352 if _compressible:
355 if _compressible:
353 return util.compengines['zlib'].decompressorreader(resp)
356 return util.compengines['zlib'].decompressorreader(resp)
354 return resp
357 return resp
355 elif version_info == (0, 2):
358 elif version_info == (0, 2):
356 # application/mercurial-0.2 always identifies the compression
359 # application/mercurial-0.2 always identifies the compression
357 # engine in the payload header.
360 # engine in the payload header.
358 elen = struct.unpack('B', resp.read(1))[0]
361 elen = struct.unpack('B', resp.read(1))[0]
359 ename = resp.read(elen)
362 ename = resp.read(elen)
360 engine = util.compengines.forwiretype(ename)
363 engine = util.compengines.forwiretype(ename)
361 return engine.decompressorreader(resp)
364 return engine.decompressorreader(resp)
362 else:
365 else:
363 raise error.RepoError(_("'%s' uses newer protocol %s") %
366 raise error.RepoError(_("'%s' uses newer protocol %s") %
364 (safeurl, version))
367 (safeurl, version))
365
368
366 if _compressible:
369 if _compressible:
367 return util.compengines['zlib'].decompressorreader(resp)
370 return util.compengines['zlib'].decompressorreader(resp)
368
371
369 return resp
372 return resp
370
373
371 def _call(self, cmd, **args):
374 def _call(self, cmd, **args):
372 fp = self._callstream(cmd, **args)
375 fp = self._callstream(cmd, **args)
373 try:
376 try:
374 return fp.read()
377 return fp.read()
375 finally:
378 finally:
376 # if using keepalive, allow connection to be reused
379 # if using keepalive, allow connection to be reused
377 fp.close()
380 fp.close()
378
381
379 def _callpush(self, cmd, cg, **args):
382 def _callpush(self, cmd, cg, **args):
380 # have to stream bundle to a temp file because we do not have
383 # have to stream bundle to a temp file because we do not have
381 # http 1.1 chunked transfer.
384 # http 1.1 chunked transfer.
382
385
383 types = self.capable('unbundle')
386 types = self.capable('unbundle')
384 try:
387 try:
385 types = types.split(',')
388 types = types.split(',')
386 except AttributeError:
389 except AttributeError:
387 # servers older than d1b16a746db6 will send 'unbundle' as a
390 # servers older than d1b16a746db6 will send 'unbundle' as a
388 # boolean capability. They only support headerless/uncompressed
391 # boolean capability. They only support headerless/uncompressed
389 # bundles.
392 # bundles.
390 types = [""]
393 types = [""]
391 for x in types:
394 for x in types:
392 if x in bundle2.bundletypes:
395 if x in bundle2.bundletypes:
393 type = x
396 type = x
394 break
397 break
395
398
396 tempname = bundle2.writebundle(self.ui, cg, None, type)
399 tempname = bundle2.writebundle(self.ui, cg, None, type)
397 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
400 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
398 headers = {'Content-Type': 'application/mercurial-0.1'}
401 headers = {'Content-Type': 'application/mercurial-0.1'}
399
402
400 try:
403 try:
401 r = self._call(cmd, data=fp, headers=headers, **args)
404 r = self._call(cmd, data=fp, headers=headers, **args)
402 vals = r.split('\n', 1)
405 vals = r.split('\n', 1)
403 if len(vals) < 2:
406 if len(vals) < 2:
404 raise error.ResponseError(_("unexpected response:"), r)
407 raise error.ResponseError(_("unexpected response:"), r)
405 return vals
408 return vals
406 except socket.error as err:
409 except socket.error as err:
407 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
410 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
408 raise error.Abort(_('push failed: %s') % err.args[1])
411 raise error.Abort(_('push failed: %s') % err.args[1])
409 raise error.Abort(err.args[1])
412 raise error.Abort(err.args[1])
410 finally:
413 finally:
411 fp.close()
414 fp.close()
412 os.unlink(tempname)
415 os.unlink(tempname)
413
416
414 def _calltwowaystream(self, cmd, fp, **args):
417 def _calltwowaystream(self, cmd, fp, **args):
415 fh = None
418 fh = None
416 fp_ = None
419 fp_ = None
417 filename = None
420 filename = None
418 try:
421 try:
419 # dump bundle to disk
422 # dump bundle to disk
420 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
423 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
421 fh = os.fdopen(fd, pycompat.sysstr("wb"))
424 fh = os.fdopen(fd, pycompat.sysstr("wb"))
422 d = fp.read(4096)
425 d = fp.read(4096)
423 while d:
426 while d:
424 fh.write(d)
427 fh.write(d)
425 d = fp.read(4096)
428 d = fp.read(4096)
426 fh.close()
429 fh.close()
427 # start http push
430 # start http push
428 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
431 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
429 headers = {'Content-Type': 'application/mercurial-0.1'}
432 headers = {'Content-Type': 'application/mercurial-0.1'}
430 return self._callstream(cmd, data=fp_, headers=headers, **args)
433 return self._callstream(cmd, data=fp_, headers=headers, **args)
431 finally:
434 finally:
432 if fp_ is not None:
435 if fp_ is not None:
433 fp_.close()
436 fp_.close()
434 if fh is not None:
437 if fh is not None:
435 fh.close()
438 fh.close()
436 os.unlink(filename)
439 os.unlink(filename)
437
440
438 def _callcompressable(self, cmd, **args):
441 def _callcompressable(self, cmd, **args):
439 return self._callstream(cmd, _compressible=True, **args)
442 return self._callstream(cmd, _compressible=True, **args)
440
443
441 def _abort(self, exception):
444 def _abort(self, exception):
442 raise exception
445 raise exception
443
446
444 class httpspeer(httppeer):
447 class httpspeer(httppeer):
445 def __init__(self, ui, path):
448 def __init__(self, ui, path):
446 if not url.has_https:
449 if not url.has_https:
447 raise error.Abort(_('Python support for SSL and HTTPS '
450 raise error.Abort(_('Python support for SSL and HTTPS '
448 'is not installed'))
451 'is not installed'))
449 httppeer.__init__(self, ui, path)
452 httppeer.__init__(self, ui, path)
450
453
451 def instance(ui, path, create):
454 def instance(ui, path, create):
452 if create:
455 if create:
453 raise error.Abort(_('cannot create new http repository'))
456 raise error.Abort(_('cannot create new http repository'))
454 try:
457 try:
455 if path.startswith('https:'):
458 if path.startswith('https:'):
456 inst = httpspeer(ui, path)
459 inst = httpspeer(ui, path)
457 else:
460 else:
458 inst = httppeer(ui, path)
461 inst = httppeer(ui, path)
459 try:
462 try:
460 # Try to do useful work when checking compatibility.
463 # Try to do useful work when checking compatibility.
461 # Usually saves a roundtrip since we want the caps anyway.
464 # Usually saves a roundtrip since we want the caps anyway.
462 inst._fetchcaps()
465 inst._fetchcaps()
463 except error.RepoError:
466 except error.RepoError:
464 # No luck, try older compatibility check.
467 # No luck, try older compatibility check.
465 inst.between([(nullid, nullid)])
468 inst.between([(nullid, nullid)])
466 return inst
469 return inst
467 except error.RepoError as httpexception:
470 except error.RepoError as httpexception:
468 try:
471 try:
469 r = statichttprepo.instance(ui, "static-" + path, create)
472 r = statichttprepo.instance(ui, "static-" + path, create)
470 ui.note(_('(falling back to static-http)\n'))
473 ui.note(_('(falling back to static-http)\n'))
471 return r
474 return r
472 except error.RepoError:
475 except error.RepoError:
473 raise httpexception # use the original http RepoError instead
476 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now