##// END OF EJS Templates
lfs: teach the blob server to handle --prefix
Matt Harbison -
r37635:b03f2e0f default
parent child Browse files
Show More
@@ -1,291 +1,291 b''
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
14 14 from mercurial.hgweb import (
15 15 common as hgwebcommon,
16 16 )
17 17
18 18 from mercurial import (
19 19 pycompat,
20 20 )
21 21
22 22 HTTP_OK = hgwebcommon.HTTP_OK
23 23 HTTP_CREATED = hgwebcommon.HTTP_CREATED
24 24 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
25 25 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
26 26
27 27 def handlewsgirequest(orig, rctx, req, res, checkperm):
28 28 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
29 29 request if it is left unprocessed by the wrapped method.
30 30 """
31 31 if orig(rctx, req, res, checkperm):
32 32 return True
33 33
34 34 if not rctx.repo.ui.configbool('experimental', 'lfs.serve'):
35 35 return False
36 36
37 37 if not req.dispatchpath:
38 38 return False
39 39
40 40 try:
41 41 if req.dispatchpath == b'.git/info/lfs/objects/batch':
42 42 checkperm(rctx, req, 'pull')
43 43 return _processbatchrequest(rctx.repo, req, res)
44 44 # TODO: reserve and use a path in the proposed http wireprotocol /api/
45 45 # namespace?
46 46 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
47 47 return _processbasictransfer(rctx.repo, req, res,
48 48 lambda perm:
49 49 checkperm(rctx, req, perm))
50 50 return False
51 51 except hgwebcommon.ErrorResponse as e:
52 52 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
53 53 # in the wrapped function. Should this be moved back to hgweb to
54 54 # be a common handler?
55 55 for k, v in e.headers:
56 56 res.headers[k] = v
57 57 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
58 58 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
59 59 return True
60 60
61 61 def _sethttperror(res, code, message=None):
62 62 res.status = hgwebcommon.statusmessage(code, message=message)
63 63 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
64 64 res.setbodybytes(b'')
65 65
66 66 def _processbatchrequest(repo, req, res):
67 67 """Handle a request for the Batch API, which is the gateway to granting file
68 68 access.
69 69
70 70 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
71 71 """
72 72
73 73 # Mercurial client request:
74 74 #
75 75 # HOST: localhost:$HGPORT
76 76 # ACCEPT: application/vnd.git-lfs+json
77 77 # ACCEPT-ENCODING: identity
78 78 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
79 79 # Content-Length: 125
80 80 # Content-Type: application/vnd.git-lfs+json
81 81 #
82 82 # {
83 83 # "objects": [
84 84 # {
85 85 # "oid": "31cf...8e5b"
86 86 # "size": 12
87 87 # }
88 88 # ]
89 89 # "operation": "upload"
90 90 # }
91 91
92 92 if (req.method != b'POST'
93 93 or req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json'
94 94 or req.headers[b'Accept'] != b'application/vnd.git-lfs+json'):
95 95 # TODO: figure out what the proper handling for a bad request to the
96 96 # Batch API is.
97 97 _sethttperror(res, HTTP_BAD_REQUEST, b'Invalid Batch API request')
98 98 return True
99 99
100 100 # XXX: specify an encoding?
101 101 lfsreq = json.loads(req.bodyfh.read())
102 102
103 103 # If no transfer handlers are explicitly requested, 'basic' is assumed.
104 104 if 'basic' not in lfsreq.get('transfers', ['basic']):
105 105 _sethttperror(res, HTTP_BAD_REQUEST,
106 106 b'Only the basic LFS transfer handler is supported')
107 107 return True
108 108
109 109 operation = lfsreq.get('operation')
110 110 if operation not in ('upload', 'download'):
111 111 _sethttperror(res, HTTP_BAD_REQUEST,
112 112 b'Unsupported LFS transfer operation: %s' % operation)
113 113 return True
114 114
115 115 localstore = repo.svfs.lfslocalblobstore
116 116
117 117 objects = [p for p in _batchresponseobjects(req, lfsreq.get('objects', []),
118 118 operation, localstore)]
119 119
120 120 rsp = {
121 121 'transfer': 'basic',
122 122 'objects': objects,
123 123 }
124 124
125 125 res.status = hgwebcommon.statusmessage(HTTP_OK)
126 126 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
127 127 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
128 128
129 129 return True
130 130
131 131 def _batchresponseobjects(req, objects, action, store):
132 132 """Yield one dictionary of attributes for the Batch API response for each
133 133 object in the list.
134 134
135 135 req: The parsedrequest for the Batch API request
136 136 objects: The list of objects in the Batch API object request list
137 137 action: 'upload' or 'download'
138 138 store: The local blob store for servicing requests"""
139 139
140 140 # Successful lfs-test-server response to solict an upload:
141 141 # {
142 142 # u'objects': [{
143 143 # u'size': 12,
144 144 # u'oid': u'31cf...8e5b',
145 145 # u'actions': {
146 146 # u'upload': {
147 147 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
148 148 # u'expires_at': u'0001-01-01T00:00:00Z',
149 149 # u'header': {
150 150 # u'Accept': u'application/vnd.git-lfs'
151 151 # }
152 152 # }
153 153 # }
154 154 # }]
155 155 # }
156 156
157 157 # TODO: Sort out the expires_at/expires_in/authenticated keys.
158 158
159 159 for obj in objects:
160 160 # Convert unicode to ASCII to create a filesystem path
161 161 oid = obj.get('oid').encode('ascii')
162 162 rsp = {
163 163 'oid': oid,
164 164 'size': obj.get('size'), # XXX: should this check the local size?
165 165 #'authenticated': True,
166 166 }
167 167
168 168 exists = True
169 169 verifies = False
170 170
171 171 # Verify an existing file on the upload request, so that the client is
172 172 # solicited to re-upload if it corrupt locally. Download requests are
173 173 # also verified, so the error can be flagged in the Batch API response.
174 174 # (Maybe we can use this to short circuit the download for `hg verify`,
175 175 # IFF the client can assert that the remote end is an hg server.)
176 176 # Otherwise, it's potentially overkill on download, since it is also
177 177 # verified as the file is streamed to the caller.
178 178 try:
179 179 verifies = store.verify(oid)
180 180 except IOError as inst:
181 181 if inst.errno != errno.ENOENT:
182 182 rsp['error'] = {
183 183 'code': 500,
184 184 'message': inst.strerror or 'Internal Server Server'
185 185 }
186 186 yield rsp
187 187 continue
188 188
189 189 exists = False
190 190
191 191 # Items are always listed for downloads. They are dropped for uploads
192 192 # IFF they already exist locally.
193 193 if action == 'download':
194 194 if not exists:
195 195 rsp['error'] = {
196 196 'code': 404,
197 197 'message': "The object does not exist"
198 198 }
199 199 yield rsp
200 200 continue
201 201
202 202 elif not verifies:
203 203 rsp['error'] = {
204 204 'code': 422, # XXX: is this the right code?
205 205 'message': "The object is corrupt"
206 206 }
207 207 yield rsp
208 208 continue
209 209
210 210 elif verifies:
211 211 yield rsp # Skip 'actions': already uploaded
212 212 continue
213 213
214 214 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
215 215
216 216 rsp['actions'] = {
217 217 '%s' % action: {
218 # TODO: Account for the --prefix, if any.
219 'href': '%s/.hg/lfs/objects/%s' % (req.baseurl, oid),
218 'href': '%s%s/.hg/lfs/objects/%s'
219 % (req.baseurl, req.apppath, oid),
220 220 # datetime.isoformat() doesn't include the 'Z' suffix
221 221 "expires_at": expiresat.strftime('%Y-%m-%dT%H:%M:%SZ'),
222 222 'header': {
223 223 # The spec doesn't mention the Accept header here, but avoid
224 224 # a gratuitous deviation from lfs-test-server in the test
225 225 # output.
226 226 'Accept': 'application/vnd.git-lfs'
227 227 }
228 228 }
229 229 }
230 230
231 231 yield rsp
232 232
233 233 def _processbasictransfer(repo, req, res, checkperm):
234 234 """Handle a single file upload (PUT) or download (GET) action for the Basic
235 235 Transfer Adapter.
236 236
237 237 After determining if the request is for an upload or download, the access
238 238 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
239 239 before accessing the files.
240 240
241 241 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
242 242 """
243 243
244 244 method = req.method
245 245 oid = req.dispatchparts[-1]
246 246 localstore = repo.svfs.lfslocalblobstore
247 247
248 248 if len(req.dispatchparts) != 4:
249 249 _sethttperror(res, HTTP_NOT_FOUND)
250 250 return True
251 251
252 252 if method == b'PUT':
253 253 checkperm('upload')
254 254
255 255 # TODO: verify Content-Type?
256 256
257 257 existed = localstore.has(oid)
258 258
259 259 # TODO: how to handle timeouts? The body proxy handles limiting to
260 260 # Content-Length, but what happens if a client sends less than it
261 261 # says it will?
262 262
263 263 # TODO: download() will abort if the checksum fails. It should raise
264 264 # something checksum specific that can be caught here, and turned
265 265 # into an http code.
266 266 localstore.download(oid, req.bodyfh)
267 267
268 268 statusmessage = hgwebcommon.statusmessage
269 269 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
270 270
271 271 # There's no payload here, but this is the header that lfs-test-server
272 272 # sends back. This eliminates some gratuitous test output conditionals.
273 273 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
274 274 res.setbodybytes(b'')
275 275
276 276 return True
277 277 elif method == b'GET':
278 278 checkperm('pull')
279 279
280 280 res.status = hgwebcommon.statusmessage(HTTP_OK)
281 281 res.headers[b'Content-Type'] = b'application/octet-stream'
282 282
283 283 # TODO: figure out how to send back the file in chunks, instead of
284 284 # reading the whole thing.
285 285 res.setbodybytes(localstore.read(oid))
286 286
287 287 return True
288 288 else:
289 289 _sethttperror(res, HTTP_BAD_REQUEST,
290 290 message=b'Unsupported LFS transfer method: %s' % method)
291 291 return True
@@ -1,67 +1,151 b''
1 1 #require serve no-reposimplestore
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > lfs=
6 6 > [lfs]
7 7 > url=http://localhost:$HGPORT/.git/info/lfs
8 8 > track=all()
9 9 > [web]
10 10 > push_ssl = False
11 11 > allow-push = *
12 12 > EOF
13 13
14 14 Serving LFS files can experimentally be turned off. The long term solution is
15 15 to support the 'verify' action in both client and server, so that the server can
16 16 tell the client to store files elsewhere.
17 17
18 18 $ hg init server
19 19 $ hg --config "lfs.usercache=$TESTTMP/servercache" \
20 20 > --config experimental.lfs.serve=False -R server serve -d \
21 21 > -p $HGPORT --pid-file=hg.pid -A $TESTTMP/access.log -E $TESTTMP/errors.log
22 22 $ cat hg.pid >> $DAEMON_PIDS
23 23
24 24 Uploads fail...
25 25
26 26 $ hg init client
27 27 $ echo 'this-is-an-lfs-file' > client/lfs.bin
28 28 $ hg -R client ci -Am 'initial commit'
29 29 adding lfs.bin
30 30 $ hg -R client push http://localhost:$HGPORT
31 31 pushing to http://localhost:$HGPORT/
32 32 searching for changes
33 33 abort: LFS HTTP error: HTTP Error 400: no such method: .git (action=upload)!
34 34 [255]
35 35
36 36 ... so do a local push to make the data available. Remove the blob from the
37 37 default cache, so it attempts to download.
38 38 $ hg --config "lfs.usercache=$TESTTMP/servercache" \
39 39 > --config "lfs.url=null://" \
40 40 > -R client push -q server
41 $ rm -rf `hg config lfs.usercache`
41 $ mv `hg config lfs.usercache` $TESTTMP/servercache
42 42
43 43 Downloads fail...
44 44
45 45 $ hg clone http://localhost:$HGPORT httpclone
46 46 requesting all changes
47 47 adding changesets
48 48 adding manifests
49 49 adding file changes
50 50 added 1 changesets with 1 changes to 1 files
51 51 new changesets 525251863cad
52 52 updating to branch default
53 53 abort: LFS HTTP error: HTTP Error 400: no such method: .git (action=download)!
54 54 [255]
55 55
56 56 $ $PYTHON $RUNTESTDIR/killdaemons.py $DAEMON_PIDS
57 57
58 58 $ cat $TESTTMP/access.log $TESTTMP/errors.log
59 59 $LOCALIP - - [$LOGDATE$] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
60 60 $LOCALIP - - [$LOGDATE$] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D525251863cad618e55d483555f3d00a2ca99597e x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
61 61 $LOCALIP - - [$LOGDATE$] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
62 62 $LOCALIP - - [$LOGDATE$] "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
63 63 $LOCALIP - - [$LOGDATE$] "POST /.git/info/lfs/objects/batch HTTP/1.1" 400 - (glob)
64 64 $LOCALIP - - [$LOGDATE$] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
65 65 $LOCALIP - - [$LOGDATE$] "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
66 66 $LOCALIP - - [$LOGDATE$] "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=525251863cad618e55d483555f3d00a2ca99597e&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
67 67 $LOCALIP - - [$LOGDATE$] "POST /.git/info/lfs/objects/batch HTTP/1.1" 400 - (glob)
68
69 Blob URIs are correct when --prefix is used
70
71 $ rm -f $TESTTMP/access.log $TESTTMP/errors.log
72 $ hg --config "lfs.usercache=$TESTTMP/servercache" -R server serve -d \
73 > -p $HGPORT --pid-file=hg.pid --prefix=subdir/mount/point \
74 > -A $TESTTMP/access.log -E $TESTTMP/errors.log
75 $ cat hg.pid >> $DAEMON_PIDS
76
77 $ hg --config lfs.url=http://localhost:$HGPORT/subdir/mount/point/.git/info/lfs \
78 > clone --debug http://localhost:$HGPORT/subdir/mount/point cloned2
79 using http://localhost:$HGPORT/subdir/mount/point
80 sending capabilities command
81 query 1; heads
82 sending batch command
83 requesting all changes
84 sending getbundle command
85 bundle2-input-bundle: with-transaction
86 bundle2-input-part: "changegroup" (params: 1 mandatory 1 advisory) supported
87 adding changesets
88 add changeset 525251863cad
89 adding manifests
90 adding file changes
91 adding lfs.bin revisions
92 added 1 changesets with 1 changes to 1 files
93 calling hook pretxnchangegroup.lfs: hgext.lfs.checkrequireslfs
94 bundle2-input-part: total payload size 648
95 bundle2-input-part: "listkeys" (params: 1 mandatory) supported
96 bundle2-input-part: "phase-heads" supported
97 bundle2-input-part: total payload size 24
98 bundle2-input-part: "cache:rev-branch-cache" supported
99 bundle2-input-part: total payload size 39
100 bundle2-input-bundle: 3 parts total
101 checking for updated bookmarks
102 updating the branch cache
103 new changesets 525251863cad
104 updating to branch default
105 resolving manifests
106 branchmerge: False, force: False, partial: False
107 ancestor: 000000000000, local: 000000000000+, remote: 525251863cad
108 Status: 200
109 Content-Length: 371
110 Content-Type: application/vnd.git-lfs+json
111 Date: $HTTP_DATE$
112 Server: testing stub value
113 {
114 "objects": [
115 {
116 "actions": {
117 "download": {
118 "expires_at": "$ISO_8601_DATE_TIME$"
119 "header": {
120 "Accept": "application/vnd.git-lfs"
121 }
122 "href": "http://localhost:$HGPORT/subdir/mount/point/.hg/lfs/objects/f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e"
123 }
124 }
125 "oid": "f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e"
126 "size": 20
127 }
128 ]
129 "transfer": "basic"
130 }
131 lfs: downloading f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e (20 bytes)
132 Status: 200
133 Content-Length: 20
134 Content-Type: application/octet-stream
135 Date: $HTTP_DATE$
136 Server: testing stub value
137 lfs: adding f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e to the usercache
138 lfs: processed: f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e
139 lfs.bin: remote created -> g
140 getting lfs.bin
141 lfs: found f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e in the local lfs store
142 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
143
144 $ $PYTHON $RUNTESTDIR/killdaemons.py $DAEMON_PIDS
145
146 $ cat $TESTTMP/access.log $TESTTMP/errors.log
147 $LOCALIP - - [$LOGDATE$] "GET /subdir/mount/point?cmd=capabilities HTTP/1.1" 200 - (glob)
148 $LOCALIP - - [$LOGDATE$] "GET /subdir/mount/point?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
149 $LOCALIP - - [$LOGDATE$] "GET /subdir/mount/point?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&bundlecaps=HG20%2Cbundle2%3DHG20%250Abookmarks%250Achangegroup%253D01%252C02%252C03%250Adigests%253Dmd5%252Csha1%252Csha512%250Aerror%253Dabort%252Cunsupportedcontent%252Cpushraced%252Cpushkey%250Ahgtagsfnodes%250Alistkeys%250Aphases%253Dheads%250Apushkey%250Aremote-changegroup%253Dhttp%252Chttps%250Arev-branch-cache%250Astream%253Dv2&cg=1&common=0000000000000000000000000000000000000000&heads=525251863cad618e55d483555f3d00a2ca99597e&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$ partial-pull (glob)
150 $LOCALIP - - [$LOGDATE$] "POST /subdir/mount/point/.git/info/lfs/objects/batch HTTP/1.1" 200 - (glob)
151 $LOCALIP - - [$LOGDATE$] "GET /subdir/mount/point/.hg/lfs/objects/f03217a32529a28a42d03b1244fe09b6e0f9fd06d7b966d4d50567be2abe6c0e HTTP/1.1" 200 - (glob)
General Comments 0
You need to be logged in to leave comments. Login now