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