##// END OF EJS Templates
git-lfs: make usage of gunicorn wrappers to write data to disk....
marcink -
r200:12e8c50a default
parent child Browse files
Show More
@@ -1,277 +1,287 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2017 RodeCode GmbH
2 # Copyright (C) 2014-2017 RodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import re
18 import re
19 import logging
19 import logging
20 from wsgiref.util import FileWrapper
20 from wsgiref.util import FileWrapper
21
21
22 import simplejson as json
22 import simplejson as json
23 from pyramid.config import Configurator
23 from pyramid.config import Configurator
24 from pyramid.response import Response, FileIter
24 from pyramid.response import Response, FileIter
25 from pyramid.httpexceptions import (
25 from pyramid.httpexceptions import (
26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
26 HTTPBadRequest, HTTPNotImplemented, HTTPNotFound, HTTPForbidden,
27 HTTPUnprocessableEntity)
27 HTTPUnprocessableEntity)
28
28
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
29 from vcsserver.git_lfs.lib import OidHandler, LFSOidStore
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
30 from vcsserver.git_lfs.utils import safe_result, get_cython_compat_decorator
31 from vcsserver.utils import safe_int
31 from vcsserver.utils import safe_int
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
36 GIT_LFS_CONTENT_TYPE = 'application/vnd.git-lfs' #+json ?
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
37 GIT_LFS_PROTO_PAT = re.compile(r'^/(.+)/(info/lfs/(.+))')
38
38
39
39
40 def write_response_error(http_exception, text=None):
40 def write_response_error(http_exception, text=None):
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
41 content_type = GIT_LFS_CONTENT_TYPE + '+json'
42 _exception = http_exception(content_type=content_type)
42 _exception = http_exception(content_type=content_type)
43 _exception.content_type = content_type
43 _exception.content_type = content_type
44 if text:
44 if text:
45 _exception.body = json.dumps({'message': text})
45 _exception.body = json.dumps({'message': text})
46 log.debug('LFS: writing response of type %s to client with text:%s',
46 log.debug('LFS: writing response of type %s to client with text:%s',
47 http_exception, text)
47 http_exception, text)
48 return _exception
48 return _exception
49
49
50
50
51 class AuthHeaderRequired(object):
51 class AuthHeaderRequired(object):
52 """
52 """
53 Decorator to check if request has proper auth-header
53 Decorator to check if request has proper auth-header
54 """
54 """
55
55
56 def __call__(self, func):
56 def __call__(self, func):
57 return get_cython_compat_decorator(self.__wrapper, func)
57 return get_cython_compat_decorator(self.__wrapper, func)
58
58
59 def __wrapper(self, func, *fargs, **fkwargs):
59 def __wrapper(self, func, *fargs, **fkwargs):
60 request = fargs[1]
60 request = fargs[1]
61 auth = request.authorization
61 auth = request.authorization
62 if not auth:
62 if not auth:
63 return write_response_error(HTTPForbidden)
63 return write_response_error(HTTPForbidden)
64 return func(*fargs[1:], **fkwargs)
64 return func(*fargs[1:], **fkwargs)
65
65
66
66
67 # views
67 # views
68
68
69 def lfs_objects(request):
69 def lfs_objects(request):
70 # indicate not supported, V1 API
70 # indicate not supported, V1 API
71 log.warning('LFS: v1 api not supported, reporting it back to client')
71 log.warning('LFS: v1 api not supported, reporting it back to client')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
72 return write_response_error(HTTPNotImplemented, 'LFS: v1 api not supported')
73
73
74
74
75 @AuthHeaderRequired()
75 @AuthHeaderRequired()
76 def lfs_objects_batch(request):
76 def lfs_objects_batch(request):
77 """
77 """
78 The client sends the following information to the Batch endpoint to transfer some objects:
78 The client sends the following information to the Batch endpoint to transfer some objects:
79
79
80 operation - Should be download or upload.
80 operation - Should be download or upload.
81 transfers - An optional Array of String identifiers for transfer
81 transfers - An optional Array of String identifiers for transfer
82 adapters that the client has configured. If omitted, the basic
82 adapters that the client has configured. If omitted, the basic
83 transfer adapter MUST be assumed by the server.
83 transfer adapter MUST be assumed by the server.
84 objects - An Array of objects to download.
84 objects - An Array of objects to download.
85 oid - String OID of the LFS object.
85 oid - String OID of the LFS object.
86 size - Integer byte size of the LFS object. Must be at least zero.
86 size - Integer byte size of the LFS object. Must be at least zero.
87 """
87 """
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
88 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
89 auth = request.authorization
89 auth = request.authorization
90 repo = request.matchdict.get('repo')
90 repo = request.matchdict.get('repo')
91 data = request.json
91 data = request.json
92 operation = data.get('operation')
92 operation = data.get('operation')
93 if operation not in ('download', 'upload'):
93 if operation not in ('download', 'upload'):
94 log.debug('LFS: unsupported operation:%s', operation)
94 log.debug('LFS: unsupported operation:%s', operation)
95 return write_response_error(
95 return write_response_error(
96 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
96 HTTPBadRequest, 'unsupported operation mode: `%s`' % operation)
97
97
98 if 'objects' not in data:
98 if 'objects' not in data:
99 log.debug('LFS: missing objects data')
99 log.debug('LFS: missing objects data')
100 return write_response_error(
100 return write_response_error(
101 HTTPBadRequest, 'missing objects data')
101 HTTPBadRequest, 'missing objects data')
102
102
103 log.debug('LFS: handling operation of type: %s', operation)
103 log.debug('LFS: handling operation of type: %s', operation)
104
104
105 objects = []
105 objects = []
106 for o in data['objects']:
106 for o in data['objects']:
107 try:
107 try:
108 oid = o['oid']
108 oid = o['oid']
109 obj_size = o['size']
109 obj_size = o['size']
110 except KeyError:
110 except KeyError:
111 log.exception('LFS, failed to extract data')
111 log.exception('LFS, failed to extract data')
112 return write_response_error(
112 return write_response_error(
113 HTTPBadRequest, 'unsupported data in objects')
113 HTTPBadRequest, 'unsupported data in objects')
114
114
115 obj_data = {'oid': oid}
115 obj_data = {'oid': oid}
116
116
117 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
117 obj_href = request.route_url('lfs_objects_oid', repo=repo, oid=oid)
118 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
118 obj_verify_href = request.route_url('lfs_objects_verify', repo=repo)
119 store = LFSOidStore(
119 store = LFSOidStore(
120 oid, repo, store_location=request.registry.git_lfs_store_path)
120 oid, repo, store_location=request.registry.git_lfs_store_path)
121 handler = OidHandler(
121 handler = OidHandler(
122 store, repo, auth, oid, obj_size, obj_data,
122 store, repo, auth, oid, obj_size, obj_data,
123 obj_href, obj_verify_href)
123 obj_href, obj_verify_href)
124
124
125 # this verifies also OIDs
125 # this verifies also OIDs
126 actions, errors = handler.exec_operation(operation)
126 actions, errors = handler.exec_operation(operation)
127 if errors:
127 if errors:
128 log.warning('LFS: got following errors: %s', errors)
128 log.warning('LFS: got following errors: %s', errors)
129 obj_data['errors'] = errors
129 obj_data['errors'] = errors
130
130
131 if actions:
131 if actions:
132 obj_data['actions'] = actions
132 obj_data['actions'] = actions
133
133
134 obj_data['size'] = obj_size
134 obj_data['size'] = obj_size
135 obj_data['authenticated'] = True
135 obj_data['authenticated'] = True
136 objects.append(obj_data)
136 objects.append(obj_data)
137
137
138 result = {'objects': objects, 'transfer': 'basic'}
138 result = {'objects': objects, 'transfer': 'basic'}
139 log.debug('LFS Response %s', safe_result(result))
139 log.debug('LFS Response %s', safe_result(result))
140
140
141 return result
141 return result
142
142
143
143
144 def lfs_objects_oid_upload(request):
144 def lfs_objects_oid_upload(request):
145 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
145 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
146 repo = request.matchdict.get('repo')
146 repo = request.matchdict.get('repo')
147 oid = request.matchdict.get('oid')
147 oid = request.matchdict.get('oid')
148 store = LFSOidStore(
148 store = LFSOidStore(
149 oid, repo, store_location=request.registry.git_lfs_store_path)
149 oid, repo, store_location=request.registry.git_lfs_store_path)
150 engine = store.get_engine(mode='wb')
150 engine = store.get_engine(mode='wb')
151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
151 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
152
153 body = request.environ['wsgi.input']
154
152 with engine as f:
155 with engine as f:
153 for chunk in FileWrapper(request.body_file_seekable, blksize=64 * 1024):
156 blksize = 64 * 1024 # 64kb
157 while True:
158 # read in chunks as stream comes in from Gunicorn
159 # this is a specific Gunicorn support function.
160 # might work differently on waitress
161 chunk = body.read(blksize)
162 if not chunk:
163 break
154 f.write(chunk)
164 f.write(chunk)
155
165
156 return {'upload': 'ok'}
166 return {'upload': 'ok'}
157
167
158
168
159 def lfs_objects_oid_download(request):
169 def lfs_objects_oid_download(request):
160 repo = request.matchdict.get('repo')
170 repo = request.matchdict.get('repo')
161 oid = request.matchdict.get('oid')
171 oid = request.matchdict.get('oid')
162
172
163 store = LFSOidStore(
173 store = LFSOidStore(
164 oid, repo, store_location=request.registry.git_lfs_store_path)
174 oid, repo, store_location=request.registry.git_lfs_store_path)
165 if not store.has_oid():
175 if not store.has_oid():
166 log.debug('LFS: oid %s does not exists in store', oid)
176 log.debug('LFS: oid %s does not exists in store', oid)
167 return write_response_error(
177 return write_response_error(
168 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
178 HTTPNotFound, 'requested file with oid `%s` not found in store' % oid)
169
179
170 # TODO(marcink): support range header ?
180 # TODO(marcink): support range header ?
171 # Range: bytes=0-, `bytes=(\d+)\-.*`
181 # Range: bytes=0-, `bytes=(\d+)\-.*`
172
182
173 f = open(store.oid_path, 'rb')
183 f = open(store.oid_path, 'rb')
174 response = Response(
184 response = Response(
175 content_type='application/octet-stream', app_iter=FileIter(f))
185 content_type='application/octet-stream', app_iter=FileIter(f))
176 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
186 response.headers.add('X-RC-LFS-Response-Oid', str(oid))
177 return response
187 return response
178
188
179
189
180 def lfs_objects_verify(request):
190 def lfs_objects_verify(request):
181 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
191 request.response.content_type = GIT_LFS_CONTENT_TYPE + '+json'
182 repo = request.matchdict.get('repo')
192 repo = request.matchdict.get('repo')
183
193
184 data = request.json
194 data = request.json
185 oid = data.get('oid')
195 oid = data.get('oid')
186 size = safe_int(data.get('size'))
196 size = safe_int(data.get('size'))
187
197
188 if not (oid and size):
198 if not (oid and size):
189 return write_response_error(
199 return write_response_error(
190 HTTPBadRequest, 'missing oid and size in request data')
200 HTTPBadRequest, 'missing oid and size in request data')
191
201
192 store = LFSOidStore(
202 store = LFSOidStore(
193 oid, repo, store_location=request.registry.git_lfs_store_path)
203 oid, repo, store_location=request.registry.git_lfs_store_path)
194 if not store.has_oid():
204 if not store.has_oid():
195 log.debug('LFS: oid %s does not exists in store', oid)
205 log.debug('LFS: oid %s does not exists in store', oid)
196 return write_response_error(
206 return write_response_error(
197 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
207 HTTPNotFound, 'oid `%s` does not exists in store' % oid)
198
208
199 store_size = store.size_oid()
209 store_size = store.size_oid()
200 if store_size != size:
210 if store_size != size:
201 msg = 'requested file size mismatch store size:%s requested:%s' % (
211 msg = 'requested file size mismatch store size:%s requested:%s' % (
202 store_size, size)
212 store_size, size)
203 return write_response_error(
213 return write_response_error(
204 HTTPUnprocessableEntity, msg)
214 HTTPUnprocessableEntity, msg)
205
215
206 return {'message': {'size': 'ok', 'in_store': 'ok'}}
216 return {'message': {'size': 'ok', 'in_store': 'ok'}}
207
217
208
218
209 def lfs_objects_lock(request):
219 def lfs_objects_lock(request):
210 return write_response_error(
220 return write_response_error(
211 HTTPNotImplemented, 'GIT LFS locking api not supported')
221 HTTPNotImplemented, 'GIT LFS locking api not supported')
212
222
213
223
214 def not_found(request):
224 def not_found(request):
215 return write_response_error(
225 return write_response_error(
216 HTTPNotFound, 'request path not found')
226 HTTPNotFound, 'request path not found')
217
227
218
228
219 def lfs_disabled(request):
229 def lfs_disabled(request):
220 return write_response_error(
230 return write_response_error(
221 HTTPNotImplemented, 'GIT LFS disabled for this repo')
231 HTTPNotImplemented, 'GIT LFS disabled for this repo')
222
232
223
233
224 def git_lfs_app(config):
234 def git_lfs_app(config):
225
235
226 # v1 API deprecation endpoint
236 # v1 API deprecation endpoint
227 config.add_route('lfs_objects',
237 config.add_route('lfs_objects',
228 '/{repo:.*?[^/]}/info/lfs/objects')
238 '/{repo:.*?[^/]}/info/lfs/objects')
229 config.add_view(lfs_objects, route_name='lfs_objects',
239 config.add_view(lfs_objects, route_name='lfs_objects',
230 request_method='POST', renderer='json')
240 request_method='POST', renderer='json')
231
241
232 # locking API
242 # locking API
233 config.add_route('lfs_objects_lock',
243 config.add_route('lfs_objects_lock',
234 '/{repo:.*?[^/]}/info/lfs/locks')
244 '/{repo:.*?[^/]}/info/lfs/locks')
235 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
245 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock',
236 request_method=('POST', 'GET'), renderer='json')
246 request_method=('POST', 'GET'), renderer='json')
237
247
238 config.add_route('lfs_objects_lock_verify',
248 config.add_route('lfs_objects_lock_verify',
239 '/{repo:.*?[^/]}/info/lfs/locks/verify')
249 '/{repo:.*?[^/]}/info/lfs/locks/verify')
240 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
250 config.add_view(lfs_objects_lock, route_name='lfs_objects_lock_verify',
241 request_method=('POST', 'GET'), renderer='json')
251 request_method=('POST', 'GET'), renderer='json')
242
252
243 # batch API
253 # batch API
244 config.add_route('lfs_objects_batch',
254 config.add_route('lfs_objects_batch',
245 '/{repo:.*?[^/]}/info/lfs/objects/batch')
255 '/{repo:.*?[^/]}/info/lfs/objects/batch')
246 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
256 config.add_view(lfs_objects_batch, route_name='lfs_objects_batch',
247 request_method='POST', renderer='json')
257 request_method='POST', renderer='json')
248
258
249 # oid upload/download API
259 # oid upload/download API
250 config.add_route('lfs_objects_oid',
260 config.add_route('lfs_objects_oid',
251 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
261 '/{repo:.*?[^/]}/info/lfs/objects/{oid}')
252 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
262 config.add_view(lfs_objects_oid_upload, route_name='lfs_objects_oid',
253 request_method='PUT', renderer='json')
263 request_method='PUT', renderer='json')
254 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
264 config.add_view(lfs_objects_oid_download, route_name='lfs_objects_oid',
255 request_method='GET', renderer='json')
265 request_method='GET', renderer='json')
256
266
257 # verification API
267 # verification API
258 config.add_route('lfs_objects_verify',
268 config.add_route('lfs_objects_verify',
259 '/{repo:.*?[^/]}/info/lfs/verify')
269 '/{repo:.*?[^/]}/info/lfs/verify')
260 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
270 config.add_view(lfs_objects_verify, route_name='lfs_objects_verify',
261 request_method='POST', renderer='json')
271 request_method='POST', renderer='json')
262
272
263 # not found handler for API
273 # not found handler for API
264 config.add_notfound_view(not_found, renderer='json')
274 config.add_notfound_view(not_found, renderer='json')
265
275
266
276
267 def create_app(git_lfs_enabled, git_lfs_store_path):
277 def create_app(git_lfs_enabled, git_lfs_store_path):
268 config = Configurator()
278 config = Configurator()
269 if git_lfs_enabled:
279 if git_lfs_enabled:
270 config.include(git_lfs_app)
280 config.include(git_lfs_app)
271 config.registry.git_lfs_store_path = git_lfs_store_path
281 config.registry.git_lfs_store_path = git_lfs_store_path
272 else:
282 else:
273 # not found handler for API, reporting disabled LFS support
283 # not found handler for API, reporting disabled LFS support
274 config.add_notfound_view(lfs_disabled, renderer='json')
284 config.add_notfound_view(lfs_disabled, renderer='json')
275
285
276 app = config.make_wsgi_app()
286 app = config.make_wsgi_app()
277 return app
287 return app
General Comments 0
You need to be logged in to leave comments. Login now