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