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