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