##// END OF EJS Templates
py3: add b'' prefixes to the LFS server module...
Matt Harbison -
r41469:7a11e4e5 default
parent child Browse files
Show More
@@ -1,349 +1,349
1 1 # wireprotolfsserver.py - lfs protocol server side implementation
2 2 #
3 3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import datetime
11 11 import errno
12 12 import json
13 13 import traceback
14 14
15 15 from mercurial.hgweb import (
16 16 common as hgwebcommon,
17 17 )
18 18
19 19 from mercurial import (
20 20 exthelper,
21 21 pycompat,
22 22 util,
23 23 wireprotoserver,
24 24 )
25 25
26 26 from . import blobstore
27 27
28 28 HTTP_OK = hgwebcommon.HTTP_OK
29 29 HTTP_CREATED = hgwebcommon.HTTP_CREATED
30 30 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
31 31 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
32 32 HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED
33 33 HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE
34 34 HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE
35 35
36 36 eh = exthelper.exthelper()
37 37
38 38 @eh.wrapfunction(wireprotoserver, 'handlewsgirequest')
39 39 def handlewsgirequest(orig, rctx, req, res, checkperm):
40 40 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
41 41 request if it is left unprocessed by the wrapped method.
42 42 """
43 43 if orig(rctx, req, res, checkperm):
44 44 return True
45 45
46 if not rctx.repo.ui.configbool('experimental', 'lfs.serve'):
46 if not rctx.repo.ui.configbool(b'experimental', b'lfs.serve'):
47 47 return False
48 48
49 49 if not util.safehasattr(rctx.repo.svfs, 'lfslocalblobstore'):
50 50 return False
51 51
52 52 if not req.dispatchpath:
53 53 return False
54 54
55 55 try:
56 56 if req.dispatchpath == b'.git/info/lfs/objects/batch':
57 checkperm(rctx, req, 'pull')
57 checkperm(rctx, req, b'pull')
58 58 return _processbatchrequest(rctx.repo, req, res)
59 59 # TODO: reserve and use a path in the proposed http wireprotocol /api/
60 60 # namespace?
61 61 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
62 62 return _processbasictransfer(rctx.repo, req, res,
63 63 lambda perm:
64 64 checkperm(rctx, req, perm))
65 65 return False
66 66 except hgwebcommon.ErrorResponse as e:
67 67 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
68 68 # in the wrapped function. Should this be moved back to hgweb to
69 69 # be a common handler?
70 70 for k, v in e.headers:
71 71 res.headers[k] = v
72 72 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
73 73 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
74 74 return True
75 75
76 76 def _sethttperror(res, code, message=None):
77 77 res.status = hgwebcommon.statusmessage(code, message=message)
78 78 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
79 79 res.setbodybytes(b'')
80 80
81 81 def _logexception(req):
82 82 """Write information about the current exception to wsgi.errors."""
83 83 tb = pycompat.sysbytes(traceback.format_exc())
84 errorlog = req.rawenv[r'wsgi.errors']
84 errorlog = req.rawenv[b'wsgi.errors']
85 85
86 86 uri = b''
87 87 if req.apppath:
88 88 uri += req.apppath
89 89 uri += b'/' + req.dispatchpath
90 90
91 91 errorlog.write(b"Exception happened while processing request '%s':\n%s" %
92 92 (uri, tb))
93 93
94 94 def _processbatchrequest(repo, req, res):
95 95 """Handle a request for the Batch API, which is the gateway to granting file
96 96 access.
97 97
98 98 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
99 99 """
100 100
101 101 # Mercurial client request:
102 102 #
103 103 # HOST: localhost:$HGPORT
104 104 # ACCEPT: application/vnd.git-lfs+json
105 105 # ACCEPT-ENCODING: identity
106 106 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
107 107 # Content-Length: 125
108 108 # Content-Type: application/vnd.git-lfs+json
109 109 #
110 110 # {
111 111 # "objects": [
112 112 # {
113 113 # "oid": "31cf...8e5b"
114 114 # "size": 12
115 115 # }
116 116 # ]
117 117 # "operation": "upload"
118 118 # }
119 119
120 120 if req.method != b'POST':
121 121 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED)
122 122 return True
123 123
124 124 if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json':
125 125 _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE)
126 126 return True
127 127
128 128 if req.headers[b'Accept'] != b'application/vnd.git-lfs+json':
129 129 _sethttperror(res, HTTP_NOT_ACCEPTABLE)
130 130 return True
131 131
132 132 # XXX: specify an encoding?
133 133 lfsreq = json.loads(req.bodyfh.read())
134 134
135 135 # If no transfer handlers are explicitly requested, 'basic' is assumed.
136 136 if 'basic' not in lfsreq.get('transfers', ['basic']):
137 137 _sethttperror(res, HTTP_BAD_REQUEST,
138 138 b'Only the basic LFS transfer handler is supported')
139 139 return True
140 140
141 141 operation = lfsreq.get('operation')
142 142 if operation not in ('upload', 'download'):
143 143 _sethttperror(res, HTTP_BAD_REQUEST,
144 144 b'Unsupported LFS transfer operation: %s' % operation)
145 145 return True
146 146
147 147 localstore = repo.svfs.lfslocalblobstore
148 148
149 149 objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []),
150 150 operation, localstore)]
151 151
152 152 rsp = {
153 153 'transfer': 'basic',
154 154 'objects': objects,
155 155 }
156 156
157 157 res.status = hgwebcommon.statusmessage(HTTP_OK)
158 158 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
159 159 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
160 160
161 161 return True
162 162
163 163 def _batchresponseobjects(req, objects, action, store):
164 164 """Yield one dictionary of attributes for the Batch API response for each
165 165 object in the list.
166 166
167 167 req: The parsedrequest for the Batch API request
168 168 objects: The list of objects in the Batch API object request list
169 169 action: 'upload' or 'download'
170 170 store: The local blob store for servicing requests"""
171 171
172 172 # Successful lfs-test-server response to solict an upload:
173 173 # {
174 174 # u'objects': [{
175 175 # u'size': 12,
176 176 # u'oid': u'31cf...8e5b',
177 177 # u'actions': {
178 178 # u'upload': {
179 179 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
180 180 # u'expires_at': u'0001-01-01T00:00:00Z',
181 181 # u'header': {
182 182 # u'Accept': u'application/vnd.git-lfs'
183 183 # }
184 184 # }
185 185 # }
186 186 # }]
187 187 # }
188 188
189 189 # TODO: Sort out the expires_at/expires_in/authenticated keys.
190 190
191 191 for obj in objects:
192 192 # Convert unicode to ASCII to create a filesystem path
193 193 oid = obj.get('oid').encode('ascii')
194 194 rsp = {
195 195 'oid': oid,
196 196 'size': obj.get('size'), # XXX: should this check the local size?
197 197 #'authenticated': True,
198 198 }
199 199
200 200 exists = True
201 201 verifies = False
202 202
203 203 # Verify an existing file on the upload request, so that the client is
204 204 # solicited to re-upload if it corrupt locally. Download requests are
205 205 # also verified, so the error can be flagged in the Batch API response.
206 206 # (Maybe we can use this to short circuit the download for `hg verify`,
207 207 # IFF the client can assert that the remote end is an hg server.)
208 208 # Otherwise, it's potentially overkill on download, since it is also
209 209 # verified as the file is streamed to the caller.
210 210 try:
211 211 verifies = store.verify(oid)
212 if verifies and action == 'upload':
212 if verifies and action == b'upload':
213 213 # The client will skip this upload, but make sure it remains
214 214 # available locally.
215 215 store.linkfromusercache(oid)
216 216 except IOError as inst:
217 217 if inst.errno != errno.ENOENT:
218 218 _logexception(req)
219 219
220 220 rsp['error'] = {
221 221 'code': 500,
222 222 'message': inst.strerror or 'Internal Server Server'
223 223 }
224 224 yield rsp
225 225 continue
226 226
227 227 exists = False
228 228
229 229 # Items are always listed for downloads. They are dropped for uploads
230 230 # IFF they already exist locally.
231 if action == 'download':
231 if action == b'download':
232 232 if not exists:
233 233 rsp['error'] = {
234 234 'code': 404,
235 235 'message': "The object does not exist"
236 236 }
237 237 yield rsp
238 238 continue
239 239
240 240 elif not verifies:
241 241 rsp['error'] = {
242 242 'code': 422, # XXX: is this the right code?
243 243 'message': "The object is corrupt"
244 244 }
245 245 yield rsp
246 246 continue
247 247
248 248 elif verifies:
249 249 yield rsp # Skip 'actions': already uploaded
250 250 continue
251 251
252 252 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
253 253
254 254 def _buildheader():
255 255 # The spec doesn't mention the Accept header here, but avoid
256 256 # a gratuitous deviation from lfs-test-server in the test
257 257 # output.
258 258 hdr = {
259 259 'Accept': 'application/vnd.git-lfs'
260 260 }
261 261
262 auth = req.headers.get('Authorization', '')
263 if auth.startswith('Basic '):
262 auth = req.headers.get(b'Authorization', b'')
263 if auth.startswith(b'Basic '):
264 264 hdr['Authorization'] = auth
265 265
266 266 return hdr
267 267
268 268 rsp['actions'] = {
269 269 '%s' % action: {
270 270 'href': '%s%s/.hg/lfs/objects/%s'
271 271 % (req.baseurl, req.apppath, oid),
272 272 # datetime.isoformat() doesn't include the 'Z' suffix
273 273 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
274 274 'header': _buildheader(),
275 275 }
276 276 }
277 277
278 278 yield rsp
279 279
280 280 def _processbasictransfer(repo, req, res, checkperm):
281 281 """Handle a single file upload (PUT) or download (GET) action for the Basic
282 282 Transfer Adapter.
283 283
284 284 After determining if the request is for an upload or download, the access
285 285 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
286 286 before accessing the files.
287 287
288 288 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
289 289 """
290 290
291 291 method = req.method
292 292 oid = req.dispatchparts[-1]
293 293 localstore = repo.svfs.lfslocalblobstore
294 294
295 295 if len(req.dispatchparts) != 4:
296 296 _sethttperror(res, HTTP_NOT_FOUND)
297 297 return True
298 298
299 299 if method == b'PUT':
300 checkperm('upload')
300 checkperm(b'upload')
301 301
302 302 # TODO: verify Content-Type?
303 303
304 304 existed = localstore.has(oid)
305 305
306 306 # TODO: how to handle timeouts? The body proxy handles limiting to
307 307 # Content-Length, but what happens if a client sends less than it
308 308 # says it will?
309 309
310 310 statusmessage = hgwebcommon.statusmessage
311 311 try:
312 312 localstore.download(oid, req.bodyfh)
313 313 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
314 314 except blobstore.LfsCorruptionError:
315 315 _logexception(req)
316 316
317 317 # XXX: Is this the right code?
318 318 res.status = statusmessage(422, b'corrupt blob')
319 319
320 320 # There's no payload here, but this is the header that lfs-test-server
321 321 # sends back. This eliminates some gratuitous test output conditionals.
322 322 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
323 323 res.setbodybytes(b'')
324 324
325 325 return True
326 326 elif method == b'GET':
327 checkperm('pull')
327 checkperm(b'pull')
328 328
329 329 res.status = hgwebcommon.statusmessage(HTTP_OK)
330 330 res.headers[b'Content-Type'] = b'application/octet-stream'
331 331
332 332 try:
333 333 # TODO: figure out how to send back the file in chunks, instead of
334 334 # reading the whole thing. (Also figure out how to send back
335 335 # an error status if an IOError occurs after a partial write
336 336 # in that case. Here, everything is read before starting.)
337 337 res.setbodybytes(localstore.read(oid))
338 338 except blobstore.LfsCorruptionError:
339 339 _logexception(req)
340 340
341 341 # XXX: Is this the right code?
342 342 res.status = hgwebcommon.statusmessage(422, b'corrupt blob')
343 343 res.setbodybytes(b'')
344 344
345 345 return True
346 346 else:
347 347 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED,
348 348 message=b'Unsupported LFS transfer method: %s' % method)
349 349 return True
General Comments 0
You need to be logged in to leave comments. Login now