##// END OF EJS Templates
lfs: add server side support for the Batch API
Matt Harbison -
r37166:ea6fc585 default
parent child Browse files
Show More
@@ -1,75 +1,246
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 import datetime
11 import errno
12 import json
13
10 14 from mercurial.hgweb import (
11 15 common as hgwebcommon,
12 16 )
13 17
14 18 from mercurial import (
15 19 pycompat,
16 20 )
17 21
22 HTTP_OK = hgwebcommon.HTTP_OK
23 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
24
18 25 def handlewsgirequest(orig, rctx, req, res, checkperm):
19 26 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
20 27 request if it is left unprocessed by the wrapped method.
21 28 """
22 29 if orig(rctx, req, res, checkperm):
23 30 return True
24 31
25 32 if not req.dispatchpath:
26 33 return False
27 34
28 35 try:
29 36 if req.dispatchpath == b'.git/info/lfs/objects/batch':
30 37 checkperm(rctx, req, 'pull')
31 38 return _processbatchrequest(rctx.repo, req, res)
32 39 # TODO: reserve and use a path in the proposed http wireprotocol /api/
33 40 # namespace?
34 41 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
35 42 return _processbasictransfer(rctx.repo, req, res,
36 43 lambda perm:
37 44 checkperm(rctx, req, perm))
38 45 return False
39 46 except hgwebcommon.ErrorResponse as e:
40 47 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
41 48 # in the wrapped function. Should this be moved back to hgweb to
42 49 # be a common handler?
43 50 for k, v in e.headers:
44 51 res.headers[k] = v
45 52 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
46 53 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
47 54 return True
48 55
56 def _sethttperror(res, code, message=None):
57 res.status = hgwebcommon.statusmessage(code, message=message)
58 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
59 res.setbodybytes(b'')
60
49 61 def _processbatchrequest(repo, req, res):
50 62 """Handle a request for the Batch API, which is the gateway to granting file
51 63 access.
52 64
53 65 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
54 66 """
55 return False
67
68 # Mercurial client request:
69 #
70 # HOST: localhost:$HGPORT
71 # ACCEPT: application/vnd.git-lfs+json
72 # ACCEPT-ENCODING: identity
73 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
74 # Content-Length: 125
75 # Content-Type: application/vnd.git-lfs+json
76 #
77 # {
78 # "objects": [
79 # {
80 # "oid": "31cf...8e5b"
81 # "size": 12
82 # }
83 # ]
84 # "operation": "upload"
85 # }
86
87 if (req.method != b'POST'
88 or req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json'
89 or req.headers[b'Accept'] != b'application/vnd.git-lfs+json'):
90 # TODO: figure out what the proper handling for a bad request to the
91 # Batch API is.
92 _sethttperror(res, HTTP_BAD_REQUEST, b'Invalid Batch API request')
93 return True
94
95 # XXX: specify an encoding?
96 lfsreq = json.loads(req.bodyfh.read())
97
98 # If no transfer handlers are explicitly requested, 'basic' is assumed.
99 if 'basic' not in lfsreq.get('transfers', ['basic']):
100 _sethttperror(res, HTTP_BAD_REQUEST,
101 b'Only the basic LFS transfer handler is supported')
102 return True
103
104 operation = lfsreq.get('operation')
105 if operation not in ('upload', 'download'):
106 _sethttperror(res, HTTP_BAD_REQUEST,
107 b'Unsupported LFS transfer operation: %s' % operation)
108 return True
109
110 localstore = repo.svfs.lfslocalblobstore
111
112 objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []),
113 operation, localstore)]
114
115 rsp = {
116 'transfer': 'basic',
117 'objects': objects,
118 }
119
120 res.status = hgwebcommon.statusmessage(HTTP_OK)
121 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
122 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
123
124 return True
125
126 def _batchresponseobjects(req, objects, action, store):
127 """Yield one dictionary of attributes for the Batch API response for each
128 object in the list.
129
130 req: The parsedrequest for the Batch API request
131 objects: The list of objects in the Batch API object request list
132 action: 'upload' or 'download'
133 store: The local blob store for servicing requests"""
134
135 # Successful lfs-test-server response to solict an upload:
136 # {
137 # u'objects': [{
138 # u'size': 12,
139 # u'oid': u'31cf...8e5b',
140 # u'actions': {
141 # u'upload': {
142 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
143 # u'expires_at': u'0001-01-01T00:00:00Z',
144 # u'header': {
145 # u'Accept': u'application/vnd.git-lfs'
146 # }
147 # }
148 # }
149 # }]
150 # }
151
152 # TODO: Sort out the expires_at/expires_in/authenticated keys.
153
154 for obj in objects:
155 # Convert unicode to ASCII to create a filesystem path
156 oid = obj.get('oid').encode('ascii')
157 rsp = {
158 'oid': oid,
159 'size': obj.get('size'), # XXX: should this check the local size?
160 #'authenticated': True,
161 }
162
163 exists = True
164 verifies = False
165
166 # Verify an existing file on the upload request, so that the client is
167 # solicited to re-upload if it corrupt locally. Download requests are
168 # also verified, so the error can be flagged in the Batch API response.
169 # (Maybe we can use this to short circuit the download for `hg verify`,
170 # IFF the client can assert that the remote end is an hg server.)
171 # Otherwise, it's potentially overkill on download, since it is also
172 # verified as the file is streamed to the caller.
173 try:
174 verifies = store.verify(oid)
175 except IOError as inst:
176 if inst.errno != errno.ENOENT:
177 rsp['error'] = {
178 'code': 500,
179 'message': inst.strerror or 'Internal Server Server'
180 }
181 yield rsp
182 continue
183
184 exists = False
185
186 # Items are always listed for downloads. They are dropped for uploads
187 # IFF they already exist locally.
188 if action == 'download':
189 if not exists:
190 rsp['error'] = {
191 'code': 404,
192 'message': "The object does not exist"
193 }
194 yield rsp
195 continue
196
197 elif not verifies:
198 rsp['error'] = {
199 'code': 422, # XXX: is this the right code?
200 'message': "The object is corrupt"
201 }
202 yield rsp
203 continue
204
205 elif verifies:
206 yield rsp # Skip 'actions': already uploaded
207 continue
208
209 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
210
211 rsp['actions'] = {
212 '%s' % action: {
213 # TODO: Account for the --prefix, if any.
214 'href': '%s/.hg/lfs/objects/%s' % (req.baseurl, oid),
215 # datetime.isoformat() doesn't include the 'Z' suffix
216 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
217 'header': {
218 # The spec doesn't mention the Accept header here, but avoid
219 # a gratuitous deviation from lfs-test-server in the test
220 # output.
221 'Accept': 'application/vnd.git-lfs'
222 }
223 }
224 }
225
226 yield rsp
56 227
57 228 def _processbasictransfer(repo, req, res, checkperm):
58 229 """Handle a single file upload (PUT) or download (GET) action for the Basic
59 230 Transfer Adapter.
60 231
61 232 After determining if the request is for an upload or download, the access
62 233 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
63 234 before accessing the files.
64 235
65 236 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
66 237 """
67 238
68 239 method = req.method
69 240
70 241 if method == b'PUT':
71 242 checkperm('upload')
72 243 elif method == b'GET':
73 244 checkperm('pull')
74 245
75 246 return False
General Comments 0
You need to be logged in to leave comments. Login now