##// END OF EJS Templates
file-store: rename module from upload_store to file_store.
marcink -
r3453:c25a3725 default
parent child Browse files
Show More
@@ -1,481 +1,481 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import inspect
22 22 import logging
23 23 import itertools
24 24 import base64
25 25
26 26 from pyramid import compat
27 27
28 28 from rhodecode.api import (
29 29 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
30 30
31 31 from rhodecode.api.utils import (
32 32 Optional, OAttr, has_superadmin_permission, get_user_or_error)
33 33 from rhodecode.lib.utils import repo2db_mapper
34 34 from rhodecode.lib import system_info
35 35 from rhodecode.lib import user_sessions
36 36 from rhodecode.lib import exc_tracking
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.utils2 import safe_int
39 39 from rhodecode.model.db import UserIpMap
40 40 from rhodecode.model.scm import ScmModel
41 41 from rhodecode.model.settings import VcsSettingsModel
42 from rhodecode.apps.upload_store import utils
43 from rhodecode.apps.upload_store.exceptions import FileNotAllowedException, \
42 from rhodecode.apps.file_store import utils
43 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
44 44 FileOverSizeException
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 @jsonrpc_method()
50 50 def get_server_info(request, apiuser):
51 51 """
52 52 Returns the |RCE| server information.
53 53
54 54 This includes the running version of |RCE| and all installed
55 55 packages. This command takes the following options:
56 56
57 57 :param apiuser: This is filled automatically from the |authtoken|.
58 58 :type apiuser: AuthUser
59 59
60 60 Example output:
61 61
62 62 .. code-block:: bash
63 63
64 64 id : <id_given_in_input>
65 65 result : {
66 66 'modules': [<module name>,...]
67 67 'py_version': <python version>,
68 68 'platform': <platform type>,
69 69 'rhodecode_version': <rhodecode version>
70 70 }
71 71 error : null
72 72 """
73 73
74 74 if not has_superadmin_permission(apiuser):
75 75 raise JSONRPCForbidden()
76 76
77 77 server_info = ScmModel().get_server_info(request.environ)
78 78 # rhodecode-index requires those
79 79
80 80 server_info['index_storage'] = server_info['search']['value']['location']
81 81 server_info['storage'] = server_info['storage']['value']['path']
82 82
83 83 return server_info
84 84
85 85
86 86 @jsonrpc_method()
87 87 def get_repo_store(request, apiuser):
88 88 """
89 89 Returns the |RCE| repository storage information.
90 90
91 91 :param apiuser: This is filled automatically from the |authtoken|.
92 92 :type apiuser: AuthUser
93 93
94 94 Example output:
95 95
96 96 .. code-block:: bash
97 97
98 98 id : <id_given_in_input>
99 99 result : {
100 100 'modules': [<module name>,...]
101 101 'py_version': <python version>,
102 102 'platform': <platform type>,
103 103 'rhodecode_version': <rhodecode version>
104 104 }
105 105 error : null
106 106 """
107 107
108 108 if not has_superadmin_permission(apiuser):
109 109 raise JSONRPCForbidden()
110 110
111 111 path = VcsSettingsModel().get_repos_location()
112 112 return {"path": path}
113 113
114 114
115 115 @jsonrpc_method()
116 116 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
117 117 """
118 118 Displays the IP Address as seen from the |RCE| server.
119 119
120 120 * This command displays the IP Address, as well as all the defined IP
121 121 addresses for the specified user. If the ``userid`` is not set, the
122 122 data returned is for the user calling the method.
123 123
124 124 This command can only be run using an |authtoken| with admin rights to
125 125 the specified repository.
126 126
127 127 This command takes the following options:
128 128
129 129 :param apiuser: This is filled automatically from |authtoken|.
130 130 :type apiuser: AuthUser
131 131 :param userid: Sets the userid for which associated IP Address data
132 132 is returned.
133 133 :type userid: Optional(str or int)
134 134
135 135 Example output:
136 136
137 137 .. code-block:: bash
138 138
139 139 id : <id_given_in_input>
140 140 result : {
141 141 "server_ip_addr": "<ip_from_clien>",
142 142 "user_ips": [
143 143 {
144 144 "ip_addr": "<ip_with_mask>",
145 145 "ip_range": ["<start_ip>", "<end_ip>"],
146 146 },
147 147 ...
148 148 ]
149 149 }
150 150
151 151 """
152 152 if not has_superadmin_permission(apiuser):
153 153 raise JSONRPCForbidden()
154 154
155 155 userid = Optional.extract(userid, evaluate_locals=locals())
156 156 userid = getattr(userid, 'user_id', userid)
157 157
158 158 user = get_user_or_error(userid)
159 159 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
160 160 return {
161 161 'server_ip_addr': request.rpc_ip_addr,
162 162 'user_ips': ips
163 163 }
164 164
165 165
166 166 @jsonrpc_method()
167 167 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
168 168 """
169 169 Triggers a rescan of the specified repositories.
170 170
171 171 * If the ``remove_obsolete`` option is set, it also deletes repositories
172 172 that are found in the database but not on the file system, so called
173 173 "clean zombies".
174 174
175 175 This command can only be run using an |authtoken| with admin rights to
176 176 the specified repository.
177 177
178 178 This command takes the following options:
179 179
180 180 :param apiuser: This is filled automatically from the |authtoken|.
181 181 :type apiuser: AuthUser
182 182 :param remove_obsolete: Deletes repositories from the database that
183 183 are not found on the filesystem.
184 184 :type remove_obsolete: Optional(``True`` | ``False``)
185 185
186 186 Example output:
187 187
188 188 .. code-block:: bash
189 189
190 190 id : <id_given_in_input>
191 191 result : {
192 192 'added': [<added repository name>,...]
193 193 'removed': [<removed repository name>,...]
194 194 }
195 195 error : null
196 196
197 197 Example error output:
198 198
199 199 .. code-block:: bash
200 200
201 201 id : <id_given_in_input>
202 202 result : null
203 203 error : {
204 204 'Error occurred during rescan repositories action'
205 205 }
206 206
207 207 """
208 208 if not has_superadmin_permission(apiuser):
209 209 raise JSONRPCForbidden()
210 210
211 211 try:
212 212 rm_obsolete = Optional.extract(remove_obsolete)
213 213 added, removed = repo2db_mapper(ScmModel().repo_scan(),
214 214 remove_obsolete=rm_obsolete)
215 215 return {'added': added, 'removed': removed}
216 216 except Exception:
217 217 log.exception('Failed to run repo rescann')
218 218 raise JSONRPCError(
219 219 'Error occurred during rescan repositories action'
220 220 )
221 221
222 222
223 223 @jsonrpc_method()
224 224 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
225 225 """
226 226 Triggers a session cleanup action.
227 227
228 228 If the ``older_then`` option is set, only sessions that hasn't been
229 229 accessed in the given number of days will be removed.
230 230
231 231 This command can only be run using an |authtoken| with admin rights to
232 232 the specified repository.
233 233
234 234 This command takes the following options:
235 235
236 236 :param apiuser: This is filled automatically from the |authtoken|.
237 237 :type apiuser: AuthUser
238 238 :param older_then: Deletes session that hasn't been accessed
239 239 in given number of days.
240 240 :type older_then: Optional(int)
241 241
242 242 Example output:
243 243
244 244 .. code-block:: bash
245 245
246 246 id : <id_given_in_input>
247 247 result: {
248 248 "backend": "<type of backend>",
249 249 "sessions_removed": <number_of_removed_sessions>
250 250 }
251 251 error : null
252 252
253 253 Example error output:
254 254
255 255 .. code-block:: bash
256 256
257 257 id : <id_given_in_input>
258 258 result : null
259 259 error : {
260 260 'Error occurred during session cleanup'
261 261 }
262 262
263 263 """
264 264 if not has_superadmin_permission(apiuser):
265 265 raise JSONRPCForbidden()
266 266
267 267 older_then = safe_int(Optional.extract(older_then)) or 60
268 268 older_than_seconds = 60 * 60 * 24 * older_then
269 269
270 270 config = system_info.rhodecode_config().get_value()['value']['config']
271 271 session_model = user_sessions.get_session_handler(
272 272 config.get('beaker.session.type', 'memory'))(config)
273 273
274 274 backend = session_model.SESSION_TYPE
275 275 try:
276 276 cleaned = session_model.clean_sessions(
277 277 older_than_seconds=older_than_seconds)
278 278 return {'sessions_removed': cleaned, 'backend': backend}
279 279 except user_sessions.CleanupCommand as msg:
280 280 return {'cleanup_command': msg.message, 'backend': backend}
281 281 except Exception as e:
282 282 log.exception('Failed session cleanup')
283 283 raise JSONRPCError(
284 284 'Error occurred during session cleanup'
285 285 )
286 286
287 287
288 288 @jsonrpc_method()
289 289 def get_method(request, apiuser, pattern=Optional('*')):
290 290 """
291 291 Returns list of all available API methods. By default match pattern
292 292 os "*" but any other pattern can be specified. eg *comment* will return
293 293 all methods with comment inside them. If just single method is matched
294 294 returned data will also include method specification
295 295
296 296 This command can only be run using an |authtoken| with admin rights to
297 297 the specified repository.
298 298
299 299 This command takes the following options:
300 300
301 301 :param apiuser: This is filled automatically from the |authtoken|.
302 302 :type apiuser: AuthUser
303 303 :param pattern: pattern to match method names against
304 304 :type pattern: Optional("*")
305 305
306 306 Example output:
307 307
308 308 .. code-block:: bash
309 309
310 310 id : <id_given_in_input>
311 311 "result": [
312 312 "changeset_comment",
313 313 "comment_pull_request",
314 314 "comment_commit"
315 315 ]
316 316 error : null
317 317
318 318 .. code-block:: bash
319 319
320 320 id : <id_given_in_input>
321 321 "result": [
322 322 "comment_commit",
323 323 {
324 324 "apiuser": "<RequiredType>",
325 325 "comment_type": "<Optional:u'note'>",
326 326 "commit_id": "<RequiredType>",
327 327 "message": "<RequiredType>",
328 328 "repoid": "<RequiredType>",
329 329 "request": "<RequiredType>",
330 330 "resolves_comment_id": "<Optional:None>",
331 331 "status": "<Optional:None>",
332 332 "userid": "<Optional:<OptionalAttr:apiuser>>"
333 333 }
334 334 ]
335 335 error : null
336 336 """
337 337 if not has_superadmin_permission(apiuser):
338 338 raise JSONRPCForbidden()
339 339
340 340 pattern = Optional.extract(pattern)
341 341
342 342 matches = find_methods(request.registry.jsonrpc_methods, pattern)
343 343
344 344 args_desc = []
345 345 if len(matches) == 1:
346 346 func = matches[matches.keys()[0]]
347 347
348 348 argspec = inspect.getargspec(func)
349 349 arglist = argspec[0]
350 350 defaults = map(repr, argspec[3] or [])
351 351
352 352 default_empty = '<RequiredType>'
353 353
354 354 # kw arguments required by this method
355 355 func_kwargs = dict(itertools.izip_longest(
356 356 reversed(arglist), reversed(defaults), fillvalue=default_empty))
357 357 args_desc.append(func_kwargs)
358 358
359 359 return matches.keys() + args_desc
360 360
361 361
362 362 @jsonrpc_method()
363 363 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
364 364 """
365 365 Stores sent exception inside the built-in exception tracker in |RCE| server.
366 366
367 367 This command can only be run using an |authtoken| with admin rights to
368 368 the specified repository.
369 369
370 370 This command takes the following options:
371 371
372 372 :param apiuser: This is filled automatically from the |authtoken|.
373 373 :type apiuser: AuthUser
374 374
375 375 :param exc_data_json: JSON data with exception e.g
376 376 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
377 377 :type exc_data_json: JSON data
378 378
379 379 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
380 380 :type prefix: Optional("rhodecode")
381 381
382 382 Example output:
383 383
384 384 .. code-block:: bash
385 385
386 386 id : <id_given_in_input>
387 387 "result": {
388 388 "exc_id": 139718459226384,
389 389 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
390 390 }
391 391 error : null
392 392 """
393 393 if not has_superadmin_permission(apiuser):
394 394 raise JSONRPCForbidden()
395 395
396 396 prefix = Optional.extract(prefix)
397 397 exc_id = exc_tracking.generate_id()
398 398
399 399 try:
400 400 exc_data = json.loads(exc_data_json)
401 401 except Exception:
402 402 log.error('Failed to parse JSON: %r', exc_data_json)
403 403 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
404 404 'Please make sure it contains a valid JSON.')
405 405
406 406 try:
407 407 exc_traceback = exc_data['exc_traceback']
408 408 exc_type_name = exc_data['exc_type_name']
409 409 except KeyError as err:
410 410 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
411 411 'in exc_data_json field. Missing: {}'.format(err))
412 412
413 413 exc_tracking._store_exception(
414 414 exc_id=exc_id, exc_traceback=exc_traceback,
415 415 exc_type_name=exc_type_name, prefix=prefix)
416 416
417 417 exc_url = request.route_url(
418 418 'admin_settings_exception_tracker_show', exception_id=exc_id)
419 419 return {'exc_id': exc_id, 'exc_url': exc_url}
420 420
421 421
422 422 @jsonrpc_method()
423 423 def upload_file(request, apiuser, filename, content):
424 424 """
425 425 Upload API for the file_store
426 426
427 427 Example usage from CLI::
428 428 rhodecode-api --instance-name=enterprise-1 upload_file "{\"content\": \"$(cat image.jpg | base64)\", \"filename\":\"image.jpg\"}"
429 429
430 430
431 431 This command can only be run using an |authtoken| with admin rights to
432 432 the specified repository.
433 433
434 434 This command takes the following options:
435 435
436 436 :param apiuser: This is filled automatically from the |authtoken|.
437 437 :type apiuser: AuthUser
438 438 :param filename: name of the file uploaded
439 439 :type filename: str
440 440 :param content: base64 encoded content of the uploaded file
441 441 :type prefix: str
442 442
443 443 Example output:
444 444
445 445 .. code-block:: bash
446 446
447 447 id : <id_given_in_input>
448 448 result: {
449 449 "access_path": "/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
450 450 "access_path_fqn": "http://server.domain.com/_file_store/download/84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg",
451 451 "store_fid": "84d156f7-8323-4ad3-9fce-4a8e88e1deaf-0.jpg"
452 452 }
453 453 error : null
454 454 """
455 455 if not has_superadmin_permission(apiuser):
456 456 raise JSONRPCForbidden()
457 457
458 458 storage = utils.get_file_storage(request.registry.settings)
459 459
460 460 try:
461 461 file_obj = compat.NativeIO(base64.decodestring(content))
462 462 except Exception as exc:
463 463 raise JSONRPCError('File `{}` content decoding error: {}.'.format(filename, exc))
464 464
465 465 metadata = {
466 466 'filename': filename,
467 467 'size': '', # filled by save_file
468 468 'user_uploaded': {'username': apiuser.username,
469 469 'user_id': apiuser.user_id,
470 470 'ip': apiuser.ip_addr}}
471 471 try:
472 store_fid = storage.save_file(file_obj, filename, metadata=metadata)
472 store_fid, metadata = storage.save_file(file_obj, filename, metadata=metadata)
473 473 except FileNotAllowedException:
474 474 raise JSONRPCError('File `{}` is not allowed.'.format(filename))
475 475
476 476 except FileOverSizeException:
477 477 raise JSONRPCError('File `{}` is exceeding allowed limit.'.format(filename))
478 478
479 479 return {'store_fid': store_fid,
480 480 'access_path_fqn': request.route_url('download_file', fid=store_fid),
481 481 'access_path': request.route_path('download_file', fid=store_fid)}
@@ -1,49 +1,49 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 from rhodecode.apps.upload_store import config_keys
21 from rhodecode.apps.file_store import config_keys
22 22 from rhodecode.config.middleware import _bool_setting, _string_setting
23 23
24 24
25 25 def _sanitize_settings_and_apply_defaults(settings):
26 26 """
27 27 Set defaults, convert to python types and validate settings.
28 28 """
29 29 _bool_setting(settings, config_keys.enabled, 'true')
30 30
31 31 _string_setting(settings, config_keys.backend, 'local')
32 32
33 33 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
34 34 _string_setting(settings, config_keys.store_path, default_store)
35 35
36 36
37 37 def includeme(config):
38 38 settings = config.registry.settings
39 39 _sanitize_settings_and_apply_defaults(settings)
40 40
41 41 config.add_route(
42 42 name='upload_file',
43 43 pattern='/_file_store/upload')
44 44 config.add_route(
45 45 name='download_file',
46 46 pattern='/_file_store/download/{fid}')
47 47
48 48 # Scan module for configuration decorators.
49 49 config.scan('.views', ignore='.tests')
1 NO CONTENT: file renamed from rhodecode/apps/upload_store/config_keys.py to rhodecode/apps/file_store/config_keys.py
1 NO CONTENT: file renamed from rhodecode/apps/upload_store/exceptions.py to rhodecode/apps/file_store/exceptions.py
1 NO CONTENT: file renamed from rhodecode/apps/upload_store/extensions.py to rhodecode/apps/file_store/extensions.py
@@ -1,171 +1,175 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import shutil
24 24
25 25 from rhodecode.lib.ext_json import json
26 from rhodecode.apps.upload_store import utils
27 from rhodecode.apps.upload_store.extensions import resolve_extensions
28 from rhodecode.apps.upload_store.exceptions import FileNotAllowedException
26 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store.extensions import resolve_extensions
28 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
29 29
30 30 METADATA_VER = 'v1'
31 31
32 32
33 33 class LocalFileStorage(object):
34 34
35 35 @classmethod
36 36 def resolve_name(cls, name, directory):
37 37 """
38 38 Resolves a unique name and the correct path. If a filename
39 39 for that path already exists then a numeric prefix with values > 0 will be
40 40 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
41 41
42 42 :param name: base name of file
43 43 :param directory: absolute directory path
44 44 """
45 45
46 46 basename, ext = os.path.splitext(name)
47 47 counter = 0
48 48 while True:
49 49 name = '%s-%d%s' % (basename, counter, ext)
50 50 path = os.path.join(directory, name)
51 51 if not os.path.exists(path):
52 52 return name, path
53 53 counter += 1
54 54
55 55 def __init__(self, base_path, extension_groups=None):
56 56
57 57 """
58 58 Local file storage
59 59
60 60 :param base_path: the absolute base path where uploads are stored
61 61 :param extension_groups: extensions string
62 62 """
63 63
64 64 extension_groups = extension_groups or ['any']
65 65 self.base_path = base_path
66 66 self.extensions = resolve_extensions([], groups=extension_groups)
67 67
68 68 def store_path(self, filename):
69 69 """
70 70 Returns absolute file path of the filename, joined to the
71 71 base_path.
72 72
73 73 :param filename: base name of file
74 74 """
75 75 return os.path.join(self.base_path, filename)
76 76
77 77 def delete(self, filename):
78 78 """
79 79 Deletes the filename. Filename is resolved with the
80 80 absolute path based on base_path. If file does not exist,
81 81 returns **False**, otherwise **True**
82 82
83 83 :param filename: base name of file
84 84 """
85 85 if self.exists(filename):
86 86 os.remove(self.store_path(filename))
87 87 return True
88 88 return False
89 89
90 90 def exists(self, filename):
91 91 """
92 92 Checks if file exists. Resolves filename's absolute
93 93 path based on base_path.
94 94
95 95 :param filename: base name of file
96 96 """
97 97 return os.path.exists(self.store_path(filename))
98 98
99 99 def filename_allowed(self, filename, extensions=None):
100 100 """Checks if a filename has an allowed extension
101 101
102 102 :param filename: base name of file
103 103 :param extensions: iterable of extensions (or self.extensions)
104 104 """
105 105 _, ext = os.path.splitext(filename)
106 106 return self.extension_allowed(ext, extensions)
107 107
108 108 def extension_allowed(self, ext, extensions=None):
109 109 """
110 110 Checks if an extension is permitted. Both e.g. ".jpg" and
111 111 "jpg" can be passed in. Extension lookup is case-insensitive.
112 112
113 :param extensions: iterable of extensions (or self.extensions)
113 :param ext: extension to check
114 :param extensions: iterable of extensions to validate against (or self.extensions)
114 115 """
115 116
116 117 extensions = extensions or self.extensions
117 118 if not extensions:
118 119 return True
119 120 if ext.startswith('.'):
120 121 ext = ext[1:]
121 122 return ext.lower() in extensions
122 123
123 124 def save_file(self, file_obj, filename, directory=None, extensions=None,
124 125 metadata=None, **kwargs):
125 126 """
126 127 Saves a file object to the uploads location.
127 128 Returns the resolved filename, i.e. the directory +
128 129 the (randomized/incremented) base name.
129 130
130 131 :param file_obj: **cgi.FieldStorage** object (or similar)
131 132 :param filename: original filename
132 133 :param directory: relative path of sub-directory
133 134 :param extensions: iterable of allowed extensions, if not default
134 135 :param metadata: JSON metadata to store next to the file with .meta suffix
135 136 :returns: modified filename
136 137 """
137 138
138 139 extensions = extensions or self.extensions
139 140
140 141 if not self.filename_allowed(filename, extensions):
141 142 raise FileNotAllowedException()
142 143
143 144 if directory:
144 145 dest_directory = os.path.join(self.base_path, directory)
145 146 else:
146 147 dest_directory = self.base_path
147 148
148 149 if not os.path.exists(dest_directory):
149 150 os.makedirs(dest_directory)
150 151
151 152 filename = utils.uid_filename(filename)
152 153
153 154 filename, path = self.resolve_name(filename, dest_directory)
154 155 filename_meta = filename + '.meta'
155 156
156 157 file_obj.seek(0)
157 158
158 159 with open(path, "wb") as dest:
159 160 shutil.copyfileobj(file_obj, dest)
160 161
161 162 if metadata:
162 163 size = os.stat(path).st_size
163 metadata.update({'size': size, "time": time.time(),
164 "meta_ver": METADATA_VER})
164 metadata.update(
165 {"size": size,
166 "time": time.time(),
167 "meta_ver": METADATA_VER})
168
165 169 with open(os.path.join(dest_directory, filename_meta), "wb") as dest_meta:
166 170 dest_meta.write(json.dumps(metadata))
167 171
168 172 if directory:
169 173 filename = os.path.join(directory, filename)
170 174
171 return filename
175 return filename, metadata
1 NO CONTENT: file renamed from rhodecode/apps/upload_store/tests/__init__.py to rhodecode/apps/file_store/tests/__init__.py
@@ -1,110 +1,110 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 import pytest
22 22
23 23 from rhodecode.lib.ext_json import json
24 24 from rhodecode.tests import TestController
25 from rhodecode.apps.upload_store import utils, config_keys
25 from rhodecode.apps.file_store import utils, config_keys
26 26
27 27
28 28 def route_path(name, params=None, **kwargs):
29 29 import urllib
30 30
31 31 base_url = {
32 32 'upload_file': '/_file_store/upload',
33 33 'download_file': '/_file_store/download/{fid}',
34 34
35 35 }[name].format(**kwargs)
36 36
37 37 if params:
38 38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 39 return base_url
40 40
41 41
42 42 class TestFileStoreViews(TestController):
43 43
44 44 @pytest.mark.parametrize("fid, content, exists", [
45 45 ('abcde-0.jpg', "xxxxx", True),
46 46 ('abcde-0.exe', "1234567", True),
47 47 ('abcde-0.jpg', "xxxxx", False),
48 48 ])
49 49 def test_get_files_from_store(self, fid, content, exists, tmpdir):
50 50 self.log_user()
51 51 store_path = self.app._pyramid_settings[config_keys.store_path]
52 52
53 53 if exists:
54 54 status = 200
55 55 store = utils.get_file_storage({config_keys.store_path: store_path})
56 56 filesystem_file = os.path.join(str(tmpdir), fid)
57 57 with open(filesystem_file, 'wb') as f:
58 58 f.write(content)
59 59
60 60 with open(filesystem_file, 'rb') as f:
61 fid = store.save_file(f, fid, metadata={'filename': fid})
61 fid, metadata = store.save_file(f, fid, metadata={'filename': fid})
62 62
63 63 else:
64 64 status = 404
65 65
66 66 response = self.app.get(route_path('download_file', fid=fid), status=status)
67 67
68 68 if exists:
69 69 assert response.text == content
70 70 metadata = os.path.join(store_path, fid + '.meta')
71 71 assert os.path.exists(metadata)
72 72 with open(metadata, 'rb') as f:
73 73 json_data = json.loads(f.read())
74 74
75 75 assert json_data
76 76 assert 'size' in json_data
77 77
78 78 def test_upload_files_without_content_to_store(self):
79 79 self.log_user()
80 80 response = self.app.post(
81 81 route_path('upload_file'),
82 82 params={'csrf_token': self.csrf_token},
83 83 status=200)
84 84
85 85 assert response.json == {
86 86 u'error': u'store_file data field is missing',
87 87 u'access_path': None,
88 88 u'store_fid': None}
89 89
90 90 def test_upload_files_bogus_content_to_store(self):
91 91 self.log_user()
92 92 response = self.app.post(
93 93 route_path('upload_file'),
94 94 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
95 95 status=200)
96 96
97 97 assert response.json == {
98 98 u'error': u'filename cannot be read from the data field',
99 99 u'access_path': None,
100 100 u'store_fid': None}
101 101
102 102 def test_upload_content_to_store(self):
103 103 self.log_user()
104 104 response = self.app.post(
105 105 route_path('upload_file'),
106 106 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
107 107 params={'csrf_token': self.csrf_token},
108 108 status=200)
109 109
110 110 assert response.json['store_fid']
@@ -1,47 +1,47 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import os
23 23 import uuid
24 24
25 25
26 26 def get_file_storage(settings):
27 from rhodecode.apps.upload_store.local_store import LocalFileStorage
28 from rhodecode.apps.upload_store import config_keys
27 from rhodecode.apps.file_store.local_store import LocalFileStorage
28 from rhodecode.apps.file_store import config_keys
29 29 store_path = settings.get(config_keys.store_path)
30 30 return LocalFileStorage(base_path=store_path)
31 31
32 32
33 33 def uid_filename(filename, randomized=True):
34 34 """
35 35 Generates a randomized or stable (uuid) filename,
36 36 preserving the original extension.
37 37
38 38 :param filename: the original filename
39 39 :param randomized: define if filename should be stable (sha1 based) or randomized
40 40 """
41 41 _, ext = os.path.splitext(filename)
42 42 if randomized:
43 43 uid = uuid.uuid4()
44 44 else:
45 45 hash_key = '{}.{}'.format(filename, 'store')
46 46 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
47 47 return str(uid) + ext.lower()
@@ -1,97 +1,96 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22 from pyramid.view import view_config
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps.upload_store import utils
28 from rhodecode.apps.upload_store.exceptions import (
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException,FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous)
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class FileStoreView(BaseAppView):
39 39 upload_key = 'store_file'
40 40
41 41 def load_default_context(self):
42 42 c = self._get_local_tmpl_context()
43 43 self.storage = utils.get_file_storage(self.request.registry.settings)
44 44 return c
45 45
46 46 @NotAnonymous()
47 47 @CSRFRequired()
48 48 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
49 49 def upload_file(self):
50 50 self.load_default_context()
51 51 file_obj = self.request.POST.get(self.upload_key)
52 52
53 53 if file_obj is None:
54 54 return {'store_fid': None,
55 55 'access_path': None,
56 56 'error': '{} data field is missing'.format(self.upload_key)}
57 57
58 58 if not hasattr(file_obj, 'filename'):
59 59 return {'store_fid': None,
60 60 'access_path': None,
61 61 'error': 'filename cannot be read from the data field'}
62 62
63 63 filename = file_obj.filename
64 64
65 65 metadata = {
66 66 'filename': filename,
67 67 'size': '', # filled by save_file
68 68 'user_uploaded': {'username': self._rhodecode_user.username,
69 69 'user_id': self._rhodecode_user.user_id,
70 70 'ip': self._rhodecode_user.ip_addr}}
71 71 try:
72 store_fid = self.storage.save_file(file_obj.file, filename,
73 metadata=metadata)
72 store_fid, metadata = self.storage.save_file(file_obj.file, filename, metadata=metadata)
74 73 except FileNotAllowedException:
75 74 return {'store_fid': None,
76 75 'access_path': None,
77 76 'error': 'File {} is not allowed.'.format(filename)}
78 77
79 78 except FileOverSizeException:
80 79 return {'store_fid': None,
81 80 'access_path': None,
82 81 'error': 'File {} is exceeding allowed limit.'.format(filename)}
83 82
84 83 return {'store_fid': store_fid,
85 84 'access_path': h.route_path('download_file', fid=store_fid)}
86 85
87 86 @view_config(route_name='download_file')
88 87 def download_file(self):
89 88 self.load_default_context()
90 89 file_uid = self.request.matchdict['fid']
91 90 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
92 91 if not self.storage.exists(file_uid):
93 92 log.debug('File with FID:%s not found in the store', file_uid)
94 93 raise HTTPNotFound()
95 94
96 95 file_path = self.storage.store_path(file_uid)
97 96 return FileResponse(file_path)
@@ -1,740 +1,740 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
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 Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26 import time
27 27
28 28 from paste.gzipper import make_gzip_middleware
29 29 import pyramid.events
30 30 from pyramid.wsgi import wsgiapp
31 31 from pyramid.authorization import ACLAuthorizationPolicy
32 32 from pyramid.config import Configurator
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 36 from pyramid.renderers import render_to_response
37 37
38 38 from rhodecode.model import meta
39 39 from rhodecode.config import patches
40 40 from rhodecode.config import utils as config_utils
41 41 from rhodecode.config.environment import load_pyramid_environment
42 42
43 43 import rhodecode.events
44 44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 45 from rhodecode.lib.request import Request
46 46 from rhodecode.lib.vcs import VCSCommunicationError
47 47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 50 from rhodecode.lib.celerylib.loader import configure_celery
51 51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
53 53 from rhodecode.lib.exc_tracking import store_exception
54 54 from rhodecode.subscribers import (
55 55 scan_repositories_if_enabled, write_js_routes_if_enabled,
56 56 write_metadata_if_needed, inject_app_settings)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 def is_http_error(response):
63 63 # error which should have traceback
64 64 return response.status_code > 499
65 65
66 66
67 67 def should_load_all():
68 68 """
69 69 Returns if all application components should be loaded. In some cases it's
70 70 desired to skip apps loading for faster shell script execution
71 71 """
72 72 return True
73 73
74 74
75 75 def make_pyramid_app(global_config, **settings):
76 76 """
77 77 Constructs the WSGI application based on Pyramid.
78 78
79 79 Specials:
80 80
81 81 * The application can also be integrated like a plugin via the call to
82 82 `includeme`. This is accompanied with the other utility functions which
83 83 are called. Changing this should be done with great care to not break
84 84 cases when these fragments are assembled from another place.
85 85
86 86 """
87 87
88 88 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
89 89 # will be replaced by the value of the environment variable "NAME" in this case.
90 90 start_time = time.time()
91 91
92 92 debug = asbool(global_config.get('debug'))
93 93 if debug:
94 94 enable_debug()
95 95
96 96 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
97 97
98 98 global_config = _substitute_values(global_config, environ)
99 99 settings = _substitute_values(settings, environ)
100 100
101 101 sanitize_settings_and_apply_defaults(global_config, settings)
102 102
103 103 config = Configurator(settings=settings)
104 104
105 105 # Apply compatibility patches
106 106 patches.inspect_getargspec()
107 107
108 108 load_pyramid_environment(global_config, settings)
109 109
110 110 # Static file view comes first
111 111 includeme_first(config)
112 112
113 113 includeme(config)
114 114
115 115 pyramid_app = config.make_wsgi_app()
116 116 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
117 117 pyramid_app.config = config
118 118
119 119 config.configure_celery(global_config['__file__'])
120 120 # creating the app uses a connection - return it after we are done
121 121 meta.Session.remove()
122 122 total_time = time.time() - start_time
123 123 log.info('Pyramid app `%s` created and configured in %.2fs',
124 124 pyramid_app.func_name, total_time)
125 125
126 126 return pyramid_app
127 127
128 128
129 129 def not_found_view(request):
130 130 """
131 131 This creates the view which should be registered as not-found-view to
132 132 pyramid.
133 133 """
134 134
135 135 if not getattr(request, 'vcs_call', None):
136 136 # handle like regular case with our error_handler
137 137 return error_handler(HTTPNotFound(), request)
138 138
139 139 # handle not found view as a vcs call
140 140 settings = request.registry.settings
141 141 ae_client = getattr(request, 'ae_client', None)
142 142 vcs_app = VCSMiddleware(
143 143 HTTPNotFound(), request.registry, settings,
144 144 appenlight_client=ae_client)
145 145
146 146 return wsgiapp(vcs_app)(None, request)
147 147
148 148
149 149 def error_handler(exception, request):
150 150 import rhodecode
151 151 from rhodecode.lib import helpers
152 152
153 153 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
154 154
155 155 base_response = HTTPInternalServerError()
156 156 # prefer original exception for the response since it may have headers set
157 157 if isinstance(exception, HTTPException):
158 158 base_response = exception
159 159 elif isinstance(exception, VCSCommunicationError):
160 160 base_response = VCSServerUnavailable()
161 161
162 162 if is_http_error(base_response):
163 163 log.exception(
164 164 'error occurred handling this request for path: %s', request.path)
165 165
166 166 error_explanation = base_response.explanation or str(base_response)
167 167 if base_response.status_code == 404:
168 168 error_explanation += " Optionally you don't have permission to access this page."
169 169 c = AttributeDict()
170 170 c.error_message = base_response.status
171 171 c.error_explanation = error_explanation
172 172 c.visual = AttributeDict()
173 173
174 174 c.visual.rhodecode_support_url = (
175 175 request.registry.settings.get('rhodecode_support_url') or
176 176 request.route_url('rhodecode_support')
177 177 )
178 178 c.redirect_time = 0
179 179 c.rhodecode_name = rhodecode_title
180 180 if not c.rhodecode_name:
181 181 c.rhodecode_name = 'Rhodecode'
182 182
183 183 c.causes = []
184 184 if is_http_error(base_response):
185 185 c.causes.append('Server is overloaded.')
186 186 c.causes.append('Server database connection is lost.')
187 187 c.causes.append('Server expected unhandled error.')
188 188
189 189 if hasattr(base_response, 'causes'):
190 190 c.causes = base_response.causes
191 191
192 192 c.messages = helpers.flash.pop_messages(request=request)
193 193
194 194 exc_info = sys.exc_info()
195 195 c.exception_id = id(exc_info)
196 196 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
197 197 or base_response.status_code > 499
198 198 c.exception_id_url = request.route_url(
199 199 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
200 200
201 201 if c.show_exception_id:
202 202 store_exception(c.exception_id, exc_info)
203 203
204 204 response = render_to_response(
205 205 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
206 206 response=base_response)
207 207
208 208 return response
209 209
210 210
211 211 def includeme_first(config):
212 212 # redirect automatic browser favicon.ico requests to correct place
213 213 def favicon_redirect(context, request):
214 214 return HTTPFound(
215 215 request.static_path('rhodecode:public/images/favicon.ico'))
216 216
217 217 config.add_view(favicon_redirect, route_name='favicon')
218 218 config.add_route('favicon', '/favicon.ico')
219 219
220 220 def robots_redirect(context, request):
221 221 return HTTPFound(
222 222 request.static_path('rhodecode:public/robots.txt'))
223 223
224 224 config.add_view(robots_redirect, route_name='robots')
225 225 config.add_route('robots', '/robots.txt')
226 226
227 227 config.add_static_view(
228 228 '_static/deform', 'deform:static')
229 229 config.add_static_view(
230 230 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
231 231
232 232
233 233 def includeme(config):
234 234 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
235 235 settings = config.registry.settings
236 236 config.set_request_factory(Request)
237 237
238 238 # plugin information
239 239 config.registry.rhodecode_plugins = collections.OrderedDict()
240 240
241 241 config.add_directive(
242 242 'register_rhodecode_plugin', register_rhodecode_plugin)
243 243
244 244 config.add_directive('configure_celery', configure_celery)
245 245
246 246 if asbool(settings.get('appenlight', 'false')):
247 247 config.include('appenlight_client.ext.pyramid_tween')
248 248
249 249 load_all = should_load_all()
250 250
251 251 # Includes which are required. The application would fail without them.
252 252 config.include('pyramid_mako')
253 253 config.include('pyramid_beaker')
254 254 config.include('rhodecode.lib.rc_cache')
255 255
256 256 config.include('rhodecode.apps._base.navigation')
257 257 config.include('rhodecode.apps._base.subscribers')
258 258 config.include('rhodecode.tweens')
259 259
260 260 config.include('rhodecode.integrations')
261 261 config.include('rhodecode.authentication')
262 262
263 263 if load_all:
264 264 from rhodecode.authentication import discover_legacy_plugins
265 265 # load CE authentication plugins
266 266 config.include('rhodecode.authentication.plugins.auth_crowd')
267 267 config.include('rhodecode.authentication.plugins.auth_headers')
268 268 config.include('rhodecode.authentication.plugins.auth_jasig_cas')
269 269 config.include('rhodecode.authentication.plugins.auth_ldap')
270 270 config.include('rhodecode.authentication.plugins.auth_pam')
271 271 config.include('rhodecode.authentication.plugins.auth_rhodecode')
272 272 config.include('rhodecode.authentication.plugins.auth_token')
273 273
274 274 # Auto discover authentication plugins and include their configuration.
275 275 discover_legacy_plugins(config)
276 276
277 277 # apps
278 278 config.include('rhodecode.apps._base')
279 279
280 280 if load_all:
281 281 config.include('rhodecode.apps.ops')
282 282 config.include('rhodecode.apps.admin')
283 283 config.include('rhodecode.apps.channelstream')
284 config.include('rhodecode.apps.upload_store')
284 config.include('rhodecode.apps.file_store')
285 285 config.include('rhodecode.apps.login')
286 286 config.include('rhodecode.apps.home')
287 287 config.include('rhodecode.apps.journal')
288 288 config.include('rhodecode.apps.repository')
289 289 config.include('rhodecode.apps.repo_group')
290 290 config.include('rhodecode.apps.user_group')
291 291 config.include('rhodecode.apps.search')
292 292 config.include('rhodecode.apps.user_profile')
293 293 config.include('rhodecode.apps.user_group_profile')
294 294 config.include('rhodecode.apps.my_account')
295 295 config.include('rhodecode.apps.svn_support')
296 296 config.include('rhodecode.apps.ssh_support')
297 297 config.include('rhodecode.apps.gist')
298 298 config.include('rhodecode.apps.debug_style')
299 299 config.include('rhodecode.api')
300 300
301 301 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
302 302 config.add_translation_dirs('rhodecode:i18n/')
303 303 settings['default_locale_name'] = settings.get('lang', 'en')
304 304
305 305 # Add subscribers.
306 306 config.add_subscriber(inject_app_settings,
307 307 pyramid.events.ApplicationCreated)
308 308 config.add_subscriber(scan_repositories_if_enabled,
309 309 pyramid.events.ApplicationCreated)
310 310 config.add_subscriber(write_metadata_if_needed,
311 311 pyramid.events.ApplicationCreated)
312 312 config.add_subscriber(write_js_routes_if_enabled,
313 313 pyramid.events.ApplicationCreated)
314 314
315 315 # request custom methods
316 316 config.add_request_method(
317 317 'rhodecode.lib.partial_renderer.get_partial_renderer',
318 318 'get_partial_renderer')
319 319
320 320 # Set the authorization policy.
321 321 authz_policy = ACLAuthorizationPolicy()
322 322 config.set_authorization_policy(authz_policy)
323 323
324 324 # Set the default renderer for HTML templates to mako.
325 325 config.add_mako_renderer('.html')
326 326
327 327 config.add_renderer(
328 328 name='json_ext',
329 329 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
330 330
331 331 # include RhodeCode plugins
332 332 includes = aslist(settings.get('rhodecode.includes', []))
333 333 for inc in includes:
334 334 config.include(inc)
335 335
336 336 # custom not found view, if our pyramid app doesn't know how to handle
337 337 # the request pass it to potential VCS handling ap
338 338 config.add_notfound_view(not_found_view)
339 339 if not settings.get('debugtoolbar.enabled', False):
340 340 # disabled debugtoolbar handle all exceptions via the error_handlers
341 341 config.add_view(error_handler, context=Exception)
342 342
343 343 # all errors including 403/404/50X
344 344 config.add_view(error_handler, context=HTTPError)
345 345
346 346
347 347 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
348 348 """
349 349 Apply outer WSGI middlewares around the application.
350 350 """
351 351 registry = config.registry
352 352 settings = registry.settings
353 353
354 354 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
355 355 pyramid_app = HttpsFixup(pyramid_app, settings)
356 356
357 357 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
358 358 pyramid_app, settings)
359 359 registry.ae_client = _ae_client
360 360
361 361 if settings['gzip_responses']:
362 362 pyramid_app = make_gzip_middleware(
363 363 pyramid_app, settings, compress_level=1)
364 364
365 365 # this should be the outer most middleware in the wsgi stack since
366 366 # middleware like Routes make database calls
367 367 def pyramid_app_with_cleanup(environ, start_response):
368 368 try:
369 369 return pyramid_app(environ, start_response)
370 370 finally:
371 371 # Dispose current database session and rollback uncommitted
372 372 # transactions.
373 373 meta.Session.remove()
374 374
375 375 # In a single threaded mode server, on non sqlite db we should have
376 376 # '0 Current Checked out connections' at the end of a request,
377 377 # if not, then something, somewhere is leaving a connection open
378 378 pool = meta.Base.metadata.bind.engine.pool
379 379 log.debug('sa pool status: %s', pool.status())
380 380 log.debug('Request processing finalized')
381 381
382 382 return pyramid_app_with_cleanup
383 383
384 384
385 385 def sanitize_settings_and_apply_defaults(global_config, settings):
386 386 """
387 387 Applies settings defaults and does all type conversion.
388 388
389 389 We would move all settings parsing and preparation into this place, so that
390 390 we have only one place left which deals with this part. The remaining parts
391 391 of the application would start to rely fully on well prepared settings.
392 392
393 393 This piece would later be split up per topic to avoid a big fat monster
394 394 function.
395 395 """
396 396
397 397 settings.setdefault('rhodecode.edition', 'Community Edition')
398 398
399 399 if 'mako.default_filters' not in settings:
400 400 # set custom default filters if we don't have it defined
401 401 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
402 402 settings['mako.default_filters'] = 'h_filter'
403 403
404 404 if 'mako.directories' not in settings:
405 405 mako_directories = settings.setdefault('mako.directories', [
406 406 # Base templates of the original application
407 407 'rhodecode:templates',
408 408 ])
409 409 log.debug(
410 410 "Using the following Mako template directories: %s",
411 411 mako_directories)
412 412
413 413 # Default includes, possible to change as a user
414 414 pyramid_includes = settings.setdefault('pyramid.includes', [
415 415 'rhodecode.lib.middleware.request_wrapper',
416 416 ])
417 417 log.debug(
418 418 "Using the following pyramid.includes: %s",
419 419 pyramid_includes)
420 420
421 421 # TODO: johbo: Re-think this, usually the call to config.include
422 422 # should allow to pass in a prefix.
423 423 settings.setdefault('rhodecode.api.url', '/_admin/api')
424 424 settings.setdefault('__file__', global_config.get('__file__'))
425 425
426 426 # Sanitize generic settings.
427 427 _list_setting(settings, 'default_encoding', 'UTF-8')
428 428 _bool_setting(settings, 'is_test', 'false')
429 429 _bool_setting(settings, 'gzip_responses', 'false')
430 430
431 431 # Call split out functions that sanitize settings for each topic.
432 432 _sanitize_appenlight_settings(settings)
433 433 _sanitize_vcs_settings(settings)
434 434 _sanitize_cache_settings(settings)
435 435
436 436 # configure instance id
437 437 config_utils.set_instance_id(settings)
438 438
439 439 return settings
440 440
441 441
442 442 def enable_debug():
443 443 """
444 444 Helper to enable debug on running instance
445 445 :return:
446 446 """
447 447 import tempfile
448 448 import textwrap
449 449 import logging.config
450 450
451 451 ini_template = textwrap.dedent("""
452 452 #####################################
453 453 ### DEBUG LOGGING CONFIGURATION ####
454 454 #####################################
455 455 [loggers]
456 456 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
457 457
458 458 [handlers]
459 459 keys = console, console_sql
460 460
461 461 [formatters]
462 462 keys = generic, color_formatter, color_formatter_sql
463 463
464 464 #############
465 465 ## LOGGERS ##
466 466 #############
467 467 [logger_root]
468 468 level = NOTSET
469 469 handlers = console
470 470
471 471 [logger_sqlalchemy]
472 472 level = INFO
473 473 handlers = console_sql
474 474 qualname = sqlalchemy.engine
475 475 propagate = 0
476 476
477 477 [logger_beaker]
478 478 level = DEBUG
479 479 handlers =
480 480 qualname = beaker.container
481 481 propagate = 1
482 482
483 483 [logger_rhodecode]
484 484 level = DEBUG
485 485 handlers =
486 486 qualname = rhodecode
487 487 propagate = 1
488 488
489 489 [logger_ssh_wrapper]
490 490 level = DEBUG
491 491 handlers =
492 492 qualname = ssh_wrapper
493 493 propagate = 1
494 494
495 495 [logger_celery]
496 496 level = DEBUG
497 497 handlers =
498 498 qualname = celery
499 499
500 500
501 501 ##############
502 502 ## HANDLERS ##
503 503 ##############
504 504
505 505 [handler_console]
506 506 class = StreamHandler
507 507 args = (sys.stderr, )
508 508 level = DEBUG
509 509 formatter = color_formatter
510 510
511 511 [handler_console_sql]
512 512 # "level = DEBUG" logs SQL queries and results.
513 513 # "level = INFO" logs SQL queries.
514 514 # "level = WARN" logs neither. (Recommended for production systems.)
515 515 class = StreamHandler
516 516 args = (sys.stderr, )
517 517 level = WARN
518 518 formatter = color_formatter_sql
519 519
520 520 ################
521 521 ## FORMATTERS ##
522 522 ################
523 523
524 524 [formatter_generic]
525 525 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
526 526 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
527 527 datefmt = %Y-%m-%d %H:%M:%S
528 528
529 529 [formatter_color_formatter]
530 530 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
531 531 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
532 532 datefmt = %Y-%m-%d %H:%M:%S
533 533
534 534 [formatter_color_formatter_sql]
535 535 class = rhodecode.lib.logging_formatter.ColorFormatterSql
536 536 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
537 537 datefmt = %Y-%m-%d %H:%M:%S
538 538 """)
539 539
540 540 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
541 541 delete=False) as f:
542 542 log.info('Saved Temporary DEBUG config at %s', f.name)
543 543 f.write(ini_template)
544 544
545 545 logging.config.fileConfig(f.name)
546 546 log.debug('DEBUG MODE ON')
547 547 os.remove(f.name)
548 548
549 549
550 550 def _sanitize_appenlight_settings(settings):
551 551 _bool_setting(settings, 'appenlight', 'false')
552 552
553 553
554 554 def _sanitize_vcs_settings(settings):
555 555 """
556 556 Applies settings defaults and does type conversion for all VCS related
557 557 settings.
558 558 """
559 559 _string_setting(settings, 'vcs.svn.compatible_version', '')
560 560 _string_setting(settings, 'git_rev_filter', '--all')
561 561 _string_setting(settings, 'vcs.hooks.protocol', 'http')
562 562 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
563 563 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
564 564 _string_setting(settings, 'vcs.server', '')
565 565 _string_setting(settings, 'vcs.server.log_level', 'debug')
566 566 _string_setting(settings, 'vcs.server.protocol', 'http')
567 567 _bool_setting(settings, 'startup.import_repos', 'false')
568 568 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
569 569 _bool_setting(settings, 'vcs.server.enable', 'true')
570 570 _bool_setting(settings, 'vcs.start_server', 'false')
571 571 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
572 572 _int_setting(settings, 'vcs.connection_timeout', 3600)
573 573
574 574 # Support legacy values of vcs.scm_app_implementation. Legacy
575 575 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
576 576 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
577 577 scm_app_impl = settings['vcs.scm_app_implementation']
578 578 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
579 579 settings['vcs.scm_app_implementation'] = 'http'
580 580
581 581
582 582 def _sanitize_cache_settings(settings):
583 583 temp_store = tempfile.gettempdir()
584 584 default_cache_dir = os.path.join(temp_store, 'rc_cache')
585 585
586 586 # save default, cache dir, and use it for all backends later.
587 587 default_cache_dir = _string_setting(
588 588 settings,
589 589 'cache_dir',
590 590 default_cache_dir, lower=False, default_when_empty=True)
591 591
592 592 # ensure we have our dir created
593 593 if not os.path.isdir(default_cache_dir):
594 594 os.makedirs(default_cache_dir, mode=0o755)
595 595
596 596 # exception store cache
597 597 _string_setting(
598 598 settings,
599 599 'exception_tracker.store_path',
600 600 temp_store, lower=False, default_when_empty=True)
601 601
602 602 # cache_perms
603 603 _string_setting(
604 604 settings,
605 605 'rc_cache.cache_perms.backend',
606 606 'dogpile.cache.rc.file_namespace', lower=False)
607 607 _int_setting(
608 608 settings,
609 609 'rc_cache.cache_perms.expiration_time',
610 610 60)
611 611 _string_setting(
612 612 settings,
613 613 'rc_cache.cache_perms.arguments.filename',
614 614 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
615 615
616 616 # cache_repo
617 617 _string_setting(
618 618 settings,
619 619 'rc_cache.cache_repo.backend',
620 620 'dogpile.cache.rc.file_namespace', lower=False)
621 621 _int_setting(
622 622 settings,
623 623 'rc_cache.cache_repo.expiration_time',
624 624 60)
625 625 _string_setting(
626 626 settings,
627 627 'rc_cache.cache_repo.arguments.filename',
628 628 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
629 629
630 630 # cache_license
631 631 _string_setting(
632 632 settings,
633 633 'rc_cache.cache_license.backend',
634 634 'dogpile.cache.rc.file_namespace', lower=False)
635 635 _int_setting(
636 636 settings,
637 637 'rc_cache.cache_license.expiration_time',
638 638 5*60)
639 639 _string_setting(
640 640 settings,
641 641 'rc_cache.cache_license.arguments.filename',
642 642 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
643 643
644 644 # cache_repo_longterm memory, 96H
645 645 _string_setting(
646 646 settings,
647 647 'rc_cache.cache_repo_longterm.backend',
648 648 'dogpile.cache.rc.memory_lru', lower=False)
649 649 _int_setting(
650 650 settings,
651 651 'rc_cache.cache_repo_longterm.expiration_time',
652 652 345600)
653 653 _int_setting(
654 654 settings,
655 655 'rc_cache.cache_repo_longterm.max_size',
656 656 10000)
657 657
658 658 # sql_cache_short
659 659 _string_setting(
660 660 settings,
661 661 'rc_cache.sql_cache_short.backend',
662 662 'dogpile.cache.rc.memory_lru', lower=False)
663 663 _int_setting(
664 664 settings,
665 665 'rc_cache.sql_cache_short.expiration_time',
666 666 30)
667 667 _int_setting(
668 668 settings,
669 669 'rc_cache.sql_cache_short.max_size',
670 670 10000)
671 671
672 672
673 673 def _int_setting(settings, name, default):
674 674 settings[name] = int(settings.get(name, default))
675 675 return settings[name]
676 676
677 677
678 678 def _bool_setting(settings, name, default):
679 679 input_val = settings.get(name, default)
680 680 if isinstance(input_val, unicode):
681 681 input_val = input_val.encode('utf8')
682 682 settings[name] = asbool(input_val)
683 683 return settings[name]
684 684
685 685
686 686 def _list_setting(settings, name, default):
687 687 raw_value = settings.get(name, default)
688 688
689 689 old_separator = ','
690 690 if old_separator in raw_value:
691 691 # If we get a comma separated list, pass it to our own function.
692 692 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
693 693 else:
694 694 # Otherwise we assume it uses pyramids space/newline separation.
695 695 settings[name] = aslist(raw_value)
696 696 return settings[name]
697 697
698 698
699 699 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
700 700 value = settings.get(name, default)
701 701
702 702 if default_when_empty and not value:
703 703 # use default value when value is empty
704 704 value = default
705 705
706 706 if lower:
707 707 value = value.lower()
708 708 settings[name] = value
709 709 return settings[name]
710 710
711 711
712 712 def _substitute_values(mapping, substitutions):
713 713 result = {}
714 714
715 715 try:
716 716 for key, value in mapping.items():
717 717 # initialize without substitution first
718 718 result[key] = value
719 719
720 720 # Note: Cannot use regular replacements, since they would clash
721 721 # with the implementation of ConfigParser. Using "format" instead.
722 722 try:
723 723 result[key] = value.format(**substitutions)
724 724 except KeyError as e:
725 725 env_var = '{}'.format(e.args[0])
726 726
727 727 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
728 728 'Make sure your environment has {var} set, or remove this ' \
729 729 'variable from config file'.format(key=key, var=env_var)
730 730
731 731 if env_var.startswith('ENV_'):
732 732 raise ValueError(msg)
733 733 else:
734 734 log.warning(msg)
735 735
736 736 except ValueError as e:
737 737 log.warning('Failed to substitute ENV variable: %s', e)
738 738 result = mapping
739 739
740 740 return result
General Comments 0
You need to be logged in to leave comments. Login now