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