131 |
131 |
132 # XXX: specify an encoding? |
132 # XXX: specify an encoding? |
133 lfsreq = json.loads(req.bodyfh.read()) |
133 lfsreq = json.loads(req.bodyfh.read()) |
134 |
134 |
135 # If no transfer handlers are explicitly requested, 'basic' is assumed. |
135 # If no transfer handlers are explicitly requested, 'basic' is assumed. |
136 if 'basic' not in lfsreq.get('transfers', ['basic']): |
136 if r'basic' not in lfsreq.get(r'transfers', [r'basic']): |
137 _sethttperror(res, HTTP_BAD_REQUEST, |
137 _sethttperror(res, HTTP_BAD_REQUEST, |
138 b'Only the basic LFS transfer handler is supported') |
138 b'Only the basic LFS transfer handler is supported') |
139 return True |
139 return True |
140 |
140 |
141 operation = lfsreq.get('operation') |
141 operation = lfsreq.get(r'operation') |
142 if operation not in ('upload', 'download'): |
142 operation = pycompat.bytestr(operation) |
|
143 |
|
144 if operation not in (b'upload', b'download'): |
143 _sethttperror(res, HTTP_BAD_REQUEST, |
145 _sethttperror(res, HTTP_BAD_REQUEST, |
144 b'Unsupported LFS transfer operation: %s' % operation) |
146 b'Unsupported LFS transfer operation: %s' % operation) |
145 return True |
147 return True |
146 |
148 |
147 localstore = repo.svfs.lfslocalblobstore |
149 localstore = repo.svfs.lfslocalblobstore |
148 |
150 |
149 objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []), |
151 objects = [p for p in _batchresponseobjects(req, lfsreq.get(r'objects', []), |
150 operation, localstore)] |
152 operation, localstore)] |
151 |
153 |
152 rsp = { |
154 rsp = { |
153 'transfer': 'basic', |
155 r'transfer': r'basic', |
154 'objects': objects, |
156 r'objects': objects, |
155 } |
157 } |
156 |
158 |
157 res.status = hgwebcommon.statusmessage(HTTP_OK) |
159 res.status = hgwebcommon.statusmessage(HTTP_OK) |
158 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json' |
160 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json' |
159 res.setbodybytes(pycompat.bytestr(json.dumps(rsp))) |
161 res.setbodybytes(pycompat.bytestr(json.dumps(rsp))) |
188 |
190 |
189 # TODO: Sort out the expires_at/expires_in/authenticated keys. |
191 # TODO: Sort out the expires_at/expires_in/authenticated keys. |
190 |
192 |
191 for obj in objects: |
193 for obj in objects: |
192 # Convert unicode to ASCII to create a filesystem path |
194 # Convert unicode to ASCII to create a filesystem path |
193 oid = obj.get('oid').encode('ascii') |
195 soid = obj.get(r'oid') |
|
196 oid = soid.encode(r'ascii') |
194 rsp = { |
197 rsp = { |
195 'oid': oid, |
198 r'oid': soid, |
196 'size': obj.get('size'), # XXX: should this check the local size? |
199 r'size': obj.get(r'size'), # XXX: should this check the local size? |
197 #'authenticated': True, |
200 #r'authenticated': True, |
198 } |
201 } |
199 |
202 |
200 exists = True |
203 exists = True |
201 verifies = False |
204 verifies = False |
202 |
205 |
215 store.linkfromusercache(oid) |
218 store.linkfromusercache(oid) |
216 except IOError as inst: |
219 except IOError as inst: |
217 if inst.errno != errno.ENOENT: |
220 if inst.errno != errno.ENOENT: |
218 _logexception(req) |
221 _logexception(req) |
219 |
222 |
220 rsp['error'] = { |
223 rsp[r'error'] = { |
221 'code': 500, |
224 r'code': 500, |
222 'message': inst.strerror or 'Internal Server Server' |
225 r'message': inst.strerror or r'Internal Server Server' |
223 } |
226 } |
224 yield rsp |
227 yield rsp |
225 continue |
228 continue |
226 |
229 |
227 exists = False |
230 exists = False |
228 |
231 |
229 # Items are always listed for downloads. They are dropped for uploads |
232 # Items are always listed for downloads. They are dropped for uploads |
230 # IFF they already exist locally. |
233 # IFF they already exist locally. |
231 if action == b'download': |
234 if action == b'download': |
232 if not exists: |
235 if not exists: |
233 rsp['error'] = { |
236 rsp[r'error'] = { |
234 'code': 404, |
237 r'code': 404, |
235 'message': "The object does not exist" |
238 r'message': r"The object does not exist" |
236 } |
239 } |
237 yield rsp |
240 yield rsp |
238 continue |
241 continue |
239 |
242 |
240 elif not verifies: |
243 elif not verifies: |
241 rsp['error'] = { |
244 rsp[r'error'] = { |
242 'code': 422, # XXX: is this the right code? |
245 r'code': 422, # XXX: is this the right code? |
243 'message': "The object is corrupt" |
246 r'message': r"The object is corrupt" |
244 } |
247 } |
245 yield rsp |
248 yield rsp |
246 continue |
249 continue |
247 |
250 |
248 elif verifies: |
251 elif verifies: |
254 def _buildheader(): |
257 def _buildheader(): |
255 # The spec doesn't mention the Accept header here, but avoid |
258 # The spec doesn't mention the Accept header here, but avoid |
256 # a gratuitous deviation from lfs-test-server in the test |
259 # a gratuitous deviation from lfs-test-server in the test |
257 # output. |
260 # output. |
258 hdr = { |
261 hdr = { |
259 'Accept': 'application/vnd.git-lfs' |
262 r'Accept': r'application/vnd.git-lfs' |
260 } |
263 } |
261 |
264 |
262 auth = req.headers.get(b'Authorization', b'') |
265 auth = req.headers.get(b'Authorization', b'') |
263 if auth.startswith(b'Basic '): |
266 if auth.startswith(b'Basic '): |
264 hdr['Authorization'] = auth |
267 hdr[r'Authorization'] = pycompat.strurl(auth) |
265 |
268 |
266 return hdr |
269 return hdr |
267 |
270 |
268 rsp['actions'] = { |
271 rsp[r'actions'] = { |
269 '%s' % action: { |
272 r'%s' % pycompat.strurl(action): { |
270 'href': '%s%s/.hg/lfs/objects/%s' |
273 r'href': pycompat.strurl(b'%s%s/.hg/lfs/objects/%s' |
271 % (req.baseurl, req.apppath, oid), |
274 % (req.baseurl, req.apppath, oid)), |
272 # datetime.isoformat() doesn't include the 'Z' suffix |
275 # datetime.isoformat() doesn't include the 'Z' suffix |
273 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'), |
276 r"expires_at": expiresat.strftime(r'%Y-%m-%dT%H:%M:%SZ'), |
274 'header': _buildheader(), |
277 r'header': _buildheader(), |
275 } |
278 } |
276 } |
279 } |
277 |
280 |
278 yield rsp |
281 yield rsp |
279 |
282 |