##// END OF EJS Templates
lfs: add server side support for the Batch API
Matt Harbison -
r37166:ea6fc585 default
parent child Browse files
Show More
@@ -7,6 +7,10 b''
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import datetime
11 import errno
12 import json
13
10 from mercurial.hgweb import (
14 from mercurial.hgweb import (
11 common as hgwebcommon,
15 common as hgwebcommon,
12 )
16 )
@@ -15,6 +19,9 b' from mercurial import ('
15 pycompat,
19 pycompat,
16 )
20 )
17
21
22 HTTP_OK = hgwebcommon.HTTP_OK
23 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
24
18 def handlewsgirequest(orig, rctx, req, res, checkperm):
25 def handlewsgirequest(orig, rctx, req, res, checkperm):
19 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
26 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
20 request if it is left unprocessed by the wrapped method.
27 request if it is left unprocessed by the wrapped method.
@@ -46,13 +53,177 b' def handlewsgirequest(orig, rctx, req, r'
46 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
53 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
47 return True
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 def _processbatchrequest(repo, req, res):
61 def _processbatchrequest(repo, req, res):
50 """Handle a request for the Batch API, which is the gateway to granting file
62 """Handle a request for the Batch API, which is the gateway to granting file
51 access.
63 access.
52
64
53 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
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 def _processbasictransfer(repo, req, res, checkperm):
228 def _processbasictransfer(repo, req, res, checkperm):
58 """Handle a single file upload (PUT) or download (GET) action for the Basic
229 """Handle a single file upload (PUT) or download (GET) action for the Basic
General Comments 0
You need to be logged in to leave comments. Login now