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