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