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