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