##// END OF EJS Templates
feat(artifacts): new artifact storage engines allowing an s3 based uploads
super-admin -
r5516:3496180b default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,269 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import fsspec # noqa
21 import logging
22
23 from rhodecode.lib.ext_json import json
24
25 from rhodecode.apps.file_store.utils import sha256_safe, ShardFileReader, get_uid_filename
26 from rhodecode.apps.file_store.extensions import resolve_extensions
27 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException # noqa: F401
28
29 log = logging.getLogger(__name__)
30
31
32 class BaseShard:
33
34 metadata_suffix: str = '.metadata'
35 storage_type: str = ''
36 fs = None
37
38 @property
39 def storage_medium(self):
40 if not self.storage_type:
41 raise ValueError('No storage type set for this shard storage_type=""')
42 return getattr(self, self.storage_type)
43
44 def __contains__(self, key):
45 full_path = self.store_path(key)
46 return self.fs.exists(full_path)
47
48 def metadata_convert(self, uid_filename, metadata):
49 return metadata
50
51 def get_metadata_filename(self, uid_filename) -> tuple[str, str]:
52 metadata_file: str = f'{uid_filename}{self.metadata_suffix}'
53 return metadata_file, self.store_path(metadata_file)
54
55 def get_metadata(self, uid_filename, ignore_missing=False) -> dict:
56 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_filename)
57 if ignore_missing and not self.fs.exists(metadata_file_path):
58 return {}
59
60 with self.fs.open(metadata_file_path, 'rb') as f:
61 metadata = json.loads(f.read())
62
63 metadata = self.metadata_convert(uid_filename, metadata)
64 return metadata
65
66 def _store(self, key: str, uid_key: str, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
67 raise NotImplementedError
68
69 def store(self, key: str, uid_key: str, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
70 return self._store(key, uid_key, value_reader, max_filesize, metadata, **kwargs)
71
72 def _fetch(self, key, presigned_url_expires: int = 0):
73 raise NotImplementedError
74
75 def fetch(self, key, **kwargs) -> tuple[ShardFileReader, dict]:
76 return self._fetch(key)
77
78 def _delete(self, key):
79 if key not in self:
80 log.exception(f'requested key={key} not found in {self}')
81 raise KeyError(key)
82
83 metadata = self.get_metadata(key)
84 _metadata_file, metadata_file_path = self.get_metadata_filename(key)
85 artifact_file_path = metadata['filename_uid_path']
86 self.fs.rm(artifact_file_path)
87 self.fs.rm(metadata_file_path)
88
89 return 1
90
91 def delete(self, key):
92 raise NotImplementedError
93
94 def store_path(self, uid_filename):
95 raise NotImplementedError
96
97
98 class BaseFileStoreBackend:
99 _shards = tuple()
100 _shard_cls = BaseShard
101 _config: dict | None = None
102 _storage_path: str = ''
103
104 def __init__(self, settings, extension_groups=None):
105 self._config = settings
106 extension_groups = extension_groups or ['any']
107 self.extensions = resolve_extensions([], groups=extension_groups)
108
109 def __contains__(self, key):
110 return self.filename_exists(key)
111
112 def __repr__(self):
113 return f'<{self.__class__.__name__}(storage={self.storage_path})>'
114
115 @property
116 def storage_path(self):
117 return self._storage_path
118
119 @classmethod
120 def get_shard_index(cls, filename: str, num_shards) -> int:
121 # Generate a hash value from the filename
122 hash_value = sha256_safe(filename)
123
124 # Convert the hash value to an integer
125 hash_int = int(hash_value, 16)
126
127 # Map the hash integer to a shard number between 1 and num_shards
128 shard_number = (hash_int % num_shards)
129
130 return shard_number
131
132 @classmethod
133 def apply_counter(cls, counter: int, filename: str) -> str:
134 """
135 Apply a counter to the filename.
136
137 :param counter: The counter value to apply.
138 :param filename: The original filename.
139 :return: The modified filename with the counter.
140 """
141 name_counted = f'{counter:d}-{filename}'
142 return name_counted
143
144 def _get_shard(self, key) -> _shard_cls:
145 index = self.get_shard_index(key, len(self._shards))
146 shard = self._shards[index]
147 return shard
148
149 def get_conf(self, key, pop=False):
150 if key not in self._config:
151 raise ValueError(
152 f"No configuration key '{key}', please make sure it exists in filestore config")
153 val = self._config[key]
154 if pop:
155 del self._config[key]
156 return val
157
158 def filename_allowed(self, filename, extensions=None):
159 """Checks if a filename has an allowed extension
160
161 :param filename: base name of file
162 :param extensions: iterable of extensions (or self.extensions)
163 """
164 _, ext = os.path.splitext(filename)
165 return self.extension_allowed(ext, extensions)
166
167 def extension_allowed(self, ext, extensions=None):
168 """
169 Checks if an extension is permitted. Both e.g. ".jpg" and
170 "jpg" can be passed in. Extension lookup is case-insensitive.
171
172 :param ext: extension to check
173 :param extensions: iterable of extensions to validate against (or self.extensions)
174 """
175 def normalize_ext(_ext):
176 if _ext.startswith('.'):
177 _ext = _ext[1:]
178 return _ext.lower()
179
180 extensions = extensions or self.extensions
181 if not extensions:
182 return True
183
184 ext = normalize_ext(ext)
185
186 return ext in [normalize_ext(x) for x in extensions]
187
188 def filename_exists(self, uid_filename):
189 shard = self._get_shard(uid_filename)
190 return uid_filename in shard
191
192 def store_path(self, uid_filename):
193 """
194 Returns absolute file path of the uid_filename
195 """
196 shard = self._get_shard(uid_filename)
197 return shard.store_path(uid_filename)
198
199 def store_metadata(self, uid_filename):
200 shard = self._get_shard(uid_filename)
201 return shard.get_metadata_filename(uid_filename)
202
203 def store(self, filename, value_reader, extensions=None, metadata=None, max_filesize=None, randomized_name=True, **kwargs):
204 extensions = extensions or self.extensions
205
206 if not self.filename_allowed(filename, extensions):
207 msg = f'filename {filename} does not allow extensions {extensions}'
208 raise FileNotAllowedException(msg)
209
210 # # TODO: check why we need this setting ? it looks stupid...
211 # no_body_seek is used in stream mode importer somehow
212 # no_body_seek = kwargs.pop('no_body_seek', False)
213 # if no_body_seek:
214 # pass
215 # else:
216 # value_reader.seek(0)
217
218 uid_filename = kwargs.pop('uid_filename', None)
219 if uid_filename is None:
220 uid_filename = get_uid_filename(filename, randomized=randomized_name)
221
222 shard = self._get_shard(uid_filename)
223
224 return shard.store(filename, uid_filename, value_reader, max_filesize, metadata, **kwargs)
225
226 def import_to_store(self, value_reader, org_filename, uid_filename, metadata, **kwargs):
227 shard = self._get_shard(uid_filename)
228 max_filesize = None
229 return shard.store(org_filename, uid_filename, value_reader, max_filesize, metadata, import_mode=True)
230
231 def delete(self, uid_filename):
232 shard = self._get_shard(uid_filename)
233 return shard.delete(uid_filename)
234
235 def fetch(self, uid_filename) -> tuple[ShardFileReader, dict]:
236 shard = self._get_shard(uid_filename)
237 return shard.fetch(uid_filename)
238
239 def get_metadata(self, uid_filename, ignore_missing=False) -> dict:
240 shard = self._get_shard(uid_filename)
241 return shard.get_metadata(uid_filename, ignore_missing=ignore_missing)
242
243 def iter_keys(self):
244 for shard in self._shards:
245 if shard.fs.exists(shard.storage_medium):
246 for path, _dirs, _files in shard.fs.walk(shard.storage_medium):
247 for key_file_path in _files:
248 if key_file_path.endswith(shard.metadata_suffix):
249 yield shard, key_file_path
250
251 def iter_artifacts(self):
252 for shard, key_file in self.iter_keys():
253 json_key = f"{shard.storage_medium}/{key_file}"
254 with shard.fs.open(json_key, 'rb') as f:
255 yield shard, json.loads(f.read())['filename_uid']
256
257 def get_statistics(self):
258 total_files = 0
259 total_size = 0
260 meta = {}
261
262 for shard, key_file in self.iter_keys():
263 json_key = f"{shard.storage_medium}/{key_file}"
264 with shard.fs.open(json_key, 'rb') as f:
265 total_files += 1
266 metadata = json.loads(f.read())
267 total_size += metadata['size']
268
269 return total_files, total_size, meta
@@ -0,0 +1,183 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class FileSystemShard(BaseShard):
35 METADATA_VER = 'v2'
36 BACKEND_TYPE = config_keys.backend_filesystem
37 storage_type: str = 'directory'
38
39 def __init__(self, index, directory, directory_folder, fs, **settings):
40 self._index: int = index
41 self._directory: str = directory
42 self._directory_folder: str = directory_folder
43 self.fs = fs
44
45 @property
46 def directory(self) -> str:
47 """Cache directory final path."""
48 return os.path.join(self._directory, self._directory_folder)
49
50 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
51
52 # ensure dir exists
53 destination, _ = os.path.split(full_path)
54 if not self.fs.exists(destination):
55 self.fs.makedirs(destination)
56
57 writer = self.fs.open(full_path, mode)
58
59 digest = hashlib.sha256()
60 oversize_cleanup = False
61 with writer:
62 size = 0
63 for chunk in iterator:
64 size += len(chunk)
65 digest.update(chunk)
66 writer.write(chunk)
67
68 if max_filesize and size > max_filesize:
69 oversize_cleanup = True
70 # free up the copied file, and raise exc
71 break
72
73 writer.flush()
74 # Get the file descriptor
75 fd = writer.fileno()
76
77 # Sync the file descriptor to disk, helps with NFS cases...
78 os.fsync(fd)
79
80 if oversize_cleanup:
81 self.fs.rm(full_path)
82 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
83
84 sha256 = digest.hexdigest()
85 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
86 return size, sha256
87
88 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
89
90 filename = key
91 uid_filename = uid_key
92 full_path = self.store_path(uid_filename)
93
94 # STORE METADATA
95 _metadata = {
96 "version": self.METADATA_VER,
97 "store_type": self.BACKEND_TYPE,
98
99 "filename": filename,
100 "filename_uid_path": full_path,
101 "filename_uid": uid_filename,
102 "sha256": "", # NOTE: filled in by reader iteration
103
104 "store_time": time.time(),
105
106 "size": 0
107 }
108
109 if metadata:
110 if kwargs.pop('import_mode', False):
111 # in import mode, we don't need to compute metadata, we just take the old version
112 _metadata["import_mode"] = True
113 else:
114 _metadata.update(metadata)
115
116 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
117 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
118 _metadata['size'] = size
119 _metadata['sha256'] = sha256
120
121 # after storing the artifacts, we write the metadata present
122 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_key)
123
124 with self.fs.open(metadata_file_path, 'wb') as f:
125 f.write(json.dumps(_metadata))
126
127 return uid_filename, _metadata
128
129 def store_path(self, uid_filename):
130 """
131 Returns absolute file path of the uid_filename
132 """
133 return os.path.join(self._directory, self._directory_folder, uid_filename)
134
135 def _fetch(self, key, presigned_url_expires: int = 0):
136 if key not in self:
137 log.exception(f'requested key={key} not found in {self}')
138 raise KeyError(key)
139
140 metadata = self.get_metadata(key)
141
142 file_path = metadata['filename_uid_path']
143 if presigned_url_expires and presigned_url_expires > 0:
144 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
145
146 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
147
148 def delete(self, key):
149 return self._delete(key)
150
151
152 class FileSystemBackend(BaseFileStoreBackend):
153 shard_name: str = 'shard_{:03d}'
154 _shard_cls = FileSystemShard
155
156 def __init__(self, settings):
157 super().__init__(settings)
158
159 store_dir = self.get_conf(config_keys.filesystem_storage_path)
160 directory = os.path.expanduser(store_dir)
161
162 self._directory = directory
163 self._storage_path = directory # common path for all from BaseCache
164 self._shard_count = int(self.get_conf(config_keys.filesystem_shards, pop=True))
165 if self._shard_count < 1:
166 raise ValueError(f'{config_keys.filesystem_shards} must be 1 or more')
167
168 log.debug('Initializing %s file_store instance', self)
169 fs = fsspec.filesystem('file')
170
171 if not fs.exists(self._directory):
172 fs.makedirs(self._directory, exist_ok=True)
173
174 self._shards = tuple(
175 self._shard_cls(
176 index=num,
177 directory=directory,
178 directory_folder=self.shard_name.format(num),
179 fs=fs,
180 **settings,
181 )
182 for num in range(self._shard_count)
183 )
@@ -0,0 +1,278 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import errno
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class LegacyFileSystemShard(BaseShard):
35 # legacy ver
36 METADATA_VER = 'v2'
37 BACKEND_TYPE = config_keys.backend_legacy_filesystem
38 storage_type: str = 'dir_struct'
39
40 # legacy suffix
41 metadata_suffix: str = '.meta'
42
43 @classmethod
44 def _sub_store_from_filename(cls, filename):
45 return filename[:2]
46
47 @classmethod
48 def apply_counter(cls, counter, filename):
49 name_counted = '%d-%s' % (counter, filename)
50 return name_counted
51
52 @classmethod
53 def safe_make_dirs(cls, dir_path):
54 if not os.path.exists(dir_path):
55 try:
56 os.makedirs(dir_path)
57 except OSError as e:
58 if e.errno != errno.EEXIST:
59 raise
60 return
61
62 @classmethod
63 def resolve_name(cls, name, directory):
64 """
65 Resolves a unique name and the correct path. If a filename
66 for that path already exists then a numeric prefix with values > 0 will be
67 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
68
69 :param name: base name of file
70 :param directory: absolute directory path
71 """
72
73 counter = 0
74 while True:
75 name_counted = cls.apply_counter(counter, name)
76
77 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
78 sub_store: str = cls._sub_store_from_filename(name_counted)
79 sub_store_path: str = os.path.join(directory, sub_store)
80 cls.safe_make_dirs(sub_store_path)
81
82 path = os.path.join(sub_store_path, name_counted)
83 if not os.path.exists(path):
84 return name_counted, path
85 counter += 1
86
87 def __init__(self, index, directory, directory_folder, fs, **settings):
88 self._index: int = index
89 self._directory: str = directory
90 self._directory_folder: str = directory_folder
91 self.fs = fs
92
93 @property
94 def dir_struct(self) -> str:
95 """Cache directory final path."""
96 return os.path.join(self._directory, '0-')
97
98 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
99
100 # ensure dir exists
101 destination, _ = os.path.split(full_path)
102 if not self.fs.exists(destination):
103 self.fs.makedirs(destination)
104
105 writer = self.fs.open(full_path, mode)
106
107 digest = hashlib.sha256()
108 oversize_cleanup = False
109 with writer:
110 size = 0
111 for chunk in iterator:
112 size += len(chunk)
113 digest.update(chunk)
114 writer.write(chunk)
115
116 if max_filesize and size > max_filesize:
117 # free up the copied file, and raise exc
118 oversize_cleanup = True
119 break
120
121 writer.flush()
122 # Get the file descriptor
123 fd = writer.fileno()
124
125 # Sync the file descriptor to disk, helps with NFS cases...
126 os.fsync(fd)
127
128 if oversize_cleanup:
129 self.fs.rm(full_path)
130 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
131
132 sha256 = digest.hexdigest()
133 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
134 return size, sha256
135
136 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
137
138 filename = key
139 uid_filename = uid_key
140
141 # NOTE:, also apply N- Counter...
142 uid_filename, full_path = self.resolve_name(uid_filename, self._directory)
143
144 # STORE METADATA
145 # TODO: make it compatible, and backward proof
146 _metadata = {
147 "version": self.METADATA_VER,
148
149 "filename": filename,
150 "filename_uid_path": full_path,
151 "filename_uid": uid_filename,
152 "sha256": "", # NOTE: filled in by reader iteration
153
154 "store_time": time.time(),
155
156 "size": 0
157 }
158 if metadata:
159 _metadata.update(metadata)
160
161 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
162 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
163 _metadata['size'] = size
164 _metadata['sha256'] = sha256
165
166 # after storing the artifacts, we write the metadata present
167 _metadata_file, metadata_file_path = self.get_metadata_filename(uid_filename)
168
169 with self.fs.open(metadata_file_path, 'wb') as f:
170 f.write(json.dumps(_metadata))
171
172 return uid_filename, _metadata
173
174 def store_path(self, uid_filename):
175 """
176 Returns absolute file path of the uid_filename
177 """
178 prefix_dir = ''
179 if '/' in uid_filename:
180 prefix_dir, filename = uid_filename.split('/')
181 sub_store = self._sub_store_from_filename(filename)
182 else:
183 sub_store = self._sub_store_from_filename(uid_filename)
184
185 return os.path.join(self._directory, prefix_dir, sub_store, uid_filename)
186
187 def metadata_convert(self, uid_filename, metadata):
188 # NOTE: backward compat mode here... this is for file created PRE 5.2 system
189 if 'meta_ver' in metadata:
190 full_path = self.store_path(uid_filename)
191 metadata = {
192 "_converted": True,
193 "_org": metadata,
194 "version": self.METADATA_VER,
195 "store_type": self.BACKEND_TYPE,
196
197 "filename": metadata['filename'],
198 "filename_uid_path": full_path,
199 "filename_uid": uid_filename,
200 "sha256": metadata['sha256'],
201
202 "store_time": metadata['time'],
203
204 "size": metadata['size']
205 }
206 return metadata
207
208 def _fetch(self, key, presigned_url_expires: int = 0):
209 if key not in self:
210 log.exception(f'requested key={key} not found in {self}')
211 raise KeyError(key)
212
213 metadata = self.get_metadata(key)
214
215 file_path = metadata['filename_uid_path']
216 if presigned_url_expires and presigned_url_expires > 0:
217 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
218
219 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
220
221 def delete(self, key):
222 return self._delete(key)
223
224 def _delete(self, key):
225 if key not in self:
226 log.exception(f'requested key={key} not found in {self}')
227 raise KeyError(key)
228
229 metadata = self.get_metadata(key)
230 metadata_file, metadata_file_path = self.get_metadata_filename(key)
231 artifact_file_path = metadata['filename_uid_path']
232 self.fs.rm(artifact_file_path)
233 self.fs.rm(metadata_file_path)
234
235 def get_metadata_filename(self, uid_filename) -> tuple[str, str]:
236
237 metadata_file: str = f'{uid_filename}{self.metadata_suffix}'
238 uid_path_in_store = self.store_path(uid_filename)
239
240 metadata_file_path = f'{uid_path_in_store}{self.metadata_suffix}'
241 return metadata_file, metadata_file_path
242
243
244 class LegacyFileSystemBackend(BaseFileStoreBackend):
245 _shard_cls = LegacyFileSystemShard
246
247 def __init__(self, settings):
248 super().__init__(settings)
249
250 store_dir = self.get_conf(config_keys.legacy_filesystem_storage_path)
251 directory = os.path.expanduser(store_dir)
252
253 self._directory = directory
254 self._storage_path = directory # common path for all from BaseCache
255
256 log.debug('Initializing %s file_store instance', self)
257 fs = fsspec.filesystem('file')
258
259 if not fs.exists(self._directory):
260 fs.makedirs(self._directory, exist_ok=True)
261
262 # legacy system uses single shard
263 self._shards = tuple(
264 [
265 self._shard_cls(
266 index=0,
267 directory=directory,
268 directory_folder='',
269 fs=fs,
270 **settings,
271 )
272 ]
273 )
274
275 @classmethod
276 def get_shard_index(cls, filename: str, num_shards) -> int:
277 # legacy filesystem doesn't use shards, and always uses single shard
278 return 0
@@ -0,0 +1,184 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import os
20 import hashlib
21 import functools
22 import time
23 import logging
24
25 from .. import config_keys
26 from ..exceptions import FileOverSizeException
27 from ..backends.base import BaseFileStoreBackend, fsspec, BaseShard, ShardFileReader
28
29 from ....lib.ext_json import json
30
31 log = logging.getLogger(__name__)
32
33
34 class S3Shard(BaseShard):
35 METADATA_VER = 'v2'
36 BACKEND_TYPE = config_keys.backend_objectstore
37 storage_type: str = 'bucket'
38
39 def __init__(self, index, bucket, bucket_folder, fs, **settings):
40 self._index: int = index
41 self._bucket_main: str = bucket
42 self._bucket_folder: str = bucket_folder
43
44 self.fs = fs
45
46 @property
47 def bucket(self) -> str:
48 """Cache bucket final path."""
49 return os.path.join(self._bucket_main, self._bucket_folder)
50
51 def _write_file(self, full_path, iterator, max_filesize, mode='wb'):
52
53 # ensure dir exists
54 destination, _ = os.path.split(full_path)
55 if not self.fs.exists(destination):
56 self.fs.makedirs(destination)
57
58 writer = self.fs.open(full_path, mode)
59
60 digest = hashlib.sha256()
61 oversize_cleanup = False
62 with writer:
63 size = 0
64 for chunk in iterator:
65 size += len(chunk)
66 digest.update(chunk)
67 writer.write(chunk)
68
69 if max_filesize and size > max_filesize:
70 oversize_cleanup = True
71 # free up the copied file, and raise exc
72 break
73
74 if oversize_cleanup:
75 self.fs.rm(full_path)
76 raise FileOverSizeException(f'given file is over size limit ({max_filesize}): {full_path}')
77
78 sha256 = digest.hexdigest()
79 log.debug('written new artifact under %s, sha256: %s', full_path, sha256)
80 return size, sha256
81
82 def _store(self, key: str, uid_key, value_reader, max_filesize: int | None = None, metadata: dict | None = None, **kwargs):
83
84 filename = key
85 uid_filename = uid_key
86 full_path = self.store_path(uid_filename)
87
88 # STORE METADATA
89 _metadata = {
90 "version": self.METADATA_VER,
91 "store_type": self.BACKEND_TYPE,
92
93 "filename": filename,
94 "filename_uid_path": full_path,
95 "filename_uid": uid_filename,
96 "sha256": "", # NOTE: filled in by reader iteration
97
98 "store_time": time.time(),
99
100 "size": 0
101 }
102
103 if metadata:
104 if kwargs.pop('import_mode', False):
105 # in import mode, we don't need to compute metadata, we just take the old version
106 _metadata["import_mode"] = True
107 else:
108 _metadata.update(metadata)
109
110 read_iterator = iter(functools.partial(value_reader.read, 2**22), b'')
111 size, sha256 = self._write_file(full_path, read_iterator, max_filesize)
112 _metadata['size'] = size
113 _metadata['sha256'] = sha256
114
115 # after storing the artifacts, we write the metadata present
116 metadata_file, metadata_file_path = self.get_metadata_filename(uid_key)
117
118 with self.fs.open(metadata_file_path, 'wb') as f:
119 f.write(json.dumps(_metadata))
120
121 return uid_filename, _metadata
122
123 def store_path(self, uid_filename):
124 """
125 Returns absolute file path of the uid_filename
126 """
127 return os.path.join(self._bucket_main, self._bucket_folder, uid_filename)
128
129 def _fetch(self, key, presigned_url_expires: int = 0):
130 if key not in self:
131 log.exception(f'requested key={key} not found in {self}')
132 raise KeyError(key)
133
134 metadata_file, metadata_file_path = self.get_metadata_filename(key)
135 with self.fs.open(metadata_file_path, 'rb') as f:
136 metadata = json.loads(f.read())
137
138 file_path = metadata['filename_uid_path']
139 if presigned_url_expires and presigned_url_expires > 0:
140 metadata['url'] = self.fs.url(file_path, expires=presigned_url_expires)
141
142 return ShardFileReader(self.fs.open(file_path, 'rb')), metadata
143
144 def delete(self, key):
145 return self._delete(key)
146
147
148 class ObjectStoreBackend(BaseFileStoreBackend):
149 shard_name: str = 'shard-{:03d}'
150 _shard_cls = S3Shard
151
152 def __init__(self, settings):
153 super().__init__(settings)
154
155 self._shard_count = int(self.get_conf(config_keys.objectstore_bucket_shards, pop=True))
156 if self._shard_count < 1:
157 raise ValueError('cache_shards must be 1 or more')
158
159 self._bucket = settings.pop(config_keys.objectstore_bucket)
160 if not self._bucket:
161 raise ValueError(f'{config_keys.objectstore_bucket} needs to have a value')
162
163 objectstore_url = self.get_conf(config_keys.objectstore_url)
164 key = settings.pop(config_keys.objectstore_key)
165 secret = settings.pop(config_keys.objectstore_secret)
166
167 self._storage_path = objectstore_url # common path for all from BaseCache
168 log.debug('Initializing %s file_store instance', self)
169 fs = fsspec.filesystem('s3', anon=False, endpoint_url=objectstore_url, key=key, secret=secret)
170
171 # init main bucket
172 if not fs.exists(self._bucket):
173 fs.mkdir(self._bucket)
174
175 self._shards = tuple(
176 self._shard_cls(
177 index=num,
178 bucket=self._bucket,
179 bucket_folder=self.shard_name.format(num),
180 fs=fs,
181 **settings,
182 )
183 for num in range(self._shard_count)
184 )
@@ -0,0 +1,128 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps import file_store
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.backends.filesystem_legacy import LegacyFileSystemBackend
23 from rhodecode.apps.file_store.backends.filesystem import FileSystemBackend
24 from rhodecode.apps.file_store.backends.objectstore import ObjectStoreBackend
25 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
26
27 from rhodecode.apps.file_store import utils as store_utils
28 from rhodecode.apps.file_store.tests import random_binary_file, file_store_instance
29
30
31 class TestFileStoreBackends:
32
33 @pytest.mark.parametrize('backend_type, expected_instance', [
34 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
35 (config_keys.backend_filesystem, FileSystemBackend),
36 (config_keys.backend_objectstore, ObjectStoreBackend),
37 ])
38 def test_get_backend(self, backend_type, expected_instance, ini_settings):
39 config = ini_settings
40 config[config_keys.backend_type] = backend_type
41 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
42 assert isinstance(f_store, expected_instance)
43
44 @pytest.mark.parametrize('backend_type, expected_instance', [
45 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
46 (config_keys.backend_filesystem, FileSystemBackend),
47 (config_keys.backend_objectstore, ObjectStoreBackend),
48 ])
49 def test_store_and_read(self, backend_type, expected_instance, ini_settings, random_binary_file):
50 filename, temp_file = random_binary_file
51 config = ini_settings
52 config[config_keys.backend_type] = backend_type
53 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
54 metadata = {
55 'user_uploaded': {
56 'username': 'user1',
57 'user_id': 10,
58 'ip': '10.20.30.40'
59 }
60 }
61 store_fid, metadata = f_store.store(filename, temp_file, extra_metadata=metadata)
62 assert store_fid
63 assert metadata
64
65 # read-after write
66 reader, metadata2 = f_store.fetch(store_fid)
67 assert reader
68 assert metadata2['filename'] == filename
69
70 @pytest.mark.parametrize('backend_type, expected_instance', [
71 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
72 (config_keys.backend_filesystem, FileSystemBackend),
73 (config_keys.backend_objectstore, ObjectStoreBackend),
74 ])
75 def test_store_file_not_allowed(self, backend_type, expected_instance, ini_settings, random_binary_file):
76 filename, temp_file = random_binary_file
77 config = ini_settings
78 config[config_keys.backend_type] = backend_type
79 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
80 with pytest.raises(FileNotAllowedException):
81 f_store.store('notallowed.exe', temp_file, extensions=['.txt'])
82
83 @pytest.mark.parametrize('backend_type, expected_instance', [
84 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend),
85 (config_keys.backend_filesystem, FileSystemBackend),
86 (config_keys.backend_objectstore, ObjectStoreBackend),
87 ])
88 def test_store_file_over_size(self, backend_type, expected_instance, ini_settings, random_binary_file):
89 filename, temp_file = random_binary_file
90 config = ini_settings
91 config[config_keys.backend_type] = backend_type
92 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
93 with pytest.raises(FileOverSizeException):
94 f_store.store('toobig.exe', temp_file, extensions=['.exe'], max_filesize=124)
95
96 @pytest.mark.parametrize('backend_type, expected_instance, extra_conf', [
97 (config_keys.backend_legacy_filesystem, LegacyFileSystemBackend, {}),
98 (config_keys.backend_filesystem, FileSystemBackend, {config_keys.filesystem_storage_path: '/tmp/test-fs-store'}),
99 (config_keys.backend_objectstore, ObjectStoreBackend, {config_keys.objectstore_bucket: 'test-bucket'}),
100 ])
101 def test_store_stats_and_keys(self, backend_type, expected_instance, extra_conf, ini_settings, random_binary_file):
102 config = ini_settings
103 config[config_keys.backend_type] = backend_type
104 config.update(extra_conf)
105
106 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
107
108 # purge storage before running
109 for shard, k in f_store.iter_artifacts():
110 f_store.delete(k)
111
112 for i in range(10):
113 filename, temp_file = random_binary_file
114
115 metadata = {
116 'user_uploaded': {
117 'username': 'user1',
118 'user_id': 10,
119 'ip': '10.20.30.40'
120 }
121 }
122 store_fid, metadata = f_store.store(filename, temp_file, extra_metadata=metadata)
123 assert store_fid
124 assert metadata
125
126 cnt, size, meta = f_store.get_statistics()
127 assert cnt == 10
128 assert 10 == len(list(f_store.iter_keys()))
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_filesystem_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_filesystem
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreFileSystemBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_filesystem_instance):
37 shard_number = file_store_filesystem_instance.get_shard_index(filename, len(file_store_filesystem_instance._shards))
38 # Check that the shard number is between 0 and max-shards
39 assert 0 <= shard_number <= len(file_store_filesystem_instance._shards)
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 3),
43 ('my-name-2', 2),
44 ('my-name-3', 4),
45 ('my-name-4', 1),
46
47 ('rhodecode-enterprise-ce', 5),
48 ('rhodecode-enterprise-ee', 6),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_filesystem_instance):
51 shard_number = file_store_filesystem_instance.get_shard_index(filename, len(file_store_filesystem_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,17 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/ No newline at end of file
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_legacy_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreLegacyBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_legacy_instance):
37 shard_number = file_store_legacy_instance.get_shard_index(filename, len(file_store_legacy_instance._shards))
38 # Check that the shard number is 0 for legacy filesystem store we don't use shards
39 assert shard_number == 0
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 0),
43 ('my-name-2', 0),
44 ('my-name-3', 0),
45 ('my-name-4', 0),
46
47 ('rhodecode-enterprise-ce', 0),
48 ('rhodecode-enterprise-ee', 0),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_legacy_instance):
51 shard_number = file_store_legacy_instance.get_shard_index(filename, len(file_store_legacy_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,52 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import pytest
19
20 from rhodecode.apps.file_store import utils as store_utils
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.apps.file_store.tests import generate_random_filename
23
24
25 @pytest.fixture()
26 def file_store_objectstore_instance(ini_settings):
27 config = ini_settings
28 config[config_keys.backend_type] = config_keys.backend_objectstore
29 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
30 return f_store
31
32
33 class TestFileStoreObjectStoreBackend:
34
35 @pytest.mark.parametrize('filename', [generate_random_filename() for _ in range(10)])
36 def test_get_shard_number(self, filename, file_store_objectstore_instance):
37 shard_number = file_store_objectstore_instance.get_shard_index(filename, len(file_store_objectstore_instance._shards))
38 # Check that the shard number is between 0 and shards
39 assert 0 <= shard_number <= len(file_store_objectstore_instance._shards)
40
41 @pytest.mark.parametrize('filename, expected_shard_num', [
42 ('my-name-1', 3),
43 ('my-name-2', 2),
44 ('my-name-3', 4),
45 ('my-name-4', 1),
46
47 ('rhodecode-enterprise-ce', 5),
48 ('rhodecode-enterprise-ee', 6),
49 ])
50 def test_get_shard_number_consistency(self, filename, expected_shard_num, file_store_objectstore_instance):
51 shard_number = file_store_objectstore_instance.get_shard_index(filename, len(file_store_objectstore_instance._shards))
52 assert expected_shard_num == shard_number
@@ -0,0 +1,122 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import sys
20 import logging
21
22 import click
23
24 from rhodecode.lib.pyramid_utils import bootstrap
25 from rhodecode.lib.ext_json import json
26 from rhodecode.model.db import FileStore
27 from rhodecode.apps.file_store import utils as store_utils
28
29 log = logging.getLogger(__name__)
30
31
32 @click.command()
33 @click.argument('ini_path', type=click.Path(exists=True))
34 @click.argument('file_uid')
35 @click.option(
36 '--source-backend-conf',
37 type=click.Path(exists=True, dir_okay=False, readable=True),
38 help='Source backend config file path in a json format'
39 )
40 @click.option(
41 '--dest-backend-conf',
42 type=click.Path(exists=True, dir_okay=False, readable=True),
43 help='Source backend config file path in a json format'
44 )
45 def main(ini_path, file_uid, source_backend_conf, dest_backend_conf):
46 return command(ini_path, file_uid, source_backend_conf, dest_backend_conf)
47
48
49 _source_settings = {}
50
51 _dest_settings = {}
52
53
54 def command(ini_path, file_uid, source_backend_conf, dest_backend_conf):
55 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
56 migrate_func(env, file_uid, source_backend_conf, dest_backend_conf)
57
58
59 def migrate_func(env, file_uid, source_backend_conf=None, dest_backend_conf=None):
60 """
61
62 Example usage::
63
64 from rhodecode.lib.rc_commands import migrate_artifact
65 migrate_artifact._source_settings = {
66 'file_store.backend.type': 'filesystem_v1',
67 'file_store.filesystem_v1.storage_path': '/var/opt/rhodecode_data/file_store',
68 }
69 migrate_artifact._dest_settings = {
70 'file_store.backend.type': 'objectstore',
71 'file_store.objectstore.url': 'http://s3-minio:9000',
72 'file_store.objectstore.bucket': 'rhodecode-file-store',
73 'file_store.objectstore.key': 's3admin',
74 'file_store.objectstore.secret': 's3secret4',
75 'file_store.objectstore.region': 'eu-central-1',
76 }
77 for db_obj in FileStore.query().all():
78 migrate_artifact.migrate_func({}, db_obj.file_uid)
79
80 """
81
82 try:
83 from rc_ee.api.views.store_api import _store_file
84 except ImportError:
85 click.secho('ERROR: Unable to import store_api. '
86 'store_api is only available in EE edition of RhodeCode',
87 fg='red')
88 sys.exit(-1)
89
90 source_settings = _source_settings
91 if source_backend_conf:
92 source_settings = json.loads(open(source_backend_conf).read())
93 dest_settings = _dest_settings
94 if dest_backend_conf:
95 dest_settings = json.loads(open(dest_backend_conf).read())
96
97 if file_uid.isnumeric():
98 file_store_db_obj = FileStore().query() \
99 .filter(FileStore.file_store_id == file_uid) \
100 .scalar()
101 else:
102 file_store_db_obj = FileStore().query() \
103 .filter(FileStore.file_uid == file_uid) \
104 .scalar()
105 if not file_store_db_obj:
106 click.secho(f'ERROR: Unable to fetch artifact from database file_uid={file_uid}',
107 fg='red')
108 sys.exit(-1)
109
110 uid_filename = file_store_db_obj.file_uid
111 org_filename = file_store_db_obj.file_display_name
112 click.secho(f'Attempting to migrate artifact {uid_filename}, filename: {org_filename}', fg='green')
113
114 # get old version of f_store based on the data.
115
116 origin_f_store = store_utils.get_filestore_backend(source_settings, always_init=True)
117 reader, metadata = origin_f_store.fetch(uid_filename)
118
119 target_f_store = store_utils.get_filestore_backend(dest_settings, always_init=True)
120 target_f_store.import_to_store(reader, org_filename, uid_filename, metadata)
121
122 click.secho(f'Migrated artifact {uid_filename}, filename: {org_filename} into {target_f_store} storage', fg='green')
@@ -1,858 +1,899 b''
1 1
2 2 ; #########################################
3 3 ; RHODECODE COMMUNITY EDITION CONFIGURATION
4 4 ; #########################################
5 5
6 6 [DEFAULT]
7 7 ; Debug flag sets all loggers to debug, and enables request tracking
8 8 debug = true
9 9
10 10 ; ########################################################################
11 11 ; EMAIL CONFIGURATION
12 12 ; These settings will be used by the RhodeCode mailing system
13 13 ; ########################################################################
14 14
15 15 ; prefix all emails subjects with given prefix, helps filtering out emails
16 16 #email_prefix = [RhodeCode]
17 17
18 18 ; email FROM address all mails will be sent
19 19 #app_email_from = rhodecode-noreply@localhost
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ; COMMON HOST/IP CONFIG, This applies mostly to develop setup,
30 30 ; Host port for gunicorn are controlled by gunicorn_conf.py
31 31 host = 127.0.0.1
32 32 port = 10020
33 33
34 34
35 35 ; ###########################
36 36 ; GUNICORN APPLICATION SERVER
37 37 ; ###########################
38 38
39 39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40 40
41 41 ; Module to use, this setting shouldn't be changed
42 42 use = egg:gunicorn#main
43 43
44 44 ; Prefix middleware for RhodeCode.
45 45 ; recommended when using proxy setup.
46 46 ; allows to set RhodeCode under a prefix in server.
47 47 ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
48 48 ; And set your prefix like: `prefix = /custom_prefix`
49 49 ; be sure to also set beaker.session.cookie_path = /custom_prefix if you need
50 50 ; to make your cookies only work on prefix url
51 51 [filter:proxy-prefix]
52 52 use = egg:PasteDeploy#prefix
53 53 prefix = /
54 54
55 55 [app:main]
56 56 ; The %(here)s variable will be replaced with the absolute path of parent directory
57 57 ; of this file
58 58 ; Each option in the app:main can be override by an environmental variable
59 59 ;
60 60 ;To override an option:
61 61 ;
62 62 ;RC_<KeyName>
63 63 ;Everything should be uppercase, . and - should be replaced by _.
64 64 ;For example, if you have these configuration settings:
65 65 ;rc_cache.repo_object.backend = foo
66 66 ;can be overridden by
67 67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68 68
69 69 use = egg:rhodecode-enterprise-ce
70 70
71 71 ; enable proxy prefix middleware, defined above
72 72 #filter-with = proxy-prefix
73 73
74 74 ; #############
75 75 ; DEBUG OPTIONS
76 76 ; #############
77 77
78 78 pyramid.reload_templates = true
79 79
80 80 # During development the we want to have the debug toolbar enabled
81 81 pyramid.includes =
82 82 pyramid_debugtoolbar
83 83
84 84 debugtoolbar.hosts = 0.0.0.0/0
85 85 debugtoolbar.exclude_prefixes =
86 86 /css
87 87 /fonts
88 88 /images
89 89 /js
90 90
91 91 ## RHODECODE PLUGINS ##
92 92 rhodecode.includes =
93 93 rhodecode.api
94 94
95 95
96 96 # api prefix url
97 97 rhodecode.api.url = /_admin/api
98 98
99 99 ; enable debug style page
100 100 debug_style = true
101 101
102 102 ; #################
103 103 ; END DEBUG OPTIONS
104 104 ; #################
105 105
106 106 ; encryption key used to encrypt social plugin tokens,
107 107 ; remote_urls with credentials etc, if not set it defaults to
108 108 ; `beaker.session.secret`
109 109 #rhodecode.encrypted_values.secret =
110 110
111 111 ; decryption strict mode (enabled by default). It controls if decryption raises
112 112 ; `SignatureVerificationError` in case of wrong key, or damaged encryption data.
113 113 #rhodecode.encrypted_values.strict = false
114 114
115 115 ; Pick algorithm for encryption. Either fernet (more secure) or aes (default)
116 116 ; fernet is safer, and we strongly recommend switching to it.
117 117 ; Due to backward compatibility aes is used as default.
118 118 #rhodecode.encrypted_values.algorithm = fernet
119 119
120 120 ; Return gzipped responses from RhodeCode (static files/application)
121 121 gzip_responses = false
122 122
123 123 ; Auto-generate javascript routes file on startup
124 124 generate_js_files = false
125 125
126 126 ; System global default language.
127 127 ; All available languages: en (default), be, de, es, fr, it, ja, pl, pt, ru, zh
128 128 lang = en
129 129
130 130 ; Perform a full repository scan and import on each server start.
131 131 ; Settings this to true could lead to very long startup time.
132 132 startup.import_repos = false
133 133
134 134 ; URL at which the application is running. This is used for Bootstrapping
135 135 ; requests in context when no web request is available. Used in ishell, or
136 136 ; SSH calls. Set this for events to receive proper url for SSH calls.
137 137 app.base_url = http://rhodecode.local
138 138
139 139 ; Host at which the Service API is running.
140 140 app.service_api.host = http://rhodecode.local:10020
141 141
142 142 ; Secret for Service API authentication.
143 143 app.service_api.token =
144 144
145 145 ; Unique application ID. Should be a random unique string for security.
146 146 app_instance_uuid = rc-production
147 147
148 148 ; Cut off limit for large diffs (size in bytes). If overall diff size on
149 149 ; commit, or pull request exceeds this limit this diff will be displayed
150 150 ; partially. E.g 512000 == 512Kb
151 151 cut_off_limit_diff = 512000
152 152
153 153 ; Cut off limit for large files inside diffs (size in bytes). Each individual
154 154 ; file inside diff which exceeds this limit will be displayed partially.
155 155 ; E.g 128000 == 128Kb
156 156 cut_off_limit_file = 128000
157 157
158 158 ; Use cached version of vcs repositories everywhere. Recommended to be `true`
159 159 vcs_full_cache = true
160 160
161 161 ; Force https in RhodeCode, fixes https redirects, assumes it's always https.
162 162 ; Normally this is controlled by proper flags sent from http server such as Nginx or Apache
163 163 force_https = false
164 164
165 165 ; use Strict-Transport-Security headers
166 166 use_htsts = false
167 167
168 168 ; Set to true if your repos are exposed using the dumb protocol
169 169 git_update_server_info = false
170 170
171 171 ; RSS/ATOM feed options
172 172 rss_cut_off_limit = 256000
173 173 rss_items_per_page = 10
174 174 rss_include_diff = false
175 175
176 176 ; gist URL alias, used to create nicer urls for gist. This should be an
177 177 ; url that does rewrites to _admin/gists/{gistid}.
178 178 ; example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
179 179 ; RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
180 180 gist_alias_url =
181 181
182 182 ; List of views (using glob pattern syntax) that AUTH TOKENS could be
183 183 ; used for access.
184 184 ; Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
185 185 ; came from the the logged in user who own this authentication token.
186 186 ; Additionally @TOKEN syntax can be used to bound the view to specific
187 187 ; authentication token. Such view would be only accessible when used together
188 188 ; with this authentication token
189 189 ; list of all views can be found under `/_admin/permissions/auth_token_access`
190 190 ; The list should be "," separated and on a single line.
191 191 ; Most common views to enable:
192 192
193 193 # RepoCommitsView:repo_commit_download
194 194 # RepoCommitsView:repo_commit_patch
195 195 # RepoCommitsView:repo_commit_raw
196 196 # RepoCommitsView:repo_commit_raw@TOKEN
197 197 # RepoFilesView:repo_files_diff
198 198 # RepoFilesView:repo_archivefile
199 199 # RepoFilesView:repo_file_raw
200 200 # GistView:*
201 201 api_access_controllers_whitelist =
202 202
203 203 ; Default encoding used to convert from and to unicode
204 204 ; can be also a comma separated list of encoding in case of mixed encodings
205 205 default_encoding = UTF-8
206 206
207 207 ; instance-id prefix
208 208 ; a prefix key for this instance used for cache invalidation when running
209 209 ; multiple instances of RhodeCode, make sure it's globally unique for
210 210 ; all running RhodeCode instances. Leave empty if you don't use it
211 211 instance_id =
212 212
213 213 ; Fallback authentication plugin. Set this to a plugin ID to force the usage
214 214 ; of an authentication plugin also if it is disabled by it's settings.
215 215 ; This could be useful if you are unable to log in to the system due to broken
216 216 ; authentication settings. Then you can enable e.g. the internal RhodeCode auth
217 217 ; module to log in again and fix the settings.
218 218 ; Available builtin plugin IDs (hash is part of the ID):
219 219 ; egg:rhodecode-enterprise-ce#rhodecode
220 220 ; egg:rhodecode-enterprise-ce#pam
221 221 ; egg:rhodecode-enterprise-ce#ldap
222 222 ; egg:rhodecode-enterprise-ce#jasig_cas
223 223 ; egg:rhodecode-enterprise-ce#headers
224 224 ; egg:rhodecode-enterprise-ce#crowd
225 225
226 226 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
227 227
228 228 ; Flag to control loading of legacy plugins in py:/path format
229 229 auth_plugin.import_legacy_plugins = true
230 230
231 231 ; alternative return HTTP header for failed authentication. Default HTTP
232 232 ; response is 401 HTTPUnauthorized. Currently HG clients have troubles with
233 233 ; handling that causing a series of failed authentication calls.
234 234 ; Set this variable to 403 to return HTTPForbidden, or any other HTTP code
235 235 ; This will be served instead of default 401 on bad authentication
236 236 auth_ret_code =
237 237
238 238 ; use special detection method when serving auth_ret_code, instead of serving
239 239 ; ret_code directly, use 401 initially (Which triggers credentials prompt)
240 240 ; and then serve auth_ret_code to clients
241 241 auth_ret_code_detection = false
242 242
243 243 ; locking return code. When repository is locked return this HTTP code. 2XX
244 244 ; codes don't break the transactions while 4XX codes do
245 245 lock_ret_code = 423
246 246
247 247 ; Filesystem location were repositories should be stored
248 248 repo_store.path = /var/opt/rhodecode_repo_store
249 249
250 250 ; allows to setup custom hooks in settings page
251 251 allow_custom_hooks_settings = true
252 252
253 253 ; Generated license token required for EE edition license.
254 254 ; New generated token value can be found in Admin > settings > license page.
255 255 license_token =
256 256
257 257 ; This flag hides sensitive information on the license page such as token, and license data
258 258 license.hide_license_info = false
259 259
260 260 ; supervisor connection uri, for managing supervisor and logs.
261 261 supervisor.uri =
262 262
263 263 ; supervisord group name/id we only want this RC instance to handle
264 264 supervisor.group_id = dev
265 265
266 266 ; Display extended labs settings
267 267 labs_settings_active = true
268 268
269 269 ; Custom exception store path, defaults to TMPDIR
270 270 ; This is used to store exception from RhodeCode in shared directory
271 271 #exception_tracker.store_path =
272 272
273 273 ; Send email with exception details when it happens
274 274 #exception_tracker.send_email = false
275 275
276 276 ; Comma separated list of recipients for exception emails,
277 277 ; e.g admin@rhodecode.com,devops@rhodecode.com
278 278 ; Can be left empty, then emails will be sent to ALL super-admins
279 279 #exception_tracker.send_email_recipients =
280 280
281 281 ; optional prefix to Add to email Subject
282 282 #exception_tracker.email_prefix = [RHODECODE ERROR]
283 283
284 ; File store configuration. This is used to store and serve uploaded files
285 file_store.enabled = true
284 ; NOTE: this setting IS DEPRECATED:
285 ; file_store backend is always enabled
286 #file_store.enabled = true
286 287
288 ; NOTE: this setting IS DEPRECATED:
289 ; file_store.backend = X -> use `file_store.backend.type = filesystem_v2` instead
287 290 ; Storage backend, available options are: local
288 file_store.backend = local
291 #file_store.backend = local
289 292
293 ; NOTE: this setting IS DEPRECATED:
294 ; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
290 295 ; path to store the uploaded binaries and artifacts
291 file_store.storage_path = /var/opt/rhodecode_data/file_store
296 #file_store.storage_path = /var/opt/rhodecode_data/file_store
297
298 ; Artifacts file-store, is used to store comment attachments and artifacts uploads.
299 ; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
300 ; filesystem_v1 is backwards compat with pre 5.1 storage changes
301 ; new installations should choose filesystem_v2 or objectstore (s3-based), pick filesystem when migrating from
302 ; previous installations to keep the artifacts without a need of migration
303 #file_store.backend.type = filesystem_v2
304
305 ; filesystem options...
306 #file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/artifacts_file_store
307
308 ; filesystem_v2 options...
309 #file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/artifacts_file_store
310 #file_store.filesystem_v2.shards = 8
292 311
312 ; objectstore options...
313 ; url for s3 compatible storage that allows to upload artifacts
314 ; e.g http://minio:9000
315 #file_store.backend.type = objectstore
316 #file_store.objectstore.url = http://s3-minio:9000
317
318 ; a top-level bucket to put all other shards in
319 ; objects will be stored in rhodecode-file-store/shard-N based on the bucket_shards number
320 #file_store.objectstore.bucket = rhodecode-file-store
321
322 ; number of sharded buckets to create to distribute archives across
323 ; default is 8 shards
324 #file_store.objectstore.bucket_shards = 8
325
326 ; key for s3 auth
327 #file_store.objectstore.key = s3admin
328
329 ; secret for s3 auth
330 #file_store.objectstore.secret = s3secret4
331
332 ;region for s3 storage
333 #file_store.objectstore.region = eu-central-1
293 334
294 335 ; Redis url to acquire/check generation of archives locks
295 336 archive_cache.locking.url = redis://redis:6379/1
296 337
297 338 ; Storage backend, only 'filesystem' and 'objectstore' are available now
298 339 archive_cache.backend.type = filesystem
299 340
300 341 ; url for s3 compatible storage that allows to upload artifacts
301 342 ; e.g http://minio:9000
302 343 archive_cache.objectstore.url = http://s3-minio:9000
303 344
304 345 ; key for s3 auth
305 346 archive_cache.objectstore.key = key
306 347
307 348 ; secret for s3 auth
308 349 archive_cache.objectstore.secret = secret
309 350
310 351 ;region for s3 storage
311 352 archive_cache.objectstore.region = eu-central-1
312 353
313 354 ; number of sharded buckets to create to distribute archives across
314 355 ; default is 8 shards
315 356 archive_cache.objectstore.bucket_shards = 8
316 357
317 358 ; a top-level bucket to put all other shards in
318 359 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
319 360 archive_cache.objectstore.bucket = rhodecode-archive-cache
320 361
321 362 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
322 363 archive_cache.objectstore.retry = false
323 364
324 365 ; number of seconds to wait for next try using retry
325 366 archive_cache.objectstore.retry_backoff = 1
326 367
327 368 ; how many tries do do a retry fetch from this backend
328 369 archive_cache.objectstore.retry_attempts = 10
329 370
330 371 ; Default is $cache_dir/archive_cache if not set
331 372 ; Generated repo archives will be cached at this location
332 373 ; and served from the cache during subsequent requests for the same archive of
333 374 ; the repository. This path is important to be shared across filesystems and with
334 375 ; RhodeCode and vcsserver
335 376 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
336 377
337 378 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
338 379 archive_cache.filesystem.cache_size_gb = 1
339 380
340 381 ; Eviction policy used to clear out after cache_size_gb limit is reached
341 382 archive_cache.filesystem.eviction_policy = least-recently-stored
342 383
343 384 ; By default cache uses sharding technique, this specifies how many shards are there
344 385 ; default is 8 shards
345 386 archive_cache.filesystem.cache_shards = 8
346 387
347 388 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
348 389 archive_cache.filesystem.retry = false
349 390
350 391 ; number of seconds to wait for next try using retry
351 392 archive_cache.filesystem.retry_backoff = 1
352 393
353 394 ; how many tries do do a retry fetch from this backend
354 395 archive_cache.filesystem.retry_attempts = 10
355 396
356 397
357 398 ; #############
358 399 ; CELERY CONFIG
359 400 ; #############
360 401
361 402 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini
362 403
363 404 use_celery = true
364 405
365 406 ; path to store schedule database
366 407 #celerybeat-schedule.path =
367 408
368 409 ; connection url to the message broker (default redis)
369 410 celery.broker_url = redis://redis:6379/8
370 411
371 412 ; results backend to get results for (default redis)
372 413 celery.result_backend = redis://redis:6379/8
373 414
374 415 ; rabbitmq example
375 416 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
376 417
377 418 ; maximum tasks to execute before worker restart
378 419 celery.max_tasks_per_child = 20
379 420
380 421 ; tasks will never be sent to the queue, but executed locally instead.
381 422 celery.task_always_eager = false
382 423
383 424 ; #############
384 425 ; DOGPILE CACHE
385 426 ; #############
386 427
387 428 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
388 429 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
389 430 cache_dir = /var/opt/rhodecode_data
390 431
391 432 ; *********************************************
392 433 ; `sql_cache_short` cache for heavy SQL queries
393 434 ; Only supported backend is `memory_lru`
394 435 ; *********************************************
395 436 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
396 437 rc_cache.sql_cache_short.expiration_time = 30
397 438
398 439
399 440 ; *****************************************************
400 441 ; `cache_repo_longterm` cache for repo object instances
401 442 ; Only supported backend is `memory_lru`
402 443 ; *****************************************************
403 444 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
404 445 ; by default we use 30 Days, cache is still invalidated on push
405 446 rc_cache.cache_repo_longterm.expiration_time = 2592000
406 447 ; max items in LRU cache, set to smaller number to save memory, and expire last used caches
407 448 rc_cache.cache_repo_longterm.max_size = 10000
408 449
409 450
410 451 ; *********************************************
411 452 ; `cache_general` cache for general purpose use
412 453 ; for simplicity use rc.file_namespace backend,
413 454 ; for performance and scale use rc.redis
414 455 ; *********************************************
415 456 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
416 457 rc_cache.cache_general.expiration_time = 43200
417 458 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
418 459 #rc_cache.cache_general.arguments.filename = /tmp/cache_general_db
419 460
420 461 ; alternative `cache_general` redis backend with distributed lock
421 462 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
422 463 #rc_cache.cache_general.expiration_time = 300
423 464
424 465 ; redis_expiration_time needs to be greater then expiration_time
425 466 #rc_cache.cache_general.arguments.redis_expiration_time = 7200
426 467
427 468 #rc_cache.cache_general.arguments.host = localhost
428 469 #rc_cache.cache_general.arguments.port = 6379
429 470 #rc_cache.cache_general.arguments.db = 0
430 471 #rc_cache.cache_general.arguments.socket_timeout = 30
431 472 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
432 473 #rc_cache.cache_general.arguments.distributed_lock = true
433 474
434 475 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
435 476 #rc_cache.cache_general.arguments.lock_auto_renewal = true
436 477
437 478 ; *************************************************
438 479 ; `cache_perms` cache for permission tree, auth TTL
439 480 ; for simplicity use rc.file_namespace backend,
440 481 ; for performance and scale use rc.redis
441 482 ; *************************************************
442 483 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
443 484 rc_cache.cache_perms.expiration_time = 3600
444 485 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
445 486 #rc_cache.cache_perms.arguments.filename = /tmp/cache_perms_db
446 487
447 488 ; alternative `cache_perms` redis backend with distributed lock
448 489 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
449 490 #rc_cache.cache_perms.expiration_time = 300
450 491
451 492 ; redis_expiration_time needs to be greater then expiration_time
452 493 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
453 494
454 495 #rc_cache.cache_perms.arguments.host = localhost
455 496 #rc_cache.cache_perms.arguments.port = 6379
456 497 #rc_cache.cache_perms.arguments.db = 0
457 498 #rc_cache.cache_perms.arguments.socket_timeout = 30
458 499 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
459 500 #rc_cache.cache_perms.arguments.distributed_lock = true
460 501
461 502 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
462 503 #rc_cache.cache_perms.arguments.lock_auto_renewal = true
463 504
464 505 ; ***************************************************
465 506 ; `cache_repo` cache for file tree, Readme, RSS FEEDS
466 507 ; for simplicity use rc.file_namespace backend,
467 508 ; for performance and scale use rc.redis
468 509 ; ***************************************************
469 510 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
470 511 rc_cache.cache_repo.expiration_time = 2592000
471 512 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
472 513 #rc_cache.cache_repo.arguments.filename = /tmp/cache_repo_db
473 514
474 515 ; alternative `cache_repo` redis backend with distributed lock
475 516 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
476 517 #rc_cache.cache_repo.expiration_time = 2592000
477 518
478 519 ; redis_expiration_time needs to be greater then expiration_time
479 520 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
480 521
481 522 #rc_cache.cache_repo.arguments.host = localhost
482 523 #rc_cache.cache_repo.arguments.port = 6379
483 524 #rc_cache.cache_repo.arguments.db = 1
484 525 #rc_cache.cache_repo.arguments.socket_timeout = 30
485 526 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
486 527 #rc_cache.cache_repo.arguments.distributed_lock = true
487 528
488 529 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
489 530 #rc_cache.cache_repo.arguments.lock_auto_renewal = true
490 531
491 532 ; ##############
492 533 ; BEAKER SESSION
493 534 ; ##############
494 535
495 536 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
496 537 ; types are file, ext:redis, ext:database, ext:memcached
497 538 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
498 539 #beaker.session.type = file
499 540 #beaker.session.data_dir = %(here)s/data/sessions
500 541
501 542 ; Redis based sessions
502 543 beaker.session.type = ext:redis
503 544 beaker.session.url = redis://redis:6379/2
504 545
505 546 ; DB based session, fast, and allows easy management over logged in users
506 547 #beaker.session.type = ext:database
507 548 #beaker.session.table_name = db_session
508 549 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
509 550 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
510 551 #beaker.session.sa.pool_recycle = 3600
511 552 #beaker.session.sa.echo = false
512 553
513 554 beaker.session.key = rhodecode
514 555 beaker.session.secret = develop-rc-uytcxaz
515 556 beaker.session.lock_dir = /data_ramdisk/lock
516 557
517 558 ; Secure encrypted cookie. Requires AES and AES python libraries
518 559 ; you must disable beaker.session.secret to use this
519 560 #beaker.session.encrypt_key = key_for_encryption
520 561 #beaker.session.validate_key = validation_key
521 562
522 563 ; Sets session as invalid (also logging out user) if it haven not been
523 564 ; accessed for given amount of time in seconds
524 565 beaker.session.timeout = 2592000
525 566 beaker.session.httponly = true
526 567
527 568 ; Path to use for the cookie. Set to prefix if you use prefix middleware
528 569 #beaker.session.cookie_path = /custom_prefix
529 570
530 571 ; Set https secure cookie
531 572 beaker.session.secure = false
532 573
533 574 ; default cookie expiration time in seconds, set to `true` to set expire
534 575 ; at browser close
535 576 #beaker.session.cookie_expires = 3600
536 577
537 578 ; #############################
538 579 ; SEARCH INDEXING CONFIGURATION
539 580 ; #############################
540 581
541 582 ; Full text search indexer is available in rhodecode-tools under
542 583 ; `rhodecode-tools index` command
543 584
544 585 ; WHOOSH Backend, doesn't require additional services to run
545 586 ; it works good with few dozen repos
546 587 search.module = rhodecode.lib.index.whoosh
547 588 search.location = %(here)s/data/index
548 589
549 590 ; ####################
550 591 ; CHANNELSTREAM CONFIG
551 592 ; ####################
552 593
553 594 ; channelstream enables persistent connections and live notification
554 595 ; in the system. It's also used by the chat system
555 596
556 597 channelstream.enabled = true
557 598
558 599 ; server address for channelstream server on the backend
559 600 channelstream.server = channelstream:9800
560 601
561 602 ; location of the channelstream server from outside world
562 603 ; use ws:// for http or wss:// for https. This address needs to be handled
563 604 ; by external HTTP server such as Nginx or Apache
564 605 ; see Nginx/Apache configuration examples in our docs
565 606 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
566 607 channelstream.secret = ENV_GENERATED
567 608 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
568 609
569 610 ; Internal application path that Javascript uses to connect into.
570 611 ; If you use proxy-prefix the prefix should be added before /_channelstream
571 612 channelstream.proxy_path = /_channelstream
572 613
573 614
574 615 ; ##############################
575 616 ; MAIN RHODECODE DATABASE CONFIG
576 617 ; ##############################
577 618
578 619 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
579 620 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
580 621 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
581 622 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
582 623 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
583 624
584 625 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
585 626
586 627 ; see sqlalchemy docs for other advanced settings
587 628 ; print the sql statements to output
588 629 sqlalchemy.db1.echo = false
589 630
590 631 ; recycle the connections after this amount of seconds
591 632 sqlalchemy.db1.pool_recycle = 3600
592 633
593 634 ; the number of connections to keep open inside the connection pool.
594 635 ; 0 indicates no limit
595 636 ; the general calculus with gevent is:
596 637 ; if your system allows 500 concurrent greenlets (max_connections) that all do database access,
597 638 ; then increase pool size + max overflow so that they add up to 500.
598 639 #sqlalchemy.db1.pool_size = 5
599 640
600 641 ; The number of connections to allow in connection pool "overflow", that is
601 642 ; connections that can be opened above and beyond the pool_size setting,
602 643 ; which defaults to five.
603 644 #sqlalchemy.db1.max_overflow = 10
604 645
605 646 ; Connection check ping, used to detect broken database connections
606 647 ; could be enabled to better handle cases if MySQL has gone away errors
607 648 #sqlalchemy.db1.ping_connection = true
608 649
609 650 ; ##########
610 651 ; VCS CONFIG
611 652 ; ##########
612 653 vcs.server.enable = true
613 654 vcs.server = vcsserver:10010
614 655
615 656 ; Web server connectivity protocol, responsible for web based VCS operations
616 657 ; Available protocols are:
617 658 ; `http` - use http-rpc backend (default)
618 659 vcs.server.protocol = http
619 660
620 661 ; Push/Pull operations protocol, available options are:
621 662 ; `http` - use http-rpc backend (default)
622 663 vcs.scm_app_implementation = http
623 664
624 665 ; Push/Pull operations hooks protocol, available options are:
625 666 ; `http` - use http-rpc backend (default)
626 667 ; `celery` - use celery based hooks
627 668 #DEPRECATED:vcs.hooks.protocol = http
628 669 vcs.hooks.protocol.v2 = celery
629 670
630 671 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
631 672 ; accessible via network.
632 673 ; Use vcs.hooks.host = "*" to bind to current hostname (for Docker)
633 674 vcs.hooks.host = *
634 675
635 676 ; Start VCSServer with this instance as a subprocess, useful for development
636 677 vcs.start_server = false
637 678
638 679 ; List of enabled VCS backends, available options are:
639 680 ; `hg` - mercurial
640 681 ; `git` - git
641 682 ; `svn` - subversion
642 683 vcs.backends = hg, git, svn
643 684
644 685 ; Wait this number of seconds before killing connection to the vcsserver
645 686 vcs.connection_timeout = 3600
646 687
647 688 ; Cache flag to cache vcsserver remote calls locally
648 689 ; It uses cache_region `cache_repo`
649 690 vcs.methods.cache = true
650 691
651 692 ; ####################################################
652 693 ; Subversion proxy support (mod_dav_svn)
653 694 ; Maps RhodeCode repo groups into SVN paths for Apache
654 695 ; ####################################################
655 696
656 697 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
657 698 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
658 699 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
659 700 #vcs.svn.compatible_version = 1.8
660 701
661 702 ; Redis connection settings for svn integrations logic
662 703 ; This connection string needs to be the same on ce and vcsserver
663 704 vcs.svn.redis_conn = redis://redis:6379/0
664 705
665 706 ; Enable SVN proxy of requests over HTTP
666 707 vcs.svn.proxy.enabled = true
667 708
668 709 ; host to connect to running SVN subsystem
669 710 vcs.svn.proxy.host = http://svn:8090
670 711
671 712 ; Enable or disable the config file generation.
672 713 svn.proxy.generate_config = true
673 714
674 715 ; Generate config file with `SVNListParentPath` set to `On`.
675 716 svn.proxy.list_parent_path = true
676 717
677 718 ; Set location and file name of generated config file.
678 719 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
679 720
680 721 ; alternative mod_dav config template. This needs to be a valid mako template
681 722 ; Example template can be found in the source code:
682 723 ; rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
683 724 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
684 725
685 726 ; Used as a prefix to the `Location` block in the generated config file.
686 727 ; In most cases it should be set to `/`.
687 728 svn.proxy.location_root = /
688 729
689 730 ; Command to reload the mod dav svn configuration on change.
690 731 ; Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
691 732 ; Make sure user who runs RhodeCode process is allowed to reload Apache
692 733 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
693 734
694 735 ; If the timeout expires before the reload command finishes, the command will
695 736 ; be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
696 737 #svn.proxy.reload_timeout = 10
697 738
698 739 ; ####################
699 740 ; SSH Support Settings
700 741 ; ####################
701 742
702 743 ; Defines if a custom authorized_keys file should be created and written on
703 744 ; any change user ssh keys. Setting this to false also disables possibility
704 745 ; of adding SSH keys by users from web interface. Super admins can still
705 746 ; manage SSH Keys.
706 747 ssh.generate_authorized_keyfile = true
707 748
708 749 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
709 750 # ssh.authorized_keys_ssh_opts =
710 751
711 752 ; Path to the authorized_keys file where the generate entries are placed.
712 753 ; It is possible to have multiple key files specified in `sshd_config` e.g.
713 754 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
714 755 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
715 756
716 757 ; Command to execute the SSH wrapper. The binary is available in the
717 758 ; RhodeCode installation directory.
718 759 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
719 760 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
720 761 #DEPRECATED: ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
721 762 ssh.wrapper_cmd.v2 = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
722 763
723 764 ; Allow shell when executing the ssh-wrapper command
724 765 ssh.wrapper_cmd_allow_shell = false
725 766
726 767 ; Enables logging, and detailed output send back to the client during SSH
727 768 ; operations. Useful for debugging, shouldn't be used in production.
728 769 ssh.enable_debug_logging = true
729 770
730 771 ; Paths to binary executable, by default they are the names, but we can
731 772 ; override them if we want to use a custom one
732 773 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
733 774 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
734 775 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
735 776
736 777 ; Enables SSH key generator web interface. Disabling this still allows users
737 778 ; to add their own keys.
738 779 ssh.enable_ui_key_generator = true
739 780
740 781 ; Statsd client config, this is used to send metrics to statsd
741 782 ; We recommend setting statsd_exported and scrape them using Prometheus
742 783 #statsd.enabled = false
743 784 #statsd.statsd_host = 0.0.0.0
744 785 #statsd.statsd_port = 8125
745 786 #statsd.statsd_prefix =
746 787 #statsd.statsd_ipv6 = false
747 788
748 789 ; configure logging automatically at server startup set to false
749 790 ; to use the below custom logging config.
750 791 ; RC_LOGGING_FORMATTER
751 792 ; RC_LOGGING_LEVEL
752 793 ; env variables can control the settings for logging in case of autoconfigure
753 794
754 795 #logging.autoconfigure = true
755 796
756 797 ; specify your own custom logging config file to configure logging
757 798 #logging.logging_conf_file = /path/to/custom_logging.ini
758 799
759 800 ; Dummy marker to add new entries after.
760 801 ; Add any custom entries below. Please don't remove this marker.
761 802 custom.conf = 1
762 803
763 804
764 805 ; #####################
765 806 ; LOGGING CONFIGURATION
766 807 ; #####################
767 808
768 809 [loggers]
769 810 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
770 811
771 812 [handlers]
772 813 keys = console, console_sql
773 814
774 815 [formatters]
775 816 keys = generic, json, color_formatter, color_formatter_sql
776 817
777 818 ; #######
778 819 ; LOGGERS
779 820 ; #######
780 821 [logger_root]
781 822 level = NOTSET
782 823 handlers = console
783 824
784 825 [logger_sqlalchemy]
785 826 level = INFO
786 827 handlers = console_sql
787 828 qualname = sqlalchemy.engine
788 829 propagate = 0
789 830
790 831 [logger_beaker]
791 832 level = DEBUG
792 833 handlers =
793 834 qualname = beaker.container
794 835 propagate = 1
795 836
796 837 [logger_rhodecode]
797 838 level = DEBUG
798 839 handlers =
799 840 qualname = rhodecode
800 841 propagate = 1
801 842
802 843 [logger_ssh_wrapper]
803 844 level = DEBUG
804 845 handlers =
805 846 qualname = ssh_wrapper
806 847 propagate = 1
807 848
808 849 [logger_celery]
809 850 level = DEBUG
810 851 handlers =
811 852 qualname = celery
812 853
813 854
814 855 ; ########
815 856 ; HANDLERS
816 857 ; ########
817 858
818 859 [handler_console]
819 860 class = StreamHandler
820 861 args = (sys.stderr, )
821 862 level = DEBUG
822 863 ; To enable JSON formatted logs replace 'generic/color_formatter' with 'json'
823 864 ; This allows sending properly formatted logs to grafana loki or elasticsearch
824 865 formatter = color_formatter
825 866
826 867 [handler_console_sql]
827 868 ; "level = DEBUG" logs SQL queries and results.
828 869 ; "level = INFO" logs SQL queries.
829 870 ; "level = WARN" logs neither. (Recommended for production systems.)
830 871 class = StreamHandler
831 872 args = (sys.stderr, )
832 873 level = WARN
833 874 ; To enable JSON formatted logs replace 'generic/color_formatter_sql' with 'json'
834 875 ; This allows sending properly formatted logs to grafana loki or elasticsearch
835 876 formatter = color_formatter_sql
836 877
837 878 ; ##########
838 879 ; FORMATTERS
839 880 ; ##########
840 881
841 882 [formatter_generic]
842 883 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
843 884 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
844 885 datefmt = %Y-%m-%d %H:%M:%S
845 886
846 887 [formatter_color_formatter]
847 888 class = rhodecode.lib.logging_formatter.ColorFormatter
848 889 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
849 890 datefmt = %Y-%m-%d %H:%M:%S
850 891
851 892 [formatter_color_formatter_sql]
852 893 class = rhodecode.lib.logging_formatter.ColorFormatterSql
853 894 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
854 895 datefmt = %Y-%m-%d %H:%M:%S
855 896
856 897 [formatter_json]
857 898 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
858 899 class = rhodecode.lib._vendor.jsonlogger.JsonFormatter
@@ -1,826 +1,867 b''
1 1
2 2 ; #########################################
3 3 ; RHODECODE COMMUNITY EDITION CONFIGURATION
4 4 ; #########################################
5 5
6 6 [DEFAULT]
7 7 ; Debug flag sets all loggers to debug, and enables request tracking
8 8 debug = false
9 9
10 10 ; ########################################################################
11 11 ; EMAIL CONFIGURATION
12 12 ; These settings will be used by the RhodeCode mailing system
13 13 ; ########################################################################
14 14
15 15 ; prefix all emails subjects with given prefix, helps filtering out emails
16 16 #email_prefix = [RhodeCode]
17 17
18 18 ; email FROM address all mails will be sent
19 19 #app_email_from = rhodecode-noreply@localhost
20 20
21 21 #smtp_server = mail.server.com
22 22 #smtp_username =
23 23 #smtp_password =
24 24 #smtp_port =
25 25 #smtp_use_tls = false
26 26 #smtp_use_ssl = true
27 27
28 28 [server:main]
29 29 ; COMMON HOST/IP CONFIG, This applies mostly to develop setup,
30 30 ; Host port for gunicorn are controlled by gunicorn_conf.py
31 31 host = 127.0.0.1
32 32 port = 10020
33 33
34 34
35 35 ; ###########################
36 36 ; GUNICORN APPLICATION SERVER
37 37 ; ###########################
38 38
39 39 ; run with gunicorn --config gunicorn_conf.py --paste rhodecode.ini
40 40
41 41 ; Module to use, this setting shouldn't be changed
42 42 use = egg:gunicorn#main
43 43
44 44 ; Prefix middleware for RhodeCode.
45 45 ; recommended when using proxy setup.
46 46 ; allows to set RhodeCode under a prefix in server.
47 47 ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well.
48 48 ; And set your prefix like: `prefix = /custom_prefix`
49 49 ; be sure to also set beaker.session.cookie_path = /custom_prefix if you need
50 50 ; to make your cookies only work on prefix url
51 51 [filter:proxy-prefix]
52 52 use = egg:PasteDeploy#prefix
53 53 prefix = /
54 54
55 55 [app:main]
56 56 ; The %(here)s variable will be replaced with the absolute path of parent directory
57 57 ; of this file
58 58 ; Each option in the app:main can be override by an environmental variable
59 59 ;
60 60 ;To override an option:
61 61 ;
62 62 ;RC_<KeyName>
63 63 ;Everything should be uppercase, . and - should be replaced by _.
64 64 ;For example, if you have these configuration settings:
65 65 ;rc_cache.repo_object.backend = foo
66 66 ;can be overridden by
67 67 ;export RC_CACHE_REPO_OBJECT_BACKEND=foo
68 68
69 69 use = egg:rhodecode-enterprise-ce
70 70
71 71 ; enable proxy prefix middleware, defined above
72 72 #filter-with = proxy-prefix
73 73
74 74 ; encryption key used to encrypt social plugin tokens,
75 75 ; remote_urls with credentials etc, if not set it defaults to
76 76 ; `beaker.session.secret`
77 77 #rhodecode.encrypted_values.secret =
78 78
79 79 ; decryption strict mode (enabled by default). It controls if decryption raises
80 80 ; `SignatureVerificationError` in case of wrong key, or damaged encryption data.
81 81 #rhodecode.encrypted_values.strict = false
82 82
83 83 ; Pick algorithm for encryption. Either fernet (more secure) or aes (default)
84 84 ; fernet is safer, and we strongly recommend switching to it.
85 85 ; Due to backward compatibility aes is used as default.
86 86 #rhodecode.encrypted_values.algorithm = fernet
87 87
88 88 ; Return gzipped responses from RhodeCode (static files/application)
89 89 gzip_responses = false
90 90
91 91 ; Auto-generate javascript routes file on startup
92 92 generate_js_files = false
93 93
94 94 ; System global default language.
95 95 ; All available languages: en (default), be, de, es, fr, it, ja, pl, pt, ru, zh
96 96 lang = en
97 97
98 98 ; Perform a full repository scan and import on each server start.
99 99 ; Settings this to true could lead to very long startup time.
100 100 startup.import_repos = false
101 101
102 102 ; URL at which the application is running. This is used for Bootstrapping
103 103 ; requests in context when no web request is available. Used in ishell, or
104 104 ; SSH calls. Set this for events to receive proper url for SSH calls.
105 105 app.base_url = http://rhodecode.local
106 106
107 107 ; Host at which the Service API is running.
108 108 app.service_api.host = http://rhodecode.local:10020
109 109
110 110 ; Secret for Service API authentication.
111 111 app.service_api.token =
112 112
113 113 ; Unique application ID. Should be a random unique string for security.
114 114 app_instance_uuid = rc-production
115 115
116 116 ; Cut off limit for large diffs (size in bytes). If overall diff size on
117 117 ; commit, or pull request exceeds this limit this diff will be displayed
118 118 ; partially. E.g 512000 == 512Kb
119 119 cut_off_limit_diff = 512000
120 120
121 121 ; Cut off limit for large files inside diffs (size in bytes). Each individual
122 122 ; file inside diff which exceeds this limit will be displayed partially.
123 123 ; E.g 128000 == 128Kb
124 124 cut_off_limit_file = 128000
125 125
126 126 ; Use cached version of vcs repositories everywhere. Recommended to be `true`
127 127 vcs_full_cache = true
128 128
129 129 ; Force https in RhodeCode, fixes https redirects, assumes it's always https.
130 130 ; Normally this is controlled by proper flags sent from http server such as Nginx or Apache
131 131 force_https = false
132 132
133 133 ; use Strict-Transport-Security headers
134 134 use_htsts = false
135 135
136 136 ; Set to true if your repos are exposed using the dumb protocol
137 137 git_update_server_info = false
138 138
139 139 ; RSS/ATOM feed options
140 140 rss_cut_off_limit = 256000
141 141 rss_items_per_page = 10
142 142 rss_include_diff = false
143 143
144 144 ; gist URL alias, used to create nicer urls for gist. This should be an
145 145 ; url that does rewrites to _admin/gists/{gistid}.
146 146 ; example: http://gist.rhodecode.org/{gistid}. Empty means use the internal
147 147 ; RhodeCode url, ie. http[s]://rhodecode.server/_admin/gists/{gistid}
148 148 gist_alias_url =
149 149
150 150 ; List of views (using glob pattern syntax) that AUTH TOKENS could be
151 151 ; used for access.
152 152 ; Adding ?auth_token=TOKEN_HASH to the url authenticates this request as if it
153 153 ; came from the the logged in user who own this authentication token.
154 154 ; Additionally @TOKEN syntax can be used to bound the view to specific
155 155 ; authentication token. Such view would be only accessible when used together
156 156 ; with this authentication token
157 157 ; list of all views can be found under `/_admin/permissions/auth_token_access`
158 158 ; The list should be "," separated and on a single line.
159 159 ; Most common views to enable:
160 160
161 161 # RepoCommitsView:repo_commit_download
162 162 # RepoCommitsView:repo_commit_patch
163 163 # RepoCommitsView:repo_commit_raw
164 164 # RepoCommitsView:repo_commit_raw@TOKEN
165 165 # RepoFilesView:repo_files_diff
166 166 # RepoFilesView:repo_archivefile
167 167 # RepoFilesView:repo_file_raw
168 168 # GistView:*
169 169 api_access_controllers_whitelist =
170 170
171 171 ; Default encoding used to convert from and to unicode
172 172 ; can be also a comma separated list of encoding in case of mixed encodings
173 173 default_encoding = UTF-8
174 174
175 175 ; instance-id prefix
176 176 ; a prefix key for this instance used for cache invalidation when running
177 177 ; multiple instances of RhodeCode, make sure it's globally unique for
178 178 ; all running RhodeCode instances. Leave empty if you don't use it
179 179 instance_id =
180 180
181 181 ; Fallback authentication plugin. Set this to a plugin ID to force the usage
182 182 ; of an authentication plugin also if it is disabled by it's settings.
183 183 ; This could be useful if you are unable to log in to the system due to broken
184 184 ; authentication settings. Then you can enable e.g. the internal RhodeCode auth
185 185 ; module to log in again and fix the settings.
186 186 ; Available builtin plugin IDs (hash is part of the ID):
187 187 ; egg:rhodecode-enterprise-ce#rhodecode
188 188 ; egg:rhodecode-enterprise-ce#pam
189 189 ; egg:rhodecode-enterprise-ce#ldap
190 190 ; egg:rhodecode-enterprise-ce#jasig_cas
191 191 ; egg:rhodecode-enterprise-ce#headers
192 192 ; egg:rhodecode-enterprise-ce#crowd
193 193
194 194 #rhodecode.auth_plugin_fallback = egg:rhodecode-enterprise-ce#rhodecode
195 195
196 196 ; Flag to control loading of legacy plugins in py:/path format
197 197 auth_plugin.import_legacy_plugins = true
198 198
199 199 ; alternative return HTTP header for failed authentication. Default HTTP
200 200 ; response is 401 HTTPUnauthorized. Currently HG clients have troubles with
201 201 ; handling that causing a series of failed authentication calls.
202 202 ; Set this variable to 403 to return HTTPForbidden, or any other HTTP code
203 203 ; This will be served instead of default 401 on bad authentication
204 204 auth_ret_code =
205 205
206 206 ; use special detection method when serving auth_ret_code, instead of serving
207 207 ; ret_code directly, use 401 initially (Which triggers credentials prompt)
208 208 ; and then serve auth_ret_code to clients
209 209 auth_ret_code_detection = false
210 210
211 211 ; locking return code. When repository is locked return this HTTP code. 2XX
212 212 ; codes don't break the transactions while 4XX codes do
213 213 lock_ret_code = 423
214 214
215 215 ; Filesystem location were repositories should be stored
216 216 repo_store.path = /var/opt/rhodecode_repo_store
217 217
218 218 ; allows to setup custom hooks in settings page
219 219 allow_custom_hooks_settings = true
220 220
221 221 ; Generated license token required for EE edition license.
222 222 ; New generated token value can be found in Admin > settings > license page.
223 223 license_token =
224 224
225 225 ; This flag hides sensitive information on the license page such as token, and license data
226 226 license.hide_license_info = false
227 227
228 228 ; supervisor connection uri, for managing supervisor and logs.
229 229 supervisor.uri =
230 230
231 231 ; supervisord group name/id we only want this RC instance to handle
232 232 supervisor.group_id = prod
233 233
234 234 ; Display extended labs settings
235 235 labs_settings_active = true
236 236
237 237 ; Custom exception store path, defaults to TMPDIR
238 238 ; This is used to store exception from RhodeCode in shared directory
239 239 #exception_tracker.store_path =
240 240
241 241 ; Send email with exception details when it happens
242 242 #exception_tracker.send_email = false
243 243
244 244 ; Comma separated list of recipients for exception emails,
245 245 ; e.g admin@rhodecode.com,devops@rhodecode.com
246 246 ; Can be left empty, then emails will be sent to ALL super-admins
247 247 #exception_tracker.send_email_recipients =
248 248
249 249 ; optional prefix to Add to email Subject
250 250 #exception_tracker.email_prefix = [RHODECODE ERROR]
251 251
252 ; File store configuration. This is used to store and serve uploaded files
253 file_store.enabled = true
252 ; NOTE: this setting IS DEPRECATED:
253 ; file_store backend is always enabled
254 #file_store.enabled = true
254 255
256 ; NOTE: this setting IS DEPRECATED:
257 ; file_store.backend = X -> use `file_store.backend.type = filesystem_v2` instead
255 258 ; Storage backend, available options are: local
256 file_store.backend = local
259 #file_store.backend = local
257 260
261 ; NOTE: this setting IS DEPRECATED:
262 ; file_store.storage_path = X -> use `file_store.filesystem_v2.storage_path = X` instead
258 263 ; path to store the uploaded binaries and artifacts
259 file_store.storage_path = /var/opt/rhodecode_data/file_store
264 #file_store.storage_path = /var/opt/rhodecode_data/file_store
265
266 ; Artifacts file-store, is used to store comment attachments and artifacts uploads.
267 ; file_store backend type: filesystem_v1, filesystem_v2 or objectstore (s3-based) are available as options
268 ; filesystem_v1 is backwards compat with pre 5.1 storage changes
269 ; new installations should choose filesystem_v2 or objectstore (s3-based), pick filesystem when migrating from
270 ; previous installations to keep the artifacts without a need of migration
271 #file_store.backend.type = filesystem_v2
272
273 ; filesystem options...
274 #file_store.filesystem_v1.storage_path = /var/opt/rhodecode_data/artifacts_file_store
275
276 ; filesystem_v2 options...
277 #file_store.filesystem_v2.storage_path = /var/opt/rhodecode_data/artifacts_file_store
278 #file_store.filesystem_v2.shards = 8
260 279
280 ; objectstore options...
281 ; url for s3 compatible storage that allows to upload artifacts
282 ; e.g http://minio:9000
283 #file_store.backend.type = objectstore
284 #file_store.objectstore.url = http://s3-minio:9000
285
286 ; a top-level bucket to put all other shards in
287 ; objects will be stored in rhodecode-file-store/shard-N based on the bucket_shards number
288 #file_store.objectstore.bucket = rhodecode-file-store
289
290 ; number of sharded buckets to create to distribute archives across
291 ; default is 8 shards
292 #file_store.objectstore.bucket_shards = 8
293
294 ; key for s3 auth
295 #file_store.objectstore.key = s3admin
296
297 ; secret for s3 auth
298 #file_store.objectstore.secret = s3secret4
299
300 ;region for s3 storage
301 #file_store.objectstore.region = eu-central-1
261 302
262 303 ; Redis url to acquire/check generation of archives locks
263 304 archive_cache.locking.url = redis://redis:6379/1
264 305
265 306 ; Storage backend, only 'filesystem' and 'objectstore' are available now
266 307 archive_cache.backend.type = filesystem
267 308
268 309 ; url for s3 compatible storage that allows to upload artifacts
269 310 ; e.g http://minio:9000
270 311 archive_cache.objectstore.url = http://s3-minio:9000
271 312
272 313 ; key for s3 auth
273 314 archive_cache.objectstore.key = key
274 315
275 316 ; secret for s3 auth
276 317 archive_cache.objectstore.secret = secret
277 318
278 319 ;region for s3 storage
279 320 archive_cache.objectstore.region = eu-central-1
280 321
281 322 ; number of sharded buckets to create to distribute archives across
282 323 ; default is 8 shards
283 324 archive_cache.objectstore.bucket_shards = 8
284 325
285 326 ; a top-level bucket to put all other shards in
286 327 ; objects will be stored in rhodecode-archive-cache/shard-N based on the bucket_shards number
287 328 archive_cache.objectstore.bucket = rhodecode-archive-cache
288 329
289 330 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
290 331 archive_cache.objectstore.retry = false
291 332
292 333 ; number of seconds to wait for next try using retry
293 334 archive_cache.objectstore.retry_backoff = 1
294 335
295 336 ; how many tries do do a retry fetch from this backend
296 337 archive_cache.objectstore.retry_attempts = 10
297 338
298 339 ; Default is $cache_dir/archive_cache if not set
299 340 ; Generated repo archives will be cached at this location
300 341 ; and served from the cache during subsequent requests for the same archive of
301 342 ; the repository. This path is important to be shared across filesystems and with
302 343 ; RhodeCode and vcsserver
303 344 archive_cache.filesystem.store_dir = /var/opt/rhodecode_data/archive_cache
304 345
305 346 ; The limit in GB sets how much data we cache before recycling last used, defaults to 10 gb
306 347 archive_cache.filesystem.cache_size_gb = 40
307 348
308 349 ; Eviction policy used to clear out after cache_size_gb limit is reached
309 350 archive_cache.filesystem.eviction_policy = least-recently-stored
310 351
311 352 ; By default cache uses sharding technique, this specifies how many shards are there
312 353 ; default is 8 shards
313 354 archive_cache.filesystem.cache_shards = 8
314 355
315 356 ; if true, this cache will try to retry with retry_attempts=N times waiting retry_backoff time
316 357 archive_cache.filesystem.retry = false
317 358
318 359 ; number of seconds to wait for next try using retry
319 360 archive_cache.filesystem.retry_backoff = 1
320 361
321 362 ; how many tries do do a retry fetch from this backend
322 363 archive_cache.filesystem.retry_attempts = 10
323 364
324 365
325 366 ; #############
326 367 ; CELERY CONFIG
327 368 ; #############
328 369
329 370 ; manually run celery: /path/to/celery worker --task-events --beat --app rhodecode.lib.celerylib.loader --scheduler rhodecode.lib.celerylib.scheduler.RcScheduler --loglevel DEBUG --ini /path/to/rhodecode.ini
330 371
331 372 use_celery = true
332 373
333 374 ; path to store schedule database
334 375 #celerybeat-schedule.path =
335 376
336 377 ; connection url to the message broker (default redis)
337 378 celery.broker_url = redis://redis:6379/8
338 379
339 380 ; results backend to get results for (default redis)
340 381 celery.result_backend = redis://redis:6379/8
341 382
342 383 ; rabbitmq example
343 384 #celery.broker_url = amqp://rabbitmq:qweqwe@localhost:5672/rabbitmqhost
344 385
345 386 ; maximum tasks to execute before worker restart
346 387 celery.max_tasks_per_child = 20
347 388
348 389 ; tasks will never be sent to the queue, but executed locally instead.
349 390 celery.task_always_eager = false
350 391
351 392 ; #############
352 393 ; DOGPILE CACHE
353 394 ; #############
354 395
355 396 ; Default cache dir for caches. Putting this into a ramdisk can boost performance.
356 397 ; eg. /tmpfs/data_ramdisk, however this directory might require large amount of space
357 398 cache_dir = /var/opt/rhodecode_data
358 399
359 400 ; *********************************************
360 401 ; `sql_cache_short` cache for heavy SQL queries
361 402 ; Only supported backend is `memory_lru`
362 403 ; *********************************************
363 404 rc_cache.sql_cache_short.backend = dogpile.cache.rc.memory_lru
364 405 rc_cache.sql_cache_short.expiration_time = 30
365 406
366 407
367 408 ; *****************************************************
368 409 ; `cache_repo_longterm` cache for repo object instances
369 410 ; Only supported backend is `memory_lru`
370 411 ; *****************************************************
371 412 rc_cache.cache_repo_longterm.backend = dogpile.cache.rc.memory_lru
372 413 ; by default we use 30 Days, cache is still invalidated on push
373 414 rc_cache.cache_repo_longterm.expiration_time = 2592000
374 415 ; max items in LRU cache, set to smaller number to save memory, and expire last used caches
375 416 rc_cache.cache_repo_longterm.max_size = 10000
376 417
377 418
378 419 ; *********************************************
379 420 ; `cache_general` cache for general purpose use
380 421 ; for simplicity use rc.file_namespace backend,
381 422 ; for performance and scale use rc.redis
382 423 ; *********************************************
383 424 rc_cache.cache_general.backend = dogpile.cache.rc.file_namespace
384 425 rc_cache.cache_general.expiration_time = 43200
385 426 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
386 427 #rc_cache.cache_general.arguments.filename = /tmp/cache_general_db
387 428
388 429 ; alternative `cache_general` redis backend with distributed lock
389 430 #rc_cache.cache_general.backend = dogpile.cache.rc.redis
390 431 #rc_cache.cache_general.expiration_time = 300
391 432
392 433 ; redis_expiration_time needs to be greater then expiration_time
393 434 #rc_cache.cache_general.arguments.redis_expiration_time = 7200
394 435
395 436 #rc_cache.cache_general.arguments.host = localhost
396 437 #rc_cache.cache_general.arguments.port = 6379
397 438 #rc_cache.cache_general.arguments.db = 0
398 439 #rc_cache.cache_general.arguments.socket_timeout = 30
399 440 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
400 441 #rc_cache.cache_general.arguments.distributed_lock = true
401 442
402 443 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
403 444 #rc_cache.cache_general.arguments.lock_auto_renewal = true
404 445
405 446 ; *************************************************
406 447 ; `cache_perms` cache for permission tree, auth TTL
407 448 ; for simplicity use rc.file_namespace backend,
408 449 ; for performance and scale use rc.redis
409 450 ; *************************************************
410 451 rc_cache.cache_perms.backend = dogpile.cache.rc.file_namespace
411 452 rc_cache.cache_perms.expiration_time = 3600
412 453 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
413 454 #rc_cache.cache_perms.arguments.filename = /tmp/cache_perms_db
414 455
415 456 ; alternative `cache_perms` redis backend with distributed lock
416 457 #rc_cache.cache_perms.backend = dogpile.cache.rc.redis
417 458 #rc_cache.cache_perms.expiration_time = 300
418 459
419 460 ; redis_expiration_time needs to be greater then expiration_time
420 461 #rc_cache.cache_perms.arguments.redis_expiration_time = 7200
421 462
422 463 #rc_cache.cache_perms.arguments.host = localhost
423 464 #rc_cache.cache_perms.arguments.port = 6379
424 465 #rc_cache.cache_perms.arguments.db = 0
425 466 #rc_cache.cache_perms.arguments.socket_timeout = 30
426 467 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
427 468 #rc_cache.cache_perms.arguments.distributed_lock = true
428 469
429 470 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
430 471 #rc_cache.cache_perms.arguments.lock_auto_renewal = true
431 472
432 473 ; ***************************************************
433 474 ; `cache_repo` cache for file tree, Readme, RSS FEEDS
434 475 ; for simplicity use rc.file_namespace backend,
435 476 ; for performance and scale use rc.redis
436 477 ; ***************************************************
437 478 rc_cache.cache_repo.backend = dogpile.cache.rc.file_namespace
438 479 rc_cache.cache_repo.expiration_time = 2592000
439 480 ; file cache store path. Defaults to `cache_dir =` value or tempdir if both values are not set
440 481 #rc_cache.cache_repo.arguments.filename = /tmp/cache_repo_db
441 482
442 483 ; alternative `cache_repo` redis backend with distributed lock
443 484 #rc_cache.cache_repo.backend = dogpile.cache.rc.redis
444 485 #rc_cache.cache_repo.expiration_time = 2592000
445 486
446 487 ; redis_expiration_time needs to be greater then expiration_time
447 488 #rc_cache.cache_repo.arguments.redis_expiration_time = 2678400
448 489
449 490 #rc_cache.cache_repo.arguments.host = localhost
450 491 #rc_cache.cache_repo.arguments.port = 6379
451 492 #rc_cache.cache_repo.arguments.db = 1
452 493 #rc_cache.cache_repo.arguments.socket_timeout = 30
453 494 ; more Redis options: https://dogpilecache.sqlalchemy.org/en/latest/api.html#redis-backends
454 495 #rc_cache.cache_repo.arguments.distributed_lock = true
455 496
456 497 ; auto-renew lock to prevent stale locks, slower but safer. Use only if problems happen
457 498 #rc_cache.cache_repo.arguments.lock_auto_renewal = true
458 499
459 500 ; ##############
460 501 ; BEAKER SESSION
461 502 ; ##############
462 503
463 504 ; beaker.session.type is type of storage options for the logged users sessions. Current allowed
464 505 ; types are file, ext:redis, ext:database, ext:memcached
465 506 ; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session
466 507 #beaker.session.type = file
467 508 #beaker.session.data_dir = %(here)s/data/sessions
468 509
469 510 ; Redis based sessions
470 511 beaker.session.type = ext:redis
471 512 beaker.session.url = redis://redis:6379/2
472 513
473 514 ; DB based session, fast, and allows easy management over logged in users
474 515 #beaker.session.type = ext:database
475 516 #beaker.session.table_name = db_session
476 517 #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode
477 518 #beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode
478 519 #beaker.session.sa.pool_recycle = 3600
479 520 #beaker.session.sa.echo = false
480 521
481 522 beaker.session.key = rhodecode
482 523 beaker.session.secret = production-rc-uytcxaz
483 524 beaker.session.lock_dir = /data_ramdisk/lock
484 525
485 526 ; Secure encrypted cookie. Requires AES and AES python libraries
486 527 ; you must disable beaker.session.secret to use this
487 528 #beaker.session.encrypt_key = key_for_encryption
488 529 #beaker.session.validate_key = validation_key
489 530
490 531 ; Sets session as invalid (also logging out user) if it haven not been
491 532 ; accessed for given amount of time in seconds
492 533 beaker.session.timeout = 2592000
493 534 beaker.session.httponly = true
494 535
495 536 ; Path to use for the cookie. Set to prefix if you use prefix middleware
496 537 #beaker.session.cookie_path = /custom_prefix
497 538
498 539 ; Set https secure cookie
499 540 beaker.session.secure = false
500 541
501 542 ; default cookie expiration time in seconds, set to `true` to set expire
502 543 ; at browser close
503 544 #beaker.session.cookie_expires = 3600
504 545
505 546 ; #############################
506 547 ; SEARCH INDEXING CONFIGURATION
507 548 ; #############################
508 549
509 550 ; Full text search indexer is available in rhodecode-tools under
510 551 ; `rhodecode-tools index` command
511 552
512 553 ; WHOOSH Backend, doesn't require additional services to run
513 554 ; it works good with few dozen repos
514 555 search.module = rhodecode.lib.index.whoosh
515 556 search.location = %(here)s/data/index
516 557
517 558 ; ####################
518 559 ; CHANNELSTREAM CONFIG
519 560 ; ####################
520 561
521 562 ; channelstream enables persistent connections and live notification
522 563 ; in the system. It's also used by the chat system
523 564
524 565 channelstream.enabled = true
525 566
526 567 ; server address for channelstream server on the backend
527 568 channelstream.server = channelstream:9800
528 569
529 570 ; location of the channelstream server from outside world
530 571 ; use ws:// for http or wss:// for https. This address needs to be handled
531 572 ; by external HTTP server such as Nginx or Apache
532 573 ; see Nginx/Apache configuration examples in our docs
533 574 channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
534 575 channelstream.secret = ENV_GENERATED
535 576 channelstream.history.location = /var/opt/rhodecode_data/channelstream_history
536 577
537 578 ; Internal application path that Javascript uses to connect into.
538 579 ; If you use proxy-prefix the prefix should be added before /_channelstream
539 580 channelstream.proxy_path = /_channelstream
540 581
541 582
542 583 ; ##############################
543 584 ; MAIN RHODECODE DATABASE CONFIG
544 585 ; ##############################
545 586
546 587 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30
547 588 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
548 589 #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode?charset=utf8
549 590 ; pymysql is an alternative driver for MySQL, use in case of problems with default one
550 591 #sqlalchemy.db1.url = mysql+pymysql://root:qweqwe@localhost/rhodecode
551 592
552 593 sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode
553 594
554 595 ; see sqlalchemy docs for other advanced settings
555 596 ; print the sql statements to output
556 597 sqlalchemy.db1.echo = false
557 598
558 599 ; recycle the connections after this amount of seconds
559 600 sqlalchemy.db1.pool_recycle = 3600
560 601
561 602 ; the number of connections to keep open inside the connection pool.
562 603 ; 0 indicates no limit
563 604 ; the general calculus with gevent is:
564 605 ; if your system allows 500 concurrent greenlets (max_connections) that all do database access,
565 606 ; then increase pool size + max overflow so that they add up to 500.
566 607 #sqlalchemy.db1.pool_size = 5
567 608
568 609 ; The number of connections to allow in connection pool "overflow", that is
569 610 ; connections that can be opened above and beyond the pool_size setting,
570 611 ; which defaults to five.
571 612 #sqlalchemy.db1.max_overflow = 10
572 613
573 614 ; Connection check ping, used to detect broken database connections
574 615 ; could be enabled to better handle cases if MySQL has gone away errors
575 616 #sqlalchemy.db1.ping_connection = true
576 617
577 618 ; ##########
578 619 ; VCS CONFIG
579 620 ; ##########
580 621 vcs.server.enable = true
581 622 vcs.server = vcsserver:10010
582 623
583 624 ; Web server connectivity protocol, responsible for web based VCS operations
584 625 ; Available protocols are:
585 626 ; `http` - use http-rpc backend (default)
586 627 vcs.server.protocol = http
587 628
588 629 ; Push/Pull operations protocol, available options are:
589 630 ; `http` - use http-rpc backend (default)
590 631 vcs.scm_app_implementation = http
591 632
592 633 ; Push/Pull operations hooks protocol, available options are:
593 634 ; `http` - use http-rpc backend (default)
594 635 ; `celery` - use celery based hooks
595 636 #DEPRECATED:vcs.hooks.protocol = http
596 637 vcs.hooks.protocol.v2 = celery
597 638
598 639 ; Host on which this instance is listening for hooks. vcsserver will call this host to pull/push hooks so it should be
599 640 ; accessible via network.
600 641 ; Use vcs.hooks.host = "*" to bind to current hostname (for Docker)
601 642 vcs.hooks.host = *
602 643
603 644 ; Start VCSServer with this instance as a subprocess, useful for development
604 645 vcs.start_server = false
605 646
606 647 ; List of enabled VCS backends, available options are:
607 648 ; `hg` - mercurial
608 649 ; `git` - git
609 650 ; `svn` - subversion
610 651 vcs.backends = hg, git, svn
611 652
612 653 ; Wait this number of seconds before killing connection to the vcsserver
613 654 vcs.connection_timeout = 3600
614 655
615 656 ; Cache flag to cache vcsserver remote calls locally
616 657 ; It uses cache_region `cache_repo`
617 658 vcs.methods.cache = true
618 659
619 660 ; ####################################################
620 661 ; Subversion proxy support (mod_dav_svn)
621 662 ; Maps RhodeCode repo groups into SVN paths for Apache
622 663 ; ####################################################
623 664
624 665 ; Compatibility version when creating SVN repositories. Defaults to newest version when commented out.
625 666 ; Set a numeric version for your current SVN e.g 1.8, or 1.12
626 667 ; Legacy available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible, pre-1.9-compatible
627 668 #vcs.svn.compatible_version = 1.8
628 669
629 670 ; Redis connection settings for svn integrations logic
630 671 ; This connection string needs to be the same on ce and vcsserver
631 672 vcs.svn.redis_conn = redis://redis:6379/0
632 673
633 674 ; Enable SVN proxy of requests over HTTP
634 675 vcs.svn.proxy.enabled = true
635 676
636 677 ; host to connect to running SVN subsystem
637 678 vcs.svn.proxy.host = http://svn:8090
638 679
639 680 ; Enable or disable the config file generation.
640 681 svn.proxy.generate_config = true
641 682
642 683 ; Generate config file with `SVNListParentPath` set to `On`.
643 684 svn.proxy.list_parent_path = true
644 685
645 686 ; Set location and file name of generated config file.
646 687 svn.proxy.config_file_path = /etc/rhodecode/conf/svn/mod_dav_svn.conf
647 688
648 689 ; alternative mod_dav config template. This needs to be a valid mako template
649 690 ; Example template can be found in the source code:
650 691 ; rhodecode/apps/svn_support/templates/mod-dav-svn.conf.mako
651 692 #svn.proxy.config_template = ~/.rccontrol/enterprise-1/custom_svn_conf.mako
652 693
653 694 ; Used as a prefix to the `Location` block in the generated config file.
654 695 ; In most cases it should be set to `/`.
655 696 svn.proxy.location_root = /
656 697
657 698 ; Command to reload the mod dav svn configuration on change.
658 699 ; Example: `/etc/init.d/apache2 reload` or /home/USER/apache_reload.sh
659 700 ; Make sure user who runs RhodeCode process is allowed to reload Apache
660 701 #svn.proxy.reload_cmd = /etc/init.d/apache2 reload
661 702
662 703 ; If the timeout expires before the reload command finishes, the command will
663 704 ; be killed. Setting it to zero means no timeout. Defaults to 10 seconds.
664 705 #svn.proxy.reload_timeout = 10
665 706
666 707 ; ####################
667 708 ; SSH Support Settings
668 709 ; ####################
669 710
670 711 ; Defines if a custom authorized_keys file should be created and written on
671 712 ; any change user ssh keys. Setting this to false also disables possibility
672 713 ; of adding SSH keys by users from web interface. Super admins can still
673 714 ; manage SSH Keys.
674 715 ssh.generate_authorized_keyfile = true
675 716
676 717 ; Options for ssh, default is `no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding`
677 718 # ssh.authorized_keys_ssh_opts =
678 719
679 720 ; Path to the authorized_keys file where the generate entries are placed.
680 721 ; It is possible to have multiple key files specified in `sshd_config` e.g.
681 722 ; AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys_rhodecode
682 723 ssh.authorized_keys_file_path = /etc/rhodecode/conf/ssh/authorized_keys_rhodecode
683 724
684 725 ; Command to execute the SSH wrapper. The binary is available in the
685 726 ; RhodeCode installation directory.
686 727 ; legacy: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
687 728 ; new rewrite: /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
688 729 #DEPRECATED: ssh.wrapper_cmd = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper
689 730 ssh.wrapper_cmd.v2 = /usr/local/bin/rhodecode_bin/bin/rc-ssh-wrapper-v2
690 731
691 732 ; Allow shell when executing the ssh-wrapper command
692 733 ssh.wrapper_cmd_allow_shell = false
693 734
694 735 ; Enables logging, and detailed output send back to the client during SSH
695 736 ; operations. Useful for debugging, shouldn't be used in production.
696 737 ssh.enable_debug_logging = false
697 738
698 739 ; Paths to binary executable, by default they are the names, but we can
699 740 ; override them if we want to use a custom one
700 741 ssh.executable.hg = /usr/local/bin/rhodecode_bin/vcs_bin/hg
701 742 ssh.executable.git = /usr/local/bin/rhodecode_bin/vcs_bin/git
702 743 ssh.executable.svn = /usr/local/bin/rhodecode_bin/vcs_bin/svnserve
703 744
704 745 ; Enables SSH key generator web interface. Disabling this still allows users
705 746 ; to add their own keys.
706 747 ssh.enable_ui_key_generator = true
707 748
708 749 ; Statsd client config, this is used to send metrics to statsd
709 750 ; We recommend setting statsd_exported and scrape them using Prometheus
710 751 #statsd.enabled = false
711 752 #statsd.statsd_host = 0.0.0.0
712 753 #statsd.statsd_port = 8125
713 754 #statsd.statsd_prefix =
714 755 #statsd.statsd_ipv6 = false
715 756
716 757 ; configure logging automatically at server startup set to false
717 758 ; to use the below custom logging config.
718 759 ; RC_LOGGING_FORMATTER
719 760 ; RC_LOGGING_LEVEL
720 761 ; env variables can control the settings for logging in case of autoconfigure
721 762
722 763 #logging.autoconfigure = true
723 764
724 765 ; specify your own custom logging config file to configure logging
725 766 #logging.logging_conf_file = /path/to/custom_logging.ini
726 767
727 768 ; Dummy marker to add new entries after.
728 769 ; Add any custom entries below. Please don't remove this marker.
729 770 custom.conf = 1
730 771
731 772
732 773 ; #####################
733 774 ; LOGGING CONFIGURATION
734 775 ; #####################
735 776
736 777 [loggers]
737 778 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
738 779
739 780 [handlers]
740 781 keys = console, console_sql
741 782
742 783 [formatters]
743 784 keys = generic, json, color_formatter, color_formatter_sql
744 785
745 786 ; #######
746 787 ; LOGGERS
747 788 ; #######
748 789 [logger_root]
749 790 level = NOTSET
750 791 handlers = console
751 792
752 793 [logger_sqlalchemy]
753 794 level = INFO
754 795 handlers = console_sql
755 796 qualname = sqlalchemy.engine
756 797 propagate = 0
757 798
758 799 [logger_beaker]
759 800 level = DEBUG
760 801 handlers =
761 802 qualname = beaker.container
762 803 propagate = 1
763 804
764 805 [logger_rhodecode]
765 806 level = DEBUG
766 807 handlers =
767 808 qualname = rhodecode
768 809 propagate = 1
769 810
770 811 [logger_ssh_wrapper]
771 812 level = DEBUG
772 813 handlers =
773 814 qualname = ssh_wrapper
774 815 propagate = 1
775 816
776 817 [logger_celery]
777 818 level = DEBUG
778 819 handlers =
779 820 qualname = celery
780 821
781 822
782 823 ; ########
783 824 ; HANDLERS
784 825 ; ########
785 826
786 827 [handler_console]
787 828 class = StreamHandler
788 829 args = (sys.stderr, )
789 830 level = INFO
790 831 ; To enable JSON formatted logs replace 'generic/color_formatter' with 'json'
791 832 ; This allows sending properly formatted logs to grafana loki or elasticsearch
792 833 formatter = generic
793 834
794 835 [handler_console_sql]
795 836 ; "level = DEBUG" logs SQL queries and results.
796 837 ; "level = INFO" logs SQL queries.
797 838 ; "level = WARN" logs neither. (Recommended for production systems.)
798 839 class = StreamHandler
799 840 args = (sys.stderr, )
800 841 level = WARN
801 842 ; To enable JSON formatted logs replace 'generic/color_formatter_sql' with 'json'
802 843 ; This allows sending properly formatted logs to grafana loki or elasticsearch
803 844 formatter = generic
804 845
805 846 ; ##########
806 847 ; FORMATTERS
807 848 ; ##########
808 849
809 850 [formatter_generic]
810 851 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
811 852 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
812 853 datefmt = %Y-%m-%d %H:%M:%S
813 854
814 855 [formatter_color_formatter]
815 856 class = rhodecode.lib.logging_formatter.ColorFormatter
816 857 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
817 858 datefmt = %Y-%m-%d %H:%M:%S
818 859
819 860 [formatter_color_formatter_sql]
820 861 class = rhodecode.lib.logging_formatter.ColorFormatterSql
821 862 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
822 863 datefmt = %Y-%m-%d %H:%M:%S
823 864
824 865 [formatter_json]
825 866 format = %(timestamp)s %(levelname)s %(name)s %(message)s %(req_id)s
826 867 class = rhodecode.lib._vendor.jsonlogger.JsonFormatter
@@ -1,423 +1,423 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import itertools
21 21 import base64
22 22
23 23 from rhodecode.api import (
24 24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
25 25
26 26 from rhodecode.api.utils import (
27 27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 28 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_repo_store_path
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib import user_sessions
31 31 from rhodecode.lib import exc_tracking
32 32 from rhodecode.lib.ext_json import json
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.model.db import UserIpMap
35 35 from rhodecode.model.scm import ScmModel
36 from rhodecode.apps.file_store import utils
36 from rhodecode.apps.file_store import utils as store_utils
37 37 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
38 38 FileOverSizeException
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 @jsonrpc_method()
44 44 def get_server_info(request, apiuser):
45 45 """
46 46 Returns the |RCE| server information.
47 47
48 48 This includes the running version of |RCE| and all installed
49 49 packages. This command takes the following options:
50 50
51 51 :param apiuser: This is filled automatically from the |authtoken|.
52 52 :type apiuser: AuthUser
53 53
54 54 Example output:
55 55
56 56 .. code-block:: bash
57 57
58 58 id : <id_given_in_input>
59 59 result : {
60 60 'modules': [<module name>,...]
61 61 'py_version': <python version>,
62 62 'platform': <platform type>,
63 63 'rhodecode_version': <rhodecode version>
64 64 }
65 65 error : null
66 66 """
67 67
68 68 if not has_superadmin_permission(apiuser):
69 69 raise JSONRPCForbidden()
70 70
71 71 server_info = ScmModel().get_server_info(request.environ)
72 72 # rhodecode-index requires those
73 73
74 74 server_info['index_storage'] = server_info['search']['value']['location']
75 75 server_info['storage'] = server_info['storage']['value']['path']
76 76
77 77 return server_info
78 78
79 79
80 80 @jsonrpc_method()
81 81 def get_repo_store(request, apiuser):
82 82 """
83 83 Returns the |RCE| repository storage information.
84 84
85 85 :param apiuser: This is filled automatically from the |authtoken|.
86 86 :type apiuser: AuthUser
87 87
88 88 Example output:
89 89
90 90 .. code-block:: bash
91 91
92 92 id : <id_given_in_input>
93 93 result : {
94 94 'modules': [<module name>,...]
95 95 'py_version': <python version>,
96 96 'platform': <platform type>,
97 97 'rhodecode_version': <rhodecode version>
98 98 }
99 99 error : null
100 100 """
101 101
102 102 if not has_superadmin_permission(apiuser):
103 103 raise JSONRPCForbidden()
104 104
105 105 path = get_rhodecode_repo_store_path()
106 106 return {"path": path}
107 107
108 108
109 109 @jsonrpc_method()
110 110 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
111 111 """
112 112 Displays the IP Address as seen from the |RCE| server.
113 113
114 114 * This command displays the IP Address, as well as all the defined IP
115 115 addresses for the specified user. If the ``userid`` is not set, the
116 116 data returned is for the user calling the method.
117 117
118 118 This command can only be run using an |authtoken| with admin rights to
119 119 the specified repository.
120 120
121 121 This command takes the following options:
122 122
123 123 :param apiuser: This is filled automatically from |authtoken|.
124 124 :type apiuser: AuthUser
125 125 :param userid: Sets the userid for which associated IP Address data
126 126 is returned.
127 127 :type userid: Optional(str or int)
128 128
129 129 Example output:
130 130
131 131 .. code-block:: bash
132 132
133 133 id : <id_given_in_input>
134 134 result : {
135 135 "server_ip_addr": "<ip_from_clien>",
136 136 "user_ips": [
137 137 {
138 138 "ip_addr": "<ip_with_mask>",
139 139 "ip_range": ["<start_ip>", "<end_ip>"],
140 140 },
141 141 ...
142 142 ]
143 143 }
144 144
145 145 """
146 146 if not has_superadmin_permission(apiuser):
147 147 raise JSONRPCForbidden()
148 148
149 149 userid = Optional.extract(userid, evaluate_locals=locals())
150 150 userid = getattr(userid, 'user_id', userid)
151 151
152 152 user = get_user_or_error(userid)
153 153 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
154 154 return {
155 155 'server_ip_addr': request.rpc_ip_addr,
156 156 'user_ips': ips
157 157 }
158 158
159 159
160 160 @jsonrpc_method()
161 161 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
162 162 """
163 163 Triggers a rescan of the specified repositories.
164 164
165 165 * If the ``remove_obsolete`` option is set, it also deletes repositories
166 166 that are found in the database but not on the file system, so called
167 167 "clean zombies".
168 168
169 169 This command can only be run using an |authtoken| with admin rights to
170 170 the specified repository.
171 171
172 172 This command takes the following options:
173 173
174 174 :param apiuser: This is filled automatically from the |authtoken|.
175 175 :type apiuser: AuthUser
176 176 :param remove_obsolete: Deletes repositories from the database that
177 177 are not found on the filesystem.
178 178 :type remove_obsolete: Optional(``True`` | ``False``)
179 179
180 180 Example output:
181 181
182 182 .. code-block:: bash
183 183
184 184 id : <id_given_in_input>
185 185 result : {
186 186 'added': [<added repository name>,...]
187 187 'removed': [<removed repository name>,...]
188 188 }
189 189 error : null
190 190
191 191 Example error output:
192 192
193 193 .. code-block:: bash
194 194
195 195 id : <id_given_in_input>
196 196 result : null
197 197 error : {
198 198 'Error occurred during rescan repositories action'
199 199 }
200 200
201 201 """
202 202 if not has_superadmin_permission(apiuser):
203 203 raise JSONRPCForbidden()
204 204
205 205 try:
206 206 rm_obsolete = Optional.extract(remove_obsolete)
207 207 added, removed = repo2db_mapper(ScmModel().repo_scan(),
208 208 remove_obsolete=rm_obsolete, force_hooks_rebuild=True)
209 209 return {'added': added, 'removed': removed}
210 210 except Exception:
211 211 log.exception('Failed to run repo rescann')
212 212 raise JSONRPCError(
213 213 'Error occurred during rescan repositories action'
214 214 )
215 215
216 216
217 217 @jsonrpc_method()
218 218 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
219 219 """
220 220 Triggers a session cleanup action.
221 221
222 222 If the ``older_then`` option is set, only sessions that hasn't been
223 223 accessed in the given number of days will be removed.
224 224
225 225 This command can only be run using an |authtoken| with admin rights to
226 226 the specified repository.
227 227
228 228 This command takes the following options:
229 229
230 230 :param apiuser: This is filled automatically from the |authtoken|.
231 231 :type apiuser: AuthUser
232 232 :param older_then: Deletes session that hasn't been accessed
233 233 in given number of days.
234 234 :type older_then: Optional(int)
235 235
236 236 Example output:
237 237
238 238 .. code-block:: bash
239 239
240 240 id : <id_given_in_input>
241 241 result: {
242 242 "backend": "<type of backend>",
243 243 "sessions_removed": <number_of_removed_sessions>
244 244 }
245 245 error : null
246 246
247 247 Example error output:
248 248
249 249 .. code-block:: bash
250 250
251 251 id : <id_given_in_input>
252 252 result : null
253 253 error : {
254 254 'Error occurred during session cleanup'
255 255 }
256 256
257 257 """
258 258 if not has_superadmin_permission(apiuser):
259 259 raise JSONRPCForbidden()
260 260
261 261 older_then = safe_int(Optional.extract(older_then)) or 60
262 262 older_than_seconds = 60 * 60 * 24 * older_then
263 263
264 264 config = system_info.rhodecode_config().get_value()['value']['config']
265 265 session_model = user_sessions.get_session_handler(
266 266 config.get('beaker.session.type', 'memory'))(config)
267 267
268 268 backend = session_model.SESSION_TYPE
269 269 try:
270 270 cleaned = session_model.clean_sessions(
271 271 older_than_seconds=older_than_seconds)
272 272 return {'sessions_removed': cleaned, 'backend': backend}
273 273 except user_sessions.CleanupCommand as msg:
274 274 return {'cleanup_command': str(msg), 'backend': backend}
275 275 except Exception as e:
276 276 log.exception('Failed session cleanup')
277 277 raise JSONRPCError(
278 278 'Error occurred during session cleanup'
279 279 )
280 280
281 281
282 282 @jsonrpc_method()
283 283 def get_method(request, apiuser, pattern=Optional('*')):
284 284 """
285 285 Returns list of all available API methods. By default match pattern
286 286 os "*" but any other pattern can be specified. eg *comment* will return
287 287 all methods with comment inside them. If just single method is matched
288 288 returned data will also include method specification
289 289
290 290 This command can only be run using an |authtoken| with admin rights to
291 291 the specified repository.
292 292
293 293 This command takes the following options:
294 294
295 295 :param apiuser: This is filled automatically from the |authtoken|.
296 296 :type apiuser: AuthUser
297 297 :param pattern: pattern to match method names against
298 298 :type pattern: Optional("*")
299 299
300 300 Example output:
301 301
302 302 .. code-block:: bash
303 303
304 304 id : <id_given_in_input>
305 305 "result": [
306 306 "changeset_comment",
307 307 "comment_pull_request",
308 308 "comment_commit"
309 309 ]
310 310 error : null
311 311
312 312 .. code-block:: bash
313 313
314 314 id : <id_given_in_input>
315 315 "result": [
316 316 "comment_commit",
317 317 {
318 318 "apiuser": "<RequiredType>",
319 319 "comment_type": "<Optional:u'note'>",
320 320 "commit_id": "<RequiredType>",
321 321 "message": "<RequiredType>",
322 322 "repoid": "<RequiredType>",
323 323 "request": "<RequiredType>",
324 324 "resolves_comment_id": "<Optional:None>",
325 325 "status": "<Optional:None>",
326 326 "userid": "<Optional:<OptionalAttr:apiuser>>"
327 327 }
328 328 ]
329 329 error : null
330 330 """
331 331 from rhodecode.config import patches
332 332 inspect = patches.inspect_getargspec()
333 333
334 334 if not has_superadmin_permission(apiuser):
335 335 raise JSONRPCForbidden()
336 336
337 337 pattern = Optional.extract(pattern)
338 338
339 339 matches = find_methods(request.registry.jsonrpc_methods, pattern)
340 340
341 341 args_desc = []
342 342 matches_keys = list(matches.keys())
343 343 if len(matches_keys) == 1:
344 344 func = matches[matches_keys[0]]
345 345
346 346 argspec = inspect.getargspec(func)
347 347 arglist = argspec[0]
348 348 defaults = list(map(repr, argspec[3] or []))
349 349
350 350 default_empty = '<RequiredType>'
351 351
352 352 # kw arguments required by this method
353 353 func_kwargs = dict(itertools.zip_longest(
354 354 reversed(arglist), reversed(defaults), fillvalue=default_empty))
355 355 args_desc.append(func_kwargs)
356 356
357 357 return matches_keys + args_desc
358 358
359 359
360 360 @jsonrpc_method()
361 361 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
362 362 """
363 363 Stores sent exception inside the built-in exception tracker in |RCE| server.
364 364
365 365 This command can only be run using an |authtoken| with admin rights to
366 366 the specified repository.
367 367
368 368 This command takes the following options:
369 369
370 370 :param apiuser: This is filled automatically from the |authtoken|.
371 371 :type apiuser: AuthUser
372 372
373 373 :param exc_data_json: JSON data with exception e.g
374 374 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
375 375 :type exc_data_json: JSON data
376 376
377 377 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
378 378 :type prefix: Optional("rhodecode")
379 379
380 380 Example output:
381 381
382 382 .. code-block:: bash
383 383
384 384 id : <id_given_in_input>
385 385 "result": {
386 386 "exc_id": 139718459226384,
387 387 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
388 388 }
389 389 error : null
390 390 """
391 391 if not has_superadmin_permission(apiuser):
392 392 raise JSONRPCForbidden()
393 393
394 394 prefix = Optional.extract(prefix)
395 395 exc_id = exc_tracking.generate_id()
396 396
397 397 try:
398 398 exc_data = json.loads(exc_data_json)
399 399 except Exception:
400 400 log.error('Failed to parse JSON: %r', exc_data_json)
401 401 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
402 402 'Please make sure it contains a valid JSON.')
403 403
404 404 try:
405 405 exc_traceback = exc_data['exc_traceback']
406 406 exc_type_name = exc_data['exc_type_name']
407 407 exc_value = ''
408 408 except KeyError as err:
409 409 raise JSONRPCError(
410 410 f'Missing exc_traceback, or exc_type_name '
411 411 f'in exc_data_json field. Missing: {err}')
412 412
413 413 class ExcType:
414 414 __name__ = exc_type_name
415 415
416 416 exc_info = (ExcType(), exc_value, exc_traceback)
417 417
418 418 exc_tracking._store_exception(
419 419 exc_id=exc_id, exc_info=exc_info, prefix=prefix)
420 420
421 421 exc_url = request.route_url(
422 422 'admin_settings_exception_tracker_show', exception_id=exc_id)
423 423 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,243 +1,249 b''
1 1
2 2
3 3 # Copyright (C) 2016-2023 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 logging
22 22 import urllib.request
23 23 import urllib.error
24 24 import urllib.parse
25 25 import os
26 26
27 27 import rhodecode
28 28 from rhodecode.apps._base import BaseAppView
29 29 from rhodecode.apps._base.navigation import navigation_list
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.lib import system_info
34 34 from rhodecode.model.update import UpdateModel
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminSystemInfoSettingsView(BaseAppView):
40 40 def load_default_context(self):
41 41 c = self._get_local_tmpl_context()
42 42 return c
43 43
44 44 def get_env_data(self):
45 45 black_list = [
46 46 'NIX_LDFLAGS',
47 47 'NIX_CFLAGS_COMPILE',
48 48 'propagatedBuildInputs',
49 49 'propagatedNativeBuildInputs',
50 50 'postInstall',
51 51 'buildInputs',
52 52 'buildPhase',
53 53 'preShellHook',
54 54 'preShellHook',
55 55 'preCheck',
56 56 'preBuild',
57 57 'postShellHook',
58 58 'postFixup',
59 59 'postCheck',
60 60 'nativeBuildInputs',
61 61 'installPhase',
62 62 'installCheckPhase',
63 63 'checkPhase',
64 64 'configurePhase',
65 65 'shellHook'
66 66 ]
67 67 secret_list = [
68 68 'RHODECODE_USER_PASS'
69 69 ]
70 70
71 71 for k, v in sorted(os.environ.items()):
72 72 if k in black_list:
73 73 continue
74 74 if k in secret_list:
75 75 v = '*****'
76 76 yield k, v
77 77
78 78 @LoginRequired()
79 79 @HasPermissionAllDecorator('hg.admin')
80 80 def settings_system_info(self):
81 81 _ = self.request.translate
82 82 c = self.load_default_context()
83 83
84 84 c.active = 'system'
85 85 c.navlist = navigation_list(self.request)
86 86
87 87 # TODO(marcink), figure out how to allow only selected users to do this
88 88 c.allowed_to_snapshot = self._rhodecode_user.admin
89 89
90 90 snapshot = str2bool(self.request.params.get('snapshot'))
91 91
92 92 c.rhodecode_update_url = UpdateModel().get_update_url()
93 93 c.env_data = self.get_env_data()
94 94 server_info = system_info.get_system_info(self.request.environ)
95 95
96 96 for key, val in server_info.items():
97 97 setattr(c, key, val)
98 98
99 99 def val(name, subkey='human_value'):
100 100 return server_info[name][subkey]
101 101
102 102 def state(name):
103 103 return server_info[name]['state']
104 104
105 105 def val2(name):
106 106 val = server_info[name]['human_value']
107 107 state = server_info[name]['state']
108 108 return val, state
109 109
110 110 update_info_msg = _('Note: please make sure this server can '
111 111 'access `${url}` for the update link to work',
112 112 mapping=dict(url=c.rhodecode_update_url))
113 113 version = UpdateModel().get_stored_version()
114 114 is_outdated = UpdateModel().is_outdated(
115 115 rhodecode.__version__, version)
116 116 update_state = {
117 117 'type': 'warning',
118 118 'message': 'New version available: {}'.format(version)
119 119 } \
120 120 if is_outdated else {}
121 121 c.data_items = [
122 122 # update info
123 123 (_('Update info'), h.literal(
124 124 '<span class="link" id="check_for_update" >%s.</span>' % (
125 125 _('Check for updates')) +
126 126 '<br/> <span >%s.</span>' % (update_info_msg)
127 127 ), ''),
128 128
129 129 # RhodeCode specific
130 130 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
131 131 (_('Latest version'), version, update_state),
132 132 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
133 133 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
134 134 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
135 135 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
136 136 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
137 137 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
138 138 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
139 139 ('', '', ''), # spacer
140 140
141 141 # Database
142 142 (_('Database'), val('database')['url'], state('database')),
143 143 (_('Database version'), val('database')['version'], state('database')),
144 144 ('', '', ''), # spacer
145 145
146 146 # Platform/Python
147 147 (_('Platform'), val('platform')['name'], state('platform')),
148 148 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
149 149 (_('Lang'), val('locale'), state('locale')),
150 150 (_('Python version'), val('python')['version'], state('python')),
151 151 (_('Python path'), val('python')['executable'], state('python')),
152 152 ('', '', ''), # spacer
153 153
154 154 # Systems stats
155 155 (_('CPU'), val('cpu')['text'], state('cpu')),
156 156 (_('Load'), val('load')['text'], state('load')),
157 157 (_('Memory'), val('memory')['text'], state('memory')),
158 158 (_('Uptime'), val('uptime')['text'], state('uptime')),
159 159 ('', '', ''), # spacer
160 160
161 161 # ulimit
162 162 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
163 163
164 164 # Repo storage
165 165 (_('Storage location'), val('storage')['path'], state('storage')),
166 166 (_('Storage info'), val('storage')['text'], state('storage')),
167 167 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
168 168 ('', '', ''), # spacer
169 169
170 170 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
171 171 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
172 172 ('', '', ''), # spacer
173 173
174 (_('Archive cache storage type'), val('storage_archive')['type'], state('storage_archive')),
174 (_('Artifacts storage backend'), val('storage_artifacts')['type'], state('storage_artifacts')),
175 (_('Artifacts storage location'), val('storage_artifacts')['path'], state('storage_artifacts')),
176 (_('Artifacts info'), val('storage_artifacts')['text'], state('storage_artifacts')),
177 ('', '', ''), # spacer
178
179 (_('Archive cache storage backend'), val('storage_archive')['type'], state('storage_archive')),
175 180 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
176 181 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
177 182 ('', '', ''), # spacer
178 183
184
179 185 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
180 186 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
181 187 ('', '', ''), # spacer
182 188
183 189 (_('Search info'), val('search')['text'], state('search')),
184 190 (_('Search location'), val('search')['location'], state('search')),
185 191 ('', '', ''), # spacer
186 192
187 193 # VCS specific
188 194 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
189 195 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
190 196 (_('GIT'), val('git'), state('git')),
191 197 (_('HG'), val('hg'), state('hg')),
192 198 (_('SVN'), val('svn'), state('svn')),
193 199
194 200 ]
195 201
196 202 c.vcsserver_data_items = [
197 203 (k, v) for k, v in (val('vcs_server_config') or {}).items()
198 204 ]
199 205
200 206 if snapshot:
201 207 if c.allowed_to_snapshot:
202 208 c.data_items.pop(0) # remove server info
203 209 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
204 210 else:
205 211 h.flash('You are not allowed to do this', category='warning')
206 212 return self._get_template_context(c)
207 213
208 214 @LoginRequired()
209 215 @HasPermissionAllDecorator('hg.admin')
210 216 def settings_system_info_check_update(self):
211 217 _ = self.request.translate
212 218 c = self.load_default_context()
213 219
214 220 update_url = UpdateModel().get_update_url()
215 221
216 222 def _err(s):
217 223 return f'<div style="color:#ff8888; padding:4px 0px">{s}</div>'
218 224
219 225 try:
220 226 data = UpdateModel().get_update_data(update_url)
221 227 except urllib.error.URLError as e:
222 228 log.exception("Exception contacting upgrade server")
223 229 self.request.override_renderer = 'string'
224 230 return _err('Failed to contact upgrade server: %r' % e)
225 231 except ValueError as e:
226 232 log.exception("Bad data sent from update server")
227 233 self.request.override_renderer = 'string'
228 234 return _err('Bad data sent from update server')
229 235
230 236 latest = data['versions'][0]
231 237
232 238 c.update_url = update_url
233 239 c.latest_data = latest
234 240 c.latest_ver = (latest['version'] or '').strip()
235 241 c.cur_ver = self.request.GET.get('ver') or rhodecode.__version__
236 242 c.should_upgrade = False
237 243
238 244 is_outdated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
239 245 if is_outdated:
240 246 c.should_upgrade = True
241 247 c.important_notices = latest['general']
242 248 UpdateModel().store_version(latest['version'])
243 249 return self._get_template_context(c)
@@ -1,66 +1,97 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import os
19 from rhodecode.apps.file_store import config_keys
19
20
20 21 from rhodecode.config.settings_maker import SettingsMaker
21 22
22 23
23 24 def _sanitize_settings_and_apply_defaults(settings):
24 25 """
25 26 Set defaults, convert to python types and validate settings.
26 27 """
28 from rhodecode.apps.file_store import config_keys
29
30 # translate "legacy" params into new config
31 settings.pop(config_keys.deprecated_enabled, True)
32 if config_keys.deprecated_backend in settings:
33 # if legacy backend key is detected we use "legacy" backward compat setting
34 settings.pop(config_keys.deprecated_backend)
35 settings[config_keys.backend_type] = config_keys.backend_legacy_filesystem
36
37 if config_keys.deprecated_store_path in settings:
38 store_path = settings.pop(config_keys.deprecated_store_path)
39 settings[config_keys.legacy_filesystem_storage_path] = store_path
40
27 41 settings_maker = SettingsMaker(settings)
28 42
29 settings_maker.make_setting(config_keys.enabled, True, parser='bool')
30 settings_maker.make_setting(config_keys.backend, 'local')
43 default_cache_dir = settings['cache_dir']
44 default_store_dir = os.path.join(default_cache_dir, 'artifacts_filestore')
45
46 # set default backend
47 settings_maker.make_setting(config_keys.backend_type, config_keys.backend_legacy_filesystem)
48
49 # legacy filesystem defaults
50 settings_maker.make_setting(config_keys.legacy_filesystem_storage_path, default_store_dir, default_when_empty=True, )
31 51
32 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
33 settings_maker.make_setting(config_keys.store_path, default_store)
52 # filesystem defaults
53 settings_maker.make_setting(config_keys.filesystem_storage_path, default_store_dir, default_when_empty=True,)
54 settings_maker.make_setting(config_keys.filesystem_shards, 8, parser='int')
55
56 # objectstore defaults
57 settings_maker.make_setting(config_keys.objectstore_url, 'http://s3-minio:9000')
58 settings_maker.make_setting(config_keys.objectstore_bucket, 'rhodecode-artifacts-filestore')
59 settings_maker.make_setting(config_keys.objectstore_bucket_shards, 8, parser='int')
60
61 settings_maker.make_setting(config_keys.objectstore_region, '')
62 settings_maker.make_setting(config_keys.objectstore_key, '')
63 settings_maker.make_setting(config_keys.objectstore_secret, '')
34 64
35 65 settings_maker.env_expand()
36 66
37 67
38 68 def includeme(config):
69
39 70 from rhodecode.apps.file_store.views import FileStoreView
40 71
41 72 settings = config.registry.settings
42 73 _sanitize_settings_and_apply_defaults(settings)
43 74
44 75 config.add_route(
45 76 name='upload_file',
46 77 pattern='/_file_store/upload')
47 78 config.add_view(
48 79 FileStoreView,
49 80 attr='upload_file',
50 81 route_name='upload_file', request_method='POST', renderer='json_ext')
51 82
52 83 config.add_route(
53 84 name='download_file',
54 85 pattern='/_file_store/download/{fid:.*}')
55 86 config.add_view(
56 87 FileStoreView,
57 88 attr='download_file',
58 89 route_name='download_file')
59 90
60 91 config.add_route(
61 92 name='download_file_by_token',
62 93 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
63 94 config.add_view(
64 95 FileStoreView,
65 96 attr='download_file_by_token',
66 97 route_name='download_file_by_token')
@@ -1,25 +1,57 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 # Definition of setting keys used to configure this module. Defined here to
21 21 # avoid repetition of keys throughout the module.
22 22
23 enabled = 'file_store.enabled'
24 backend = 'file_store.backend'
25 store_path = 'file_store.storage_path'
23 # OLD and deprecated keys not used anymore
24 deprecated_enabled = 'file_store.enabled'
25 deprecated_backend = 'file_store.backend'
26 deprecated_store_path = 'file_store.storage_path'
27
28
29 backend_type = 'file_store.backend.type'
30
31 backend_legacy_filesystem = 'filesystem_v1'
32 backend_filesystem = 'filesystem_v2'
33 backend_objectstore = 'objectstore'
34
35 backend_types = [
36 backend_legacy_filesystem,
37 backend_filesystem,
38 backend_objectstore,
39 ]
40
41 # filesystem_v1 legacy
42 legacy_filesystem_storage_path = 'file_store.filesystem_v1.storage_path'
43
44
45 # filesystem_v2 new option
46 filesystem_storage_path = 'file_store.filesystem_v2.storage_path'
47 filesystem_shards = 'file_store.filesystem_v2.shards'
48
49 # objectstore
50 objectstore_url = 'file_store.objectstore.url'
51 objectstore_bucket = 'file_store.objectstore.bucket'
52 objectstore_bucket_shards = 'file_store.objectstore.bucket_shards'
53
54 objectstore_region = 'file_store.objectstore.region'
55 objectstore_key = 'file_store.objectstore.key'
56 objectstore_secret = 'file_store.objectstore.secret'
57
@@ -1,18 +1,57 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 import os
20 import random
21 import tempfile
22 import string
23
24 import pytest
25
26 from rhodecode.apps.file_store import utils as store_utils
27
28
29 @pytest.fixture()
30 def file_store_instance(ini_settings):
31 config = ini_settings
32 f_store = store_utils.get_filestore_backend(config=config, always_init=True)
33 return f_store
34
35
36 @pytest.fixture
37 def random_binary_file():
38 # Generate random binary data
39 data = bytearray(random.getrandbits(8) for _ in range(1024 * 512)) # 512 KB of random data
40
41 # Create a temporary file
42 temp_file = tempfile.NamedTemporaryFile(delete=False)
43 filename = temp_file.name
44
45 try:
46 # Write the random binary data to the file
47 temp_file.write(data)
48 temp_file.seek(0) # Rewind the file pointer to the beginning
49 yield filename, temp_file
50 finally:
51 # Close and delete the temporary file after the test
52 temp_file.close()
53 os.remove(filename)
54
55
56 def generate_random_filename(length=10):
57 return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) No newline at end of file
@@ -1,246 +1,253 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18 19 import os
20
19 21 import pytest
20 22
21 23 from rhodecode.lib.ext_json import json
22 24 from rhodecode.model.auth_token import AuthTokenModel
23 25 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.apps.file_store import utils, config_keys
26 from rhodecode.apps.file_store import utils as store_utils
27 from rhodecode.apps.file_store import config_keys
25 28
26 29 from rhodecode.tests import TestController
27 30 from rhodecode.tests.routes import route_path
28 31
29 32
30 33 class TestFileStoreViews(TestController):
31 34
35 @pytest.fixture()
36 def create_artifact_factory(self, tmpdir, ini_settings):
37
38 def factory(user_id, content, f_name='example.txt'):
39
40 config = ini_settings
41 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
42
43 f_store = store_utils.get_filestore_backend(config)
44
45 filesystem_file = os.path.join(str(tmpdir), f_name)
46 with open(filesystem_file, 'wt') as f:
47 f.write(content)
48
49 with open(filesystem_file, 'rb') as f:
50 store_uid, metadata = f_store.store(f_name, f, metadata={'filename': f_name})
51 os.remove(filesystem_file)
52
53 entry = FileStore.create(
54 file_uid=store_uid, filename=metadata["filename"],
55 file_hash=metadata["sha256"], file_size=metadata["size"],
56 file_display_name='file_display_name',
57 file_description='repo artifact `{}`'.format(metadata["filename"]),
58 check_acl=True, user_id=user_id,
59 )
60 Session().add(entry)
61 Session().commit()
62 return entry
63 return factory
64
32 65 @pytest.mark.parametrize("fid, content, exists", [
33 66 ('abcde-0.jpg', "xxxxx", True),
34 67 ('abcde-0.exe', "1234567", True),
35 68 ('abcde-0.jpg', "xxxxx", False),
36 69 ])
37 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
70 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util, ini_settings):
38 71 user = self.log_user()
39 72 user_id = user['user_id']
40 73 repo_id = user_util.create_repo().repo_id
41 store_path = self.app._pyramid_settings[config_keys.store_path]
74
75 config = ini_settings
76 config[config_keys.backend_type] = config_keys.backend_legacy_filesystem
77
42 78 store_uid = fid
43 79
44 80 if exists:
45 81 status = 200
46 store = utils.get_file_storage({config_keys.store_path: store_path})
82 f_store = store_utils.get_filestore_backend(config)
47 83 filesystem_file = os.path.join(str(tmpdir), fid)
48 84 with open(filesystem_file, 'wt') as f:
49 85 f.write(content)
50 86
51 87 with open(filesystem_file, 'rb') as f:
52 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
88 store_uid, metadata = f_store.store(fid, f, metadata={'filename': fid})
89 os.remove(filesystem_file)
53 90
54 91 entry = FileStore.create(
55 92 file_uid=store_uid, filename=metadata["filename"],
56 93 file_hash=metadata["sha256"], file_size=metadata["size"],
57 94 file_display_name='file_display_name',
58 95 file_description='repo artifact `{}`'.format(metadata["filename"]),
59 96 check_acl=True, user_id=user_id,
60 97 scope_repo_id=repo_id
61 98 )
62 99 Session().add(entry)
63 100 Session().commit()
64 101
65 102 else:
66 103 status = 404
67 104
68 105 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
69 106
70 107 if exists:
71 108 assert response.text == content
72 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
73 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
74 assert os.path.exists(metadata_file)
75 with open(metadata_file, 'rb') as f:
76 json_data = json.loads(f.read())
77 109
78 assert json_data
79 assert 'size' in json_data
110 metadata = f_store.get_metadata(store_uid)
111
112 assert 'size' in metadata
80 113
81 114 def test_upload_files_without_content_to_store(self):
82 115 self.log_user()
83 116 response = self.app.post(
84 117 route_path('upload_file'),
85 118 params={'csrf_token': self.csrf_token},
86 119 status=200)
87 120
88 121 assert response.json == {
89 122 'error': 'store_file data field is missing',
90 123 'access_path': None,
91 124 'store_fid': None}
92 125
93 126 def test_upload_files_bogus_content_to_store(self):
94 127 self.log_user()
95 128 response = self.app.post(
96 129 route_path('upload_file'),
97 130 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
98 131 status=200)
99 132
100 133 assert response.json == {
101 134 'error': 'filename cannot be read from the data field',
102 135 'access_path': None,
103 136 'store_fid': None}
104 137
105 138 def test_upload_content_to_store(self):
106 139 self.log_user()
107 140 response = self.app.post(
108 141 route_path('upload_file'),
109 142 upload_files=[('store_file', b'myfile.txt', b'SOME CONTENT')],
110 143 params={'csrf_token': self.csrf_token},
111 144 status=200)
112 145
113 146 assert response.json['store_fid']
114 147
115 @pytest.fixture()
116 def create_artifact_factory(self, tmpdir):
117 def factory(user_id, content):
118 store_path = self.app._pyramid_settings[config_keys.store_path]
119 store = utils.get_file_storage({config_keys.store_path: store_path})
120 fid = 'example.txt'
121
122 filesystem_file = os.path.join(str(tmpdir), fid)
123 with open(filesystem_file, 'wt') as f:
124 f.write(content)
125
126 with open(filesystem_file, 'rb') as f:
127 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
128
129 entry = FileStore.create(
130 file_uid=store_uid, filename=metadata["filename"],
131 file_hash=metadata["sha256"], file_size=metadata["size"],
132 file_display_name='file_display_name',
133 file_description='repo artifact `{}`'.format(metadata["filename"]),
134 check_acl=True, user_id=user_id,
135 )
136 Session().add(entry)
137 Session().commit()
138 return entry
139 return factory
140
141 148 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
142 149 user = self.log_user()
143 150 user_id = user['user_id']
144 151 content = 'HELLO MY NAME IS ARTIFACT !'
145 152
146 153 artifact = create_artifact_factory(user_id, content)
147 154 file_uid = artifact.file_uid
148 155 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
149 156 assert response.text == content
150 157
151 158 # log-in to new user and test download again
152 159 user = user_util.create_user(password='qweqwe')
153 160 self.log_user(user.username, 'qweqwe')
154 161 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
155 162 assert response.text == content
156 163
157 164 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
158 165 user = self.log_user()
159 166 user_id = user['user_id']
160 167 content = 'HELLO MY NAME IS ARTIFACT !'
161 168
162 169 artifact = create_artifact_factory(user_id, content)
163 170 # bind to repo
164 171 repo = user_util.create_repo()
165 172 repo_id = repo.repo_id
166 173 artifact.scope_repo_id = repo_id
167 174 Session().add(artifact)
168 175 Session().commit()
169 176
170 177 file_uid = artifact.file_uid
171 178 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
172 179 assert response.text == content
173 180
174 181 # log-in to new user and test download again
175 182 user = user_util.create_user(password='qweqwe')
176 183 self.log_user(user.username, 'qweqwe')
177 184 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
178 185 assert response.text == content
179 186
180 187 # forbid user the rights to repo
181 188 repo = Repository.get(repo_id)
182 189 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
183 190 self.app.get(route_path('download_file', fid=file_uid), status=404)
184 191
185 192 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
186 193 user = self.log_user()
187 194 user_id = user['user_id']
188 195 content = 'HELLO MY NAME IS ARTIFACT !'
189 196
190 197 artifact = create_artifact_factory(user_id, content)
191 198 # bind to user
192 199 user = user_util.create_user(password='qweqwe')
193 200
194 201 artifact.scope_user_id = user.user_id
195 202 Session().add(artifact)
196 203 Session().commit()
197 204
198 205 # artifact creator doesn't have access since it's bind to another user
199 206 file_uid = artifact.file_uid
200 207 self.app.get(route_path('download_file', fid=file_uid), status=404)
201 208
202 209 # log-in to new user and test download again, should be ok since we're bind to this artifact
203 210 self.log_user(user.username, 'qweqwe')
204 211 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
205 212 assert response.text == content
206 213
207 214 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
208 215 user_id = User.get_first_super_admin().user_id
209 216 content = 'HELLO MY NAME IS ARTIFACT !'
210 217
211 218 artifact = create_artifact_factory(user_id, content)
212 219 # bind to repo
213 220 repo = user_util.create_repo()
214 221 repo_id = repo.repo_id
215 222 artifact.scope_repo_id = repo_id
216 223 Session().add(artifact)
217 224 Session().commit()
218 225
219 226 file_uid = artifact.file_uid
220 227 self.app.get(route_path('download_file_by_token',
221 228 _auth_token='bogus', fid=file_uid), status=302)
222 229
223 230 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
224 231 user = User.get_first_super_admin()
225 232 AuthTokenModel().create(user, 'test artifact token',
226 233 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
227 234
228 235 user = User.get_first_super_admin()
229 236 artifact_token = user.artifact_token
230 237
231 238 user_id = User.get_first_super_admin().user_id
232 239 content = 'HELLO MY NAME IS ARTIFACT !'
233 240
234 241 artifact = create_artifact_factory(user_id, content)
235 242 # bind to repo
236 243 repo = user_util.create_repo()
237 244 repo_id = repo.repo_id
238 245 artifact.scope_repo_id = repo_id
239 246 Session().add(artifact)
240 247 Session().commit()
241 248
242 249 file_uid = artifact.file_uid
243 250 response = self.app.get(
244 251 route_path('download_file_by_token',
245 252 _auth_token=artifact_token, fid=file_uid), status=200)
246 253 assert response.text == content
@@ -1,55 +1,145 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import io
20 20 import uuid
21 21 import pathlib
22 import s3fs
23
24 from rhodecode.lib.hash_utils import sha256_safe
25 from rhodecode.apps.file_store import config_keys
26
27
28 file_store_meta = None
29
30
31 def get_filestore_config(config) -> dict:
32
33 final_config = {}
34
35 for k, v in config.items():
36 if k.startswith('file_store'):
37 final_config[k] = v
38
39 return final_config
22 40
23 41
24 def get_file_storage(settings):
25 from rhodecode.apps.file_store.backends.local_store import LocalFileStorage
26 from rhodecode.apps.file_store import config_keys
27 store_path = settings.get(config_keys.store_path)
28 return LocalFileStorage(base_path=store_path)
42 def get_filestore_backend(config, always_init=False):
43 """
44
45 usage::
46 from rhodecode.apps.file_store import get_filestore_backend
47 f_store = get_filestore_backend(config=CONFIG)
48
49 :param config:
50 :param always_init:
51 :return:
52 """
53
54 global file_store_meta
55 if file_store_meta is not None and not always_init:
56 return file_store_meta
57
58 config = get_filestore_config(config)
59 backend = config[config_keys.backend_type]
60
61 match backend:
62 case config_keys.backend_legacy_filesystem:
63 # Legacy backward compatible storage
64 from rhodecode.apps.file_store.backends.filesystem_legacy import LegacyFileSystemBackend
65 d_cache = LegacyFileSystemBackend(
66 settings=config
67 )
68 case config_keys.backend_filesystem:
69 from rhodecode.apps.file_store.backends.filesystem import FileSystemBackend
70 d_cache = FileSystemBackend(
71 settings=config
72 )
73 case config_keys.backend_objectstore:
74 from rhodecode.apps.file_store.backends.objectstore import ObjectStoreBackend
75 d_cache = ObjectStoreBackend(
76 settings=config
77 )
78 case _:
79 raise ValueError(
80 f'file_store.backend.type only supports "{config_keys.backend_types}" got {backend}'
81 )
82
83 cache_meta = d_cache
84 return cache_meta
29 85
30 86
31 87 def splitext(filename):
32 ext = ''.join(pathlib.Path(filename).suffixes)
88 final_ext = []
89 for suffix in pathlib.Path(filename).suffixes:
90 if not suffix.isascii():
91 continue
92
93 suffix = " ".join(suffix.split()).replace(" ", "")
94 final_ext.append(suffix)
95 ext = ''.join(final_ext)
33 96 return filename, ext
34 97
35 98
36 def uid_filename(filename, randomized=True):
99 def get_uid_filename(filename, randomized=True):
37 100 """
38 101 Generates a randomized or stable (uuid) filename,
39 102 preserving the original extension.
40 103
41 104 :param filename: the original filename
42 105 :param randomized: define if filename should be stable (sha1 based) or randomized
43 106 """
44 107
45 108 _, ext = splitext(filename)
46 109 if randomized:
47 110 uid = uuid.uuid4()
48 111 else:
49 hash_key = '{}.{}'.format(filename, 'store')
112 store_suffix = "store"
113 hash_key = f'{filename}.{store_suffix}'
50 114 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
51 115 return str(uid) + ext.lower()
52 116
53 117
54 118 def bytes_to_file_obj(bytes_data):
55 return io.StringIO(bytes_data)
119 return io.BytesIO(bytes_data)
120
121
122 class ShardFileReader:
123
124 def __init__(self, file_like_reader):
125 self._file_like_reader = file_like_reader
126
127 def __getattr__(self, item):
128 if isinstance(self._file_like_reader, s3fs.core.S3File):
129 match item:
130 case 'name':
131 # S3 FileWrapper doesn't support name attribute, and we use it
132 return self._file_like_reader.full_name
133 case _:
134 return getattr(self._file_like_reader, item)
135 else:
136 return getattr(self._file_like_reader, item)
137
138
139 def archive_iterator(_reader, block_size: int = 4096 * 512):
140 # 4096 * 64 = 64KB
141 while 1:
142 data = _reader.read(block_size)
143 if not data:
144 break
145 yield data
@@ -1,200 +1,197 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18 import logging
19 19
20
21 from pyramid.response import FileResponse
20 from pyramid.response import Response
22 21 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
23 22
24 23 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps.file_store import utils
24 from rhodecode.apps.file_store import utils as store_utils
26 25 from rhodecode.apps.file_store.exceptions import (
27 26 FileNotAllowedException, FileOverSizeException)
28 27
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.lib import audit_logger
31 30 from rhodecode.lib.auth import (
32 31 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
33 32 LoginRequired)
33 from rhodecode.lib.str_utils import header_safe_str
34 34 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 35 from rhodecode.model.db import Session, FileStore, UserApiKeys
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class FileStoreView(BaseAppView):
41 41 upload_key = 'store_file'
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 self.storage = utils.get_file_storage(self.request.registry.settings)
45 self.f_store = store_utils.get_filestore_backend(self.request.registry.settings)
46 46 return c
47 47
48 48 def _guess_type(self, file_name):
49 49 """
50 50 Our own type guesser for mimetypes using the rich DB
51 51 """
52 52 if not hasattr(self, 'db'):
53 53 self.db = get_mimetypes_db()
54 54 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
55 55 return _content_type, _encoding
56 56
57 57 def _serve_file(self, file_uid):
58 if not self.storage.exists(file_uid):
59 store_path = self.storage.store_path(file_uid)
58 if not self.f_store.filename_exists(file_uid):
59 store_path = self.f_store.store_path(file_uid)
60 60 log.warning('File with FID:%s not found in the store under `%s`',
61 61 file_uid, store_path)
62 62 raise HTTPNotFound()
63 63
64 64 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
65 65 if not db_obj:
66 66 raise HTTPNotFound()
67 67
68 68 # private upload for user
69 69 if db_obj.check_acl and db_obj.scope_user_id:
70 70 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
71 71 db_obj.scope_user_id)
72 72 user = db_obj.user
73 73 if self._rhodecode_db_user.user_id != user.user_id:
74 74 log.warning('Access to file store object forbidden')
75 75 raise HTTPNotFound()
76 76
77 77 # scoped to repository permissions
78 78 if db_obj.check_acl and db_obj.scope_repo_id:
79 79 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
80 80 db_obj.scope_repo_id)
81 81 repo = db_obj.repo
82 82 perm_set = ['repository.read', 'repository.write', 'repository.admin']
83 83 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
84 84 if not has_perm:
85 85 log.warning('Access to file store object `%s` forbidden', file_uid)
86 86 raise HTTPNotFound()
87 87
88 88 # scoped to repository group permissions
89 89 if db_obj.check_acl and db_obj.scope_repo_group_id:
90 90 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
91 91 db_obj.scope_repo_group_id)
92 92 repo_group = db_obj.repo_group
93 93 perm_set = ['group.read', 'group.write', 'group.admin']
94 94 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
95 95 if not has_perm:
96 96 log.warning('Access to file store object `%s` forbidden', file_uid)
97 97 raise HTTPNotFound()
98 98
99 99 FileStore.bump_access_counter(file_uid)
100 100
101 file_path = self.storage.store_path(file_uid)
101 file_name = db_obj.file_display_name
102 102 content_type = 'application/octet-stream'
103 content_encoding = None
104 103
105 _content_type, _encoding = self._guess_type(file_path)
104 _content_type, _encoding = self._guess_type(file_name)
106 105 if _content_type:
107 106 content_type = _content_type
108 107
109 108 # For file store we don't submit any session data, this logic tells the
110 109 # Session lib to skip it
111 110 setattr(self.request, '_file_response', True)
112 response = FileResponse(
113 file_path, request=self.request,
114 content_type=content_type, content_encoding=content_encoding)
111 reader, _meta = self.f_store.fetch(file_uid)
115 112
116 file_name = db_obj.file_display_name
113 response = Response(app_iter=store_utils.archive_iterator(reader))
117 114
118 response.headers["Content-Disposition"] = (
119 f'attachment; filename="{str(file_name)}"'
120 )
115 response.content_type = str(content_type)
116 response.content_disposition = f'attachment; filename="{header_safe_str(file_name)}"'
117
121 118 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
122 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
119 response.headers["X-RC-Artifact-Desc"] = header_safe_str(db_obj.file_description)
123 120 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
124 121 return response
125 122
126 123 @LoginRequired()
127 124 @NotAnonymous()
128 125 @CSRFRequired()
129 126 def upload_file(self):
130 127 self.load_default_context()
131 128 file_obj = self.request.POST.get(self.upload_key)
132 129
133 130 if file_obj is None:
134 131 return {'store_fid': None,
135 132 'access_path': None,
136 133 'error': f'{self.upload_key} data field is missing'}
137 134
138 135 if not hasattr(file_obj, 'filename'):
139 136 return {'store_fid': None,
140 137 'access_path': None,
141 138 'error': 'filename cannot be read from the data field'}
142 139
143 140 filename = file_obj.filename
144 141
145 142 metadata = {
146 143 'user_uploaded': {'username': self._rhodecode_user.username,
147 144 'user_id': self._rhodecode_user.user_id,
148 145 'ip': self._rhodecode_user.ip_addr}}
149 146 try:
150 store_uid, metadata = self.storage.save_file(
151 file_obj.file, filename, extra_metadata=metadata)
147 store_uid, metadata = self.f_store.store(
148 filename, file_obj.file, extra_metadata=metadata)
152 149 except FileNotAllowedException:
153 150 return {'store_fid': None,
154 151 'access_path': None,
155 152 'error': f'File {filename} is not allowed.'}
156 153
157 154 except FileOverSizeException:
158 155 return {'store_fid': None,
159 156 'access_path': None,
160 157 'error': f'File {filename} is exceeding allowed limit.'}
161 158
162 159 try:
163 160 entry = FileStore.create(
164 161 file_uid=store_uid, filename=metadata["filename"],
165 162 file_hash=metadata["sha256"], file_size=metadata["size"],
166 163 file_description='upload attachment',
167 164 check_acl=False, user_id=self._rhodecode_user.user_id
168 165 )
169 166 Session().add(entry)
170 167 Session().commit()
171 168 log.debug('Stored upload in DB as %s', entry)
172 169 except Exception:
173 170 log.exception('Failed to store file %s', filename)
174 171 return {'store_fid': None,
175 172 'access_path': None,
176 173 'error': f'File {filename} failed to store in DB.'}
177 174
178 175 return {'store_fid': store_uid,
179 176 'access_path': h.route_path('download_file', fid=store_uid)}
180 177
181 178 # ACL is checked by scopes, if no scope the file is accessible to all
182 179 def download_file(self):
183 180 self.load_default_context()
184 181 file_uid = self.request.matchdict['fid']
185 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
182 log.debug('Requesting FID:%s from store %s', file_uid, self.f_store)
186 183 return self._serve_file(file_uid)
187 184
188 185 # in addition to @LoginRequired ACL is checked by scopes
189 186 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
190 187 @NotAnonymous()
191 188 def download_file_by_token(self):
192 189 """
193 190 Special view that allows to access the download file by special URL that
194 191 is stored inside the URL.
195 192
196 193 http://example.com/_file_store/token-download/TOKEN/FILE_UID
197 194 """
198 195 self.load_default_context()
199 196 file_uid = self.request.matchdict['fid']
200 197 return self._serve_file(file_uid)
@@ -1,830 +1,830 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import collections
21 21
22 22 from pyramid.httpexceptions import (
23 23 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
24 24 from pyramid.renderers import render
25 25 from pyramid.response import Response
26 26
27 27 from rhodecode.apps._base import RepoAppView
28 28 from rhodecode.apps.file_store import utils as store_utils
29 29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
30 30
31 31 from rhodecode.lib import diffs, codeblocks, channelstream
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 34 from rhodecode.lib import ext_json
35 35 from collections import OrderedDict
36 36 from rhodecode.lib.diffs import (
37 37 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
38 38 get_diff_whitespace_flag)
39 39 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.utils2 import str2bool, StrictAttributeDict, safe_str
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 RepositoryError, CommitDoesNotExistError)
45 45 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
46 46 ChangesetCommentHistory
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 def _update_with_GET(params, request):
56 56 for k in ['diff1', 'diff2', 'diff']:
57 57 params[k] += request.GET.getall(k)
58 58
59 59
60 60 class RepoCommitsView(RepoAppView):
61 61 def load_default_context(self):
62 62 c = self._get_local_tmpl_context(include_app_defaults=True)
63 63 c.rhodecode_repo = self.rhodecode_vcs_repo
64 64
65 65 return c
66 66
67 67 def _is_diff_cache_enabled(self, target_repo):
68 68 caching_enabled = self._get_general_setting(
69 69 target_repo, 'rhodecode_diff_cache')
70 70 log.debug('Diff caching enabled: %s', caching_enabled)
71 71 return caching_enabled
72 72
73 73 def _commit(self, commit_id_range, method):
74 74 _ = self.request.translate
75 75 c = self.load_default_context()
76 76 c.fulldiff = self.request.GET.get('fulldiff')
77 77 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
78 78
79 79 # fetch global flags of ignore ws or context lines
80 80 diff_context = get_diff_context(self.request)
81 81 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
82 82
83 83 # diff_limit will cut off the whole diff if the limit is applied
84 84 # otherwise it will just hide the big files from the front-end
85 85 diff_limit = c.visual.cut_off_limit_diff
86 86 file_limit = c.visual.cut_off_limit_file
87 87
88 88 # get ranges of commit ids if preset
89 89 commit_range = commit_id_range.split('...')[:2]
90 90
91 91 try:
92 92 pre_load = ['affected_files', 'author', 'branch', 'date',
93 93 'message', 'parents']
94 94 if self.rhodecode_vcs_repo.alias == 'hg':
95 95 pre_load += ['hidden', 'obsolete', 'phase']
96 96
97 97 if len(commit_range) == 2:
98 98 commits = self.rhodecode_vcs_repo.get_commits(
99 99 start_id=commit_range[0], end_id=commit_range[1],
100 100 pre_load=pre_load, translate_tags=False)
101 101 commits = list(commits)
102 102 else:
103 103 commits = [self.rhodecode_vcs_repo.get_commit(
104 104 commit_id=commit_id_range, pre_load=pre_load)]
105 105
106 106 c.commit_ranges = commits
107 107 if not c.commit_ranges:
108 108 raise RepositoryError('The commit range returned an empty result')
109 109 except CommitDoesNotExistError as e:
110 110 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
111 111 h.flash(msg, category='error')
112 112 raise HTTPNotFound()
113 113 except Exception:
114 114 log.exception("General failure")
115 115 raise HTTPNotFound()
116 116 single_commit = len(c.commit_ranges) == 1
117 117
118 118 if redirect_to_combined and not single_commit:
119 119 source_ref = getattr(c.commit_ranges[0].parents[0]
120 120 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
121 121 target_ref = c.commit_ranges[-1].raw_id
122 122 next_url = h.route_path(
123 123 'repo_compare',
124 124 repo_name=c.repo_name,
125 125 source_ref_type='rev',
126 126 source_ref=source_ref,
127 127 target_ref_type='rev',
128 128 target_ref=target_ref)
129 129 raise HTTPFound(next_url)
130 130
131 131 c.changes = OrderedDict()
132 132 c.lines_added = 0
133 133 c.lines_deleted = 0
134 134
135 135 # auto collapse if we have more than limit
136 136 collapse_limit = diffs.DiffProcessor._collapse_commits_over
137 137 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
138 138
139 139 c.commit_statuses = ChangesetStatus.STATUSES
140 140 c.inline_comments = []
141 141 c.files = []
142 142
143 143 c.comments = []
144 144 c.unresolved_comments = []
145 145 c.resolved_comments = []
146 146
147 147 # Single commit
148 148 if single_commit:
149 149 commit = c.commit_ranges[0]
150 150 c.comments = CommentsModel().get_comments(
151 151 self.db_repo.repo_id,
152 152 revision=commit.raw_id)
153 153
154 154 # comments from PR
155 155 statuses = ChangesetStatusModel().get_statuses(
156 156 self.db_repo.repo_id, commit.raw_id,
157 157 with_revisions=True)
158 158
159 159 prs = set()
160 160 reviewers = list()
161 161 reviewers_duplicates = set() # to not have duplicates from multiple votes
162 162 for c_status in statuses:
163 163
164 164 # extract associated pull-requests from votes
165 165 if c_status.pull_request:
166 166 prs.add(c_status.pull_request)
167 167
168 168 # extract reviewers
169 169 _user_id = c_status.author.user_id
170 170 if _user_id not in reviewers_duplicates:
171 171 reviewers.append(
172 172 StrictAttributeDict({
173 173 'user': c_status.author,
174 174
175 175 # fake attributed for commit, page that we don't have
176 176 # but we share the display with PR page
177 177 'mandatory': False,
178 178 'reasons': [],
179 179 'rule_user_group_data': lambda: None
180 180 })
181 181 )
182 182 reviewers_duplicates.add(_user_id)
183 183
184 184 c.reviewers_count = len(reviewers)
185 185 c.observers_count = 0
186 186
187 187 # from associated statuses, check the pull requests, and
188 188 # show comments from them
189 189 for pr in prs:
190 190 c.comments.extend(pr.comments)
191 191
192 192 c.unresolved_comments = CommentsModel()\
193 193 .get_commit_unresolved_todos(commit.raw_id)
194 194 c.resolved_comments = CommentsModel()\
195 195 .get_commit_resolved_todos(commit.raw_id)
196 196
197 197 c.inline_comments_flat = CommentsModel()\
198 198 .get_commit_inline_comments(commit.raw_id)
199 199
200 200 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
201 201 statuses, reviewers)
202 202
203 203 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
204 204
205 205 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
206 206
207 207 for review_obj, member, reasons, mandatory, status in review_statuses:
208 208 member_reviewer = h.reviewer_as_json(
209 209 member, reasons=reasons, mandatory=mandatory, role=None,
210 210 user_group=None
211 211 )
212 212
213 213 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
214 214 member_reviewer['review_status'] = current_review_status
215 215 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
216 216 member_reviewer['allowed_to_update'] = False
217 217 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
218 218
219 219 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
220 220
221 221 # NOTE(marcink): this uses the same voting logic as in pull-requests
222 222 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
223 223 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
224 224
225 225 diff = None
226 226 # Iterate over ranges (default commit view is always one commit)
227 227 for commit in c.commit_ranges:
228 228 c.changes[commit.raw_id] = []
229 229
230 230 commit2 = commit
231 231 commit1 = commit.first_parent
232 232
233 233 if method == 'show':
234 234 inline_comments = CommentsModel().get_inline_comments(
235 235 self.db_repo.repo_id, revision=commit.raw_id)
236 236 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
237 237 inline_comments))
238 238 c.inline_comments = inline_comments
239 239
240 240 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
241 241 self.db_repo)
242 242 cache_file_path = diff_cache_exist(
243 243 cache_path, 'diff', commit.raw_id,
244 244 hide_whitespace_changes, diff_context, c.fulldiff)
245 245
246 246 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
247 247 force_recache = str2bool(self.request.GET.get('force_recache'))
248 248
249 249 cached_diff = None
250 250 if caching_enabled:
251 251 cached_diff = load_cached_diff(cache_file_path)
252 252
253 253 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
254 254 if not force_recache and has_proper_diff_cache:
255 255 diffset = cached_diff['diff']
256 256 else:
257 257 vcs_diff = self.rhodecode_vcs_repo.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
263 263 diff_limit=diff_limit,
264 264 file_limit=file_limit,
265 265 show_full_diff=c.fulldiff)
266 266
267 267 _parsed = diff_processor.prepare()
268 268
269 269 diffset = codeblocks.DiffSet(
270 270 repo_name=self.db_repo_name,
271 271 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 272 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 273
274 274 diffset = self.path_filter.render_patchset_filtered(
275 275 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 276
277 277 # save cached diff
278 278 if caching_enabled:
279 279 cache_diff(cache_file_path, diffset, None)
280 280
281 281 c.limited_diff = diffset.limited_diff
282 282 c.changes[commit.raw_id] = diffset
283 283 else:
284 284 # TODO(marcink): no cache usage here...
285 285 _diff = self.rhodecode_vcs_repo.get_diff(
286 286 commit1, commit2,
287 287 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 288 diff_processor = diffs.DiffProcessor(_diff, diff_format='newdiff',
289 289 diff_limit=diff_limit,
290 290 file_limit=file_limit, show_full_diff=c.fulldiff)
291 291 # downloads/raw we only need RAW diff nothing else
292 292 diff = self.path_filter.get_raw_patch(diff_processor)
293 293 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 294
295 295 # sort comments by how they were generated
296 296 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 297 c.at_version_num = None
298 298
299 299 if len(c.commit_ranges) == 1:
300 300 c.commit = c.commit_ranges[0]
301 301 c.parent_tmpl = ''.join(
302 302 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 303
304 304 if method == 'download':
305 305 response = Response(diff)
306 306 response.content_type = 'text/plain'
307 307 response.content_disposition = (
308 308 'attachment; filename=%s.diff' % commit_id_range[:12])
309 309 return response
310 310 elif method == 'patch':
311 311
312 312 c.diff = safe_str(diff)
313 313 patch = render(
314 314 'rhodecode:templates/changeset/patch_changeset.mako',
315 315 self._get_template_context(c), self.request)
316 316 response = Response(patch)
317 317 response.content_type = 'text/plain'
318 318 return response
319 319 elif method == 'raw':
320 320 response = Response(diff)
321 321 response.content_type = 'text/plain'
322 322 return response
323 323 elif method == 'show':
324 324 if len(c.commit_ranges) == 1:
325 325 html = render(
326 326 'rhodecode:templates/changeset/changeset.mako',
327 327 self._get_template_context(c), self.request)
328 328 return Response(html)
329 329 else:
330 330 c.ancestor = None
331 331 c.target_repo = self.db_repo
332 332 html = render(
333 333 'rhodecode:templates/changeset/changeset_range.mako',
334 334 self._get_template_context(c), self.request)
335 335 return Response(html)
336 336
337 337 raise HTTPBadRequest()
338 338
339 339 @LoginRequired()
340 340 @HasRepoPermissionAnyDecorator(
341 341 'repository.read', 'repository.write', 'repository.admin')
342 342 def repo_commit_show(self):
343 343 commit_id = self.request.matchdict['commit_id']
344 344 return self._commit(commit_id, method='show')
345 345
346 346 @LoginRequired()
347 347 @HasRepoPermissionAnyDecorator(
348 348 'repository.read', 'repository.write', 'repository.admin')
349 349 def repo_commit_raw(self):
350 350 commit_id = self.request.matchdict['commit_id']
351 351 return self._commit(commit_id, method='raw')
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 def repo_commit_patch(self):
357 357 commit_id = self.request.matchdict['commit_id']
358 358 return self._commit(commit_id, method='patch')
359 359
360 360 @LoginRequired()
361 361 @HasRepoPermissionAnyDecorator(
362 362 'repository.read', 'repository.write', 'repository.admin')
363 363 def repo_commit_download(self):
364 364 commit_id = self.request.matchdict['commit_id']
365 365 return self._commit(commit_id, method='download')
366 366
367 367 def _commit_comments_create(self, commit_id, comments):
368 368 _ = self.request.translate
369 369 data = {}
370 370 if not comments:
371 371 return
372 372
373 373 commit = self.db_repo.get_commit(commit_id)
374 374
375 375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 376 for entry in comments:
377 377 c = self.load_default_context()
378 378 comment_type = entry['comment_type']
379 379 text = entry['text']
380 380 status = entry['status']
381 381 is_draft = str2bool(entry['is_draft'])
382 382 resolves_comment_id = entry['resolves_comment_id']
383 383 f_path = entry['f_path']
384 384 line_no = entry['line']
385 385 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
386 386
387 387 if status:
388 388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 389 % {'transition_icon': '>',
390 390 'status': ChangesetStatus.get_status_lbl(status)})
391 391
392 392 comment = CommentsModel().create(
393 393 text=text,
394 394 repo=self.db_repo.repo_id,
395 395 user=self._rhodecode_db_user.user_id,
396 396 commit_id=commit_id,
397 397 f_path=f_path,
398 398 line_no=line_no,
399 399 status_change=(ChangesetStatus.get_status_lbl(status)
400 400 if status else None),
401 401 status_change_type=status,
402 402 comment_type=comment_type,
403 403 is_draft=is_draft,
404 404 resolves_comment_id=resolves_comment_id,
405 405 auth_user=self._rhodecode_user,
406 406 send_email=not is_draft, # skip notification for draft comments
407 407 )
408 408 is_inline = comment.is_inline
409 409
410 410 # get status if set !
411 411 if status:
412 412 # `dont_allow_on_closed_pull_request = True` means
413 413 # if latest status was from pull request and it's closed
414 414 # disallow changing status !
415 415
416 416 try:
417 417 ChangesetStatusModel().set_status(
418 418 self.db_repo.repo_id,
419 419 status,
420 420 self._rhodecode_db_user.user_id,
421 421 comment,
422 422 revision=commit_id,
423 423 dont_allow_on_closed_pull_request=True
424 424 )
425 425 except StatusChangeOnClosedPullRequestError:
426 426 msg = _('Changing the status of a commit associated with '
427 427 'a closed pull request is not allowed')
428 428 log.exception(msg)
429 429 h.flash(msg, category='warning')
430 430 raise HTTPFound(h.route_path(
431 431 'repo_commit', repo_name=self.db_repo_name,
432 432 commit_id=commit_id))
433 433
434 434 Session().flush()
435 435 # this is somehow required to get access to some relationship
436 436 # loaded on comment
437 437 Session().refresh(comment)
438 438
439 439 # skip notifications for drafts
440 440 if not is_draft:
441 441 CommentsModel().trigger_commit_comment_hook(
442 442 self.db_repo, self._rhodecode_user, 'create',
443 443 data={'comment': comment, 'commit': commit})
444 444
445 445 comment_id = comment.comment_id
446 446 data[comment_id] = {
447 447 'target_id': target_elem_id
448 448 }
449 449 Session().flush()
450 450
451 451 c.co = comment
452 452 c.at_version_num = 0
453 453 c.is_new = True
454 454 rendered_comment = render(
455 455 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 456 self._get_template_context(c), self.request)
457 457
458 458 data[comment_id].update(comment.get_dict())
459 459 data[comment_id].update({'rendered_text': rendered_comment})
460 460
461 461 # finalize, commit and redirect
462 462 Session().commit()
463 463
464 464 # skip channelstream for draft comments
465 465 if not all_drafts:
466 466 comment_broadcast_channel = channelstream.comment_channel(
467 467 self.db_repo_name, commit_obj=commit)
468 468
469 469 comment_data = data
470 470 posted_comment_type = 'inline' if is_inline else 'general'
471 471 if len(data) == 1:
472 472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 473 else:
474 474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475 475
476 476 channelstream.comment_channelstream_push(
477 477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 478 comment_data=comment_data)
479 479
480 480 return data
481 481
482 482 @LoginRequired()
483 483 @NotAnonymous()
484 484 @HasRepoPermissionAnyDecorator(
485 485 'repository.read', 'repository.write', 'repository.admin')
486 486 @CSRFRequired()
487 487 def repo_commit_comment_create(self):
488 488 _ = self.request.translate
489 489 commit_id = self.request.matchdict['commit_id']
490 490
491 491 multi_commit_ids = []
492 492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 494 if _commit_id not in multi_commit_ids:
495 495 multi_commit_ids.append(_commit_id)
496 496
497 497 commit_ids = multi_commit_ids or [commit_id]
498 498
499 499 data = []
500 500 # Multiple comments for each passed commit id
501 501 for current_id in filter(None, commit_ids):
502 502 comment_data = {
503 503 'comment_type': self.request.POST.get('comment_type'),
504 504 'text': self.request.POST.get('text'),
505 505 'status': self.request.POST.get('changeset_status', None),
506 506 'is_draft': self.request.POST.get('draft'),
507 507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 508 'close_pull_request': self.request.POST.get('close_pull_request'),
509 509 'f_path': self.request.POST.get('f_path'),
510 510 'line': self.request.POST.get('line'),
511 511 }
512 512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 513 data.append(comment)
514 514
515 515 return data if len(data) > 1 else data[0]
516 516
517 517 @LoginRequired()
518 518 @NotAnonymous()
519 519 @HasRepoPermissionAnyDecorator(
520 520 'repository.read', 'repository.write', 'repository.admin')
521 521 @CSRFRequired()
522 522 def repo_commit_comment_preview(self):
523 523 # Technically a CSRF token is not needed as no state changes with this
524 524 # call. However, as this is a POST is better to have it, so automated
525 525 # tools don't flag it as potential CSRF.
526 526 # Post is required because the payload could be bigger than the maximum
527 527 # allowed by GET.
528 528
529 529 text = self.request.POST.get('text')
530 530 renderer = self.request.POST.get('renderer') or 'rst'
531 531 if text:
532 532 return h.render(text, renderer=renderer, mentions=True,
533 533 repo_name=self.db_repo_name)
534 534 return ''
535 535
536 536 @LoginRequired()
537 537 @HasRepoPermissionAnyDecorator(
538 538 'repository.read', 'repository.write', 'repository.admin')
539 539 @CSRFRequired()
540 540 def repo_commit_comment_history_view(self):
541 541 c = self.load_default_context()
542 542 comment_id = self.request.matchdict['comment_id']
543 543 comment_history_id = self.request.matchdict['comment_history_id']
544 544
545 545 comment = ChangesetComment.get_or_404(comment_id)
546 546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 547 if comment.draft and not comment_owner:
548 548 # if we see draft comments history, we only allow this for owner
549 549 raise HTTPNotFound()
550 550
551 551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
552 552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
553 553
554 554 if is_repo_comment:
555 555 c.comment_history = comment_history
556 556
557 557 rendered_comment = render(
558 558 'rhodecode:templates/changeset/comment_history.mako',
559 559 self._get_template_context(c), self.request)
560 560 return rendered_comment
561 561 else:
562 562 log.warning('No permissions for user %s to show comment_history_id: %s',
563 563 self._rhodecode_db_user, comment_history_id)
564 564 raise HTTPNotFound()
565 565
566 566 @LoginRequired()
567 567 @NotAnonymous()
568 568 @HasRepoPermissionAnyDecorator(
569 569 'repository.read', 'repository.write', 'repository.admin')
570 570 @CSRFRequired()
571 571 def repo_commit_comment_attachment_upload(self):
572 572 c = self.load_default_context()
573 573 upload_key = 'attachment'
574 574
575 575 file_obj = self.request.POST.get(upload_key)
576 576
577 577 if file_obj is None:
578 578 self.request.response.status = 400
579 579 return {'store_fid': None,
580 580 'access_path': None,
581 581 'error': f'{upload_key} data field is missing'}
582 582
583 583 if not hasattr(file_obj, 'filename'):
584 584 self.request.response.status = 400
585 585 return {'store_fid': None,
586 586 'access_path': None,
587 587 'error': 'filename cannot be read from the data field'}
588 588
589 589 filename = file_obj.filename
590 590 file_display_name = filename
591 591
592 592 metadata = {
593 593 'user_uploaded': {'username': self._rhodecode_user.username,
594 594 'user_id': self._rhodecode_user.user_id,
595 595 'ip': self._rhodecode_user.ip_addr}}
596 596
597 597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
598 598 allowed_extensions = [
599 599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
600 600 '.pptx', '.txt', '.xlsx', '.zip']
601 601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
602 602
603 603 try:
604 storage = store_utils.get_file_storage(self.request.registry.settings)
605 store_uid, metadata = storage.save_file(
606 file_obj.file, filename, extra_metadata=metadata,
604 f_store = store_utils.get_filestore_backend(self.request.registry.settings)
605 store_uid, metadata = f_store.store(
606 filename, file_obj.file, metadata=metadata,
607 607 extensions=allowed_extensions, max_filesize=max_file_size)
608 608 except FileNotAllowedException:
609 609 self.request.response.status = 400
610 610 permitted_extensions = ', '.join(allowed_extensions)
611 error_msg = 'File `{}` is not allowed. ' \
612 'Only following extensions are permitted: {}'.format(
613 filename, permitted_extensions)
611 error_msg = f'File `{filename}` is not allowed. ' \
612 f'Only following extensions are permitted: {permitted_extensions}'
613
614 614 return {'store_fid': None,
615 615 'access_path': None,
616 616 'error': error_msg}
617 617 except FileOverSizeException:
618 618 self.request.response.status = 400
619 619 limit_mb = h.format_byte_size_binary(max_file_size)
620 error_msg = f'File {filename} is exceeding allowed limit of {limit_mb}.'
620 621 return {'store_fid': None,
621 622 'access_path': None,
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
623 filename, limit_mb)}
623 'error': error_msg}
624 624
625 625 try:
626 626 entry = FileStore.create(
627 627 file_uid=store_uid, filename=metadata["filename"],
628 628 file_hash=metadata["sha256"], file_size=metadata["size"],
629 629 file_display_name=file_display_name,
630 630 file_description=f'comment attachment `{safe_str(filename)}`',
631 631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
632 632 scope_repo_id=self.db_repo.repo_id
633 633 )
634 634 Session().add(entry)
635 635 Session().commit()
636 636 log.debug('Stored upload in DB as %s', entry)
637 637 except Exception:
638 638 log.exception('Failed to store file %s', filename)
639 639 self.request.response.status = 400
640 640 return {'store_fid': None,
641 641 'access_path': None,
642 642 'error': f'File {filename} failed to store in DB.'}
643 643
644 644 Session().commit()
645 645
646 646 data = {
647 647 'store_fid': store_uid,
648 648 'access_path': h.route_path(
649 649 'download_file', fid=store_uid),
650 650 'fqn_access_path': h.route_url(
651 651 'download_file', fid=store_uid),
652 652 # for EE those are replaced by FQN links on repo-only like
653 653 'repo_access_path': h.route_url(
654 654 'download_file', fid=store_uid),
655 655 'repo_fqn_access_path': h.route_url(
656 656 'download_file', fid=store_uid),
657 657 }
658 658 # this data is a part of CE/EE additional code
659 659 if c.rhodecode_edition_id == 'EE':
660 660 data.update({
661 661 'repo_access_path': h.route_path(
662 662 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
663 663 'repo_fqn_access_path': h.route_url(
664 664 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
665 665 })
666 666
667 667 return data
668 668
669 669 @LoginRequired()
670 670 @NotAnonymous()
671 671 @HasRepoPermissionAnyDecorator(
672 672 'repository.read', 'repository.write', 'repository.admin')
673 673 @CSRFRequired()
674 674 def repo_commit_comment_delete(self):
675 675 commit_id = self.request.matchdict['commit_id']
676 676 comment_id = self.request.matchdict['comment_id']
677 677
678 678 comment = ChangesetComment.get_or_404(comment_id)
679 679 if not comment:
680 680 log.debug('Comment with id:%s not found, skipping', comment_id)
681 681 # comment already deleted in another call probably
682 682 return True
683 683
684 684 if comment.immutable:
685 685 # don't allow deleting comments that are immutable
686 686 raise HTTPForbidden()
687 687
688 688 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
689 689 super_admin = h.HasPermissionAny('hg.admin')()
690 690 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
691 691 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
692 692 comment_repo_admin = is_repo_admin and is_repo_comment
693 693
694 694 if comment.draft and not comment_owner:
695 695 # We never allow to delete draft comments for other than owners
696 696 raise HTTPNotFound()
697 697
698 698 if super_admin or comment_owner or comment_repo_admin:
699 699 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
700 700 Session().commit()
701 701 return True
702 702 else:
703 703 log.warning('No permissions for user %s to delete comment_id: %s',
704 704 self._rhodecode_db_user, comment_id)
705 705 raise HTTPNotFound()
706 706
707 707 @LoginRequired()
708 708 @NotAnonymous()
709 709 @HasRepoPermissionAnyDecorator(
710 710 'repository.read', 'repository.write', 'repository.admin')
711 711 @CSRFRequired()
712 712 def repo_commit_comment_edit(self):
713 713 self.load_default_context()
714 714
715 715 commit_id = self.request.matchdict['commit_id']
716 716 comment_id = self.request.matchdict['comment_id']
717 717 comment = ChangesetComment.get_or_404(comment_id)
718 718
719 719 if comment.immutable:
720 720 # don't allow deleting comments that are immutable
721 721 raise HTTPForbidden()
722 722
723 723 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
724 724 super_admin = h.HasPermissionAny('hg.admin')()
725 725 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
726 726 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
727 727 comment_repo_admin = is_repo_admin and is_repo_comment
728 728
729 729 if super_admin or comment_owner or comment_repo_admin:
730 730 text = self.request.POST.get('text')
731 731 version = self.request.POST.get('version')
732 732 if text == comment.text:
733 733 log.warning(
734 734 'Comment(repo): '
735 735 'Trying to create new version '
736 736 'with the same comment body {}'.format(
737 737 comment_id,
738 738 )
739 739 )
740 740 raise HTTPNotFound()
741 741
742 742 if version.isdigit():
743 743 version = int(version)
744 744 else:
745 745 log.warning(
746 746 'Comment(repo): Wrong version type {} {} '
747 747 'for comment {}'.format(
748 748 version,
749 749 type(version),
750 750 comment_id,
751 751 )
752 752 )
753 753 raise HTTPNotFound()
754 754
755 755 try:
756 756 comment_history = CommentsModel().edit(
757 757 comment_id=comment_id,
758 758 text=text,
759 759 auth_user=self._rhodecode_user,
760 760 version=version,
761 761 )
762 762 except CommentVersionMismatch:
763 763 raise HTTPConflict()
764 764
765 765 if not comment_history:
766 766 raise HTTPNotFound()
767 767
768 768 if not comment.draft:
769 769 commit = self.db_repo.get_commit(commit_id)
770 770 CommentsModel().trigger_commit_comment_hook(
771 771 self.db_repo, self._rhodecode_user, 'edit',
772 772 data={'comment': comment, 'commit': commit})
773 773
774 774 Session().commit()
775 775 return {
776 776 'comment_history_id': comment_history.comment_history_id,
777 777 'comment_id': comment.comment_id,
778 778 'comment_version': comment_history.version,
779 779 'comment_author_username': comment_history.author.username,
780 780 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
781 781 'comment_created_on': h.age_component(comment_history.created_on,
782 782 time_is_local=True),
783 783 }
784 784 else:
785 785 log.warning('No permissions for user %s to edit comment_id: %s',
786 786 self._rhodecode_db_user, comment_id)
787 787 raise HTTPNotFound()
788 788
789 789 @LoginRequired()
790 790 @HasRepoPermissionAnyDecorator(
791 791 'repository.read', 'repository.write', 'repository.admin')
792 792 def repo_commit_data(self):
793 793 commit_id = self.request.matchdict['commit_id']
794 794 self.load_default_context()
795 795
796 796 try:
797 797 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 798 except CommitDoesNotExistError as e:
799 799 return EmptyCommit(message=str(e))
800 800
801 801 @LoginRequired()
802 802 @HasRepoPermissionAnyDecorator(
803 803 'repository.read', 'repository.write', 'repository.admin')
804 804 def repo_commit_children(self):
805 805 commit_id = self.request.matchdict['commit_id']
806 806 self.load_default_context()
807 807
808 808 try:
809 809 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
810 810 children = commit.children
811 811 except CommitDoesNotExistError:
812 812 children = []
813 813
814 814 result = {"results": children}
815 815 return result
816 816
817 817 @LoginRequired()
818 818 @HasRepoPermissionAnyDecorator(
819 819 'repository.read', 'repository.write', 'repository.admin')
820 820 def repo_commit_parents(self):
821 821 commit_id = self.request.matchdict['commit_id']
822 822 self.load_default_context()
823 823
824 824 try:
825 825 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
826 826 parents = commit.parents
827 827 except CommitDoesNotExistError:
828 828 parents = []
829 829 result = {"results": parents}
830 830 return result
@@ -1,1716 +1,1716 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import os
22 22 import collections
23 23 import urllib.request
24 24 import urllib.parse
25 25 import urllib.error
26 26 import pathlib
27 27 import time
28 28 import random
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.hash_utils import sha1_safe
42 42 from rhodecode.lib.archive_cache import (
43 43 get_archival_cache_store, get_archival_config, ArchiveCacheGenerationLock, archive_iterator)
44 44 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
45 45 from rhodecode.lib.view_utils import parse_path_ref
46 46 from rhodecode.lib.exceptions import NonRelativePathError
47 47 from rhodecode.lib.codeblocks import (
48 48 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
49 49 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
50 50 from rhodecode.lib.type_utils import str2bool
51 from rhodecode.lib.str_utils import safe_str, safe_int
51 from rhodecode.lib.str_utils import safe_str, safe_int, header_safe_str
52 52 from rhodecode.lib.auth import (
53 53 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
54 54 from rhodecode.lib.vcs import path as vcspath
55 55 from rhodecode.lib.vcs.backends.base import EmptyCommit
56 56 from rhodecode.lib.vcs.conf import settings
57 57 from rhodecode.lib.vcs.nodes import FileNode
58 58 from rhodecode.lib.vcs.exceptions import (
59 59 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
60 60 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
61 61 NodeDoesNotExistError, CommitError, NodeError)
62 62
63 63 from rhodecode.model.scm import ScmModel
64 64 from rhodecode.model.db import Repository
65 65
66 66 log = logging.getLogger(__name__)
67 67
68 68
69 69 def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
70 70 # original backward compat name of archive
71 71 clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_'))
72 72
73 73 # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip
74 74 # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
75 75 id_sha = sha1_safe(str(db_repo_id))[:4]
76 76 sub_repo = 'sub-1' if subrepos else 'sub-0'
77 77 commit = commit_sha if with_hash else 'archive'
78 78 path_marker = (path_sha if with_hash else '') or 'all'
79 79 archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}'
80 80
81 81 return archive_name
82 82
83 83
84 84 def get_path_sha(at_path):
85 85 return safe_str(sha1_safe(at_path)[:8])
86 86
87 87
88 88 def _get_archive_spec(fname):
89 89 log.debug('Detecting archive spec for: `%s`', fname)
90 90
91 91 fileformat = None
92 92 ext = None
93 93 content_type = None
94 94 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
95 95
96 96 if fname.endswith(extension):
97 97 fileformat = a_type
98 98 log.debug('archive is of type: %s', fileformat)
99 99 ext = extension
100 100 break
101 101
102 102 if not fileformat:
103 103 raise ValueError()
104 104
105 105 # left over part of whole fname is the commit
106 106 commit_id = fname[:-len(ext)]
107 107
108 108 return commit_id, ext, fileformat, content_type
109 109
110 110
111 111 class RepoFilesView(RepoAppView):
112 112
113 113 @staticmethod
114 114 def adjust_file_path_for_svn(f_path, repo):
115 115 """
116 116 Computes the relative path of `f_path`.
117 117
118 118 This is mainly based on prefix matching of the recognized tags and
119 119 branches in the underlying repository.
120 120 """
121 121 tags_and_branches = itertools.chain(
122 122 repo.branches.keys(),
123 123 repo.tags.keys())
124 124 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
125 125
126 126 for name in tags_and_branches:
127 127 if f_path.startswith(f'{name}/'):
128 128 f_path = vcspath.relpath(f_path, name)
129 129 break
130 130 return f_path
131 131
132 132 def load_default_context(self):
133 133 c = self._get_local_tmpl_context(include_app_defaults=True)
134 134 c.rhodecode_repo = self.rhodecode_vcs_repo
135 135 c.enable_downloads = self.db_repo.enable_downloads
136 136 return c
137 137
138 138 def _ensure_not_locked(self, commit_id='tip'):
139 139 _ = self.request.translate
140 140
141 141 repo = self.db_repo
142 142 if repo.enable_locking and repo.locked[0]:
143 143 h.flash(_('This repository has been locked by %s on %s')
144 144 % (h.person_by_id(repo.locked[0]),
145 145 h.format_date(h.time_to_datetime(repo.locked[1]))),
146 146 'warning')
147 147 files_url = h.route_path(
148 148 'repo_files:default_path',
149 149 repo_name=self.db_repo_name, commit_id=commit_id)
150 150 raise HTTPFound(files_url)
151 151
152 152 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
153 153 _ = self.request.translate
154 154
155 155 if not is_head:
156 156 message = _('Cannot modify file. '
157 157 'Given commit `{}` is not head of a branch.').format(commit_id)
158 158 h.flash(message, category='warning')
159 159
160 160 if json_mode:
161 161 return message
162 162
163 163 files_url = h.route_path(
164 164 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
165 165 f_path=f_path)
166 166 raise HTTPFound(files_url)
167 167
168 168 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
169 169 _ = self.request.translate
170 170
171 171 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
172 172 self.db_repo_name, branch_name)
173 173 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
174 174 message = _('Branch `{}` changes forbidden by rule {}.').format(
175 175 h.escape(branch_name), h.escape(rule))
176 176 h.flash(message, 'warning')
177 177
178 178 if json_mode:
179 179 return message
180 180
181 181 files_url = h.route_path(
182 182 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
183 183
184 184 raise HTTPFound(files_url)
185 185
186 186 def _get_commit_and_path(self):
187 187 default_commit_id = self.db_repo.landing_ref_name
188 188 default_f_path = '/'
189 189
190 190 commit_id = self.request.matchdict.get(
191 191 'commit_id', default_commit_id)
192 192 f_path = self._get_f_path(self.request.matchdict, default_f_path)
193 193 return commit_id, f_path
194 194
195 195 def _get_default_encoding(self, c):
196 196 enc_list = getattr(c, 'default_encodings', [])
197 197 return enc_list[0] if enc_list else 'UTF-8'
198 198
199 199 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
200 200 """
201 201 This is a safe way to get commit. If an error occurs it redirects to
202 202 tip with proper message
203 203
204 204 :param commit_id: id of commit to fetch
205 205 :param redirect_after: toggle redirection
206 206 """
207 207 _ = self.request.translate
208 208
209 209 try:
210 210 return self.rhodecode_vcs_repo.get_commit(commit_id)
211 211 except EmptyRepositoryError:
212 212 if not redirect_after:
213 213 return None
214 214
215 215 add_new = upload_new = ""
216 216 if h.HasRepoPermissionAny(
217 217 'repository.write', 'repository.admin')(self.db_repo_name):
218 218 _url = h.route_path(
219 219 'repo_files_add_file',
220 220 repo_name=self.db_repo_name, commit_id=0, f_path='')
221 221 add_new = h.link_to(
222 222 _('add a new file'), _url, class_="alert-link")
223 223
224 224 _url_upld = h.route_path(
225 225 'repo_files_upload_file',
226 226 repo_name=self.db_repo_name, commit_id=0, f_path='')
227 227 upload_new = h.link_to(
228 228 _('upload a new file'), _url_upld, class_="alert-link")
229 229
230 230 h.flash(h.literal(
231 231 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
232 232 raise HTTPFound(
233 233 h.route_path('repo_summary', repo_name=self.db_repo_name))
234 234
235 235 except (CommitDoesNotExistError, LookupError) as e:
236 236 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
237 237 h.flash(msg, category='error')
238 238 raise HTTPNotFound()
239 239 except RepositoryError as e:
240 240 h.flash(h.escape(safe_str(e)), category='error')
241 241 raise HTTPNotFound()
242 242
243 243 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
244 244 """
245 245 Returns file_node, if error occurs or given path is directory,
246 246 it'll redirect to top level path
247 247 """
248 248 _ = self.request.translate
249 249
250 250 try:
251 251 file_node = commit_obj.get_node(path, pre_load=pre_load)
252 252 if file_node.is_dir():
253 253 raise RepositoryError('The given path is a directory')
254 254 except CommitDoesNotExistError:
255 255 log.exception('No such commit exists for this repository')
256 256 h.flash(_('No such commit exists for this repository'), category='error')
257 257 raise HTTPNotFound()
258 258 except RepositoryError as e:
259 259 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
260 260 h.flash(h.escape(safe_str(e)), category='error')
261 261 raise HTTPNotFound()
262 262
263 263 return file_node
264 264
265 265 def _is_valid_head(self, commit_id, repo, landing_ref):
266 266 branch_name = sha_commit_id = ''
267 267 is_head = False
268 268 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
269 269
270 270 for _branch_name, branch_commit_id in repo.branches.items():
271 271 # simple case we pass in branch name, it's a HEAD
272 272 if commit_id == _branch_name:
273 273 is_head = True
274 274 branch_name = _branch_name
275 275 sha_commit_id = branch_commit_id
276 276 break
277 277 # case when we pass in full sha commit_id, which is a head
278 278 elif commit_id == branch_commit_id:
279 279 is_head = True
280 280 branch_name = _branch_name
281 281 sha_commit_id = branch_commit_id
282 282 break
283 283
284 284 if h.is_svn(repo) and not repo.is_empty():
285 285 # Note: Subversion only has one head.
286 286 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
287 287 is_head = True
288 288 return branch_name, sha_commit_id, is_head
289 289
290 290 # checked branches, means we only need to try to get the branch/commit_sha
291 291 if repo.is_empty():
292 292 is_head = True
293 293 branch_name = landing_ref
294 294 sha_commit_id = EmptyCommit().raw_id
295 295 else:
296 296 commit = repo.get_commit(commit_id=commit_id)
297 297 if commit:
298 298 branch_name = commit.branch
299 299 sha_commit_id = commit.raw_id
300 300
301 301 return branch_name, sha_commit_id, is_head
302 302
303 303 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
304 304
305 305 repo_id = self.db_repo.repo_id
306 306 force_recache = self.get_recache_flag()
307 307
308 308 cache_seconds = safe_int(
309 309 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
310 310 cache_on = not force_recache and cache_seconds > 0
311 311 log.debug(
312 312 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
313 313 'with caching: %s[TTL: %ss]' % (
314 314 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
315 315
316 316 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
317 317 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
318 318
319 319 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
320 320 def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
321 321 log.debug('Generating cached file tree at for repo_id: %s, %s, %s',
322 322 _repo_id, _commit_id, _f_path)
323 323
324 324 c.full_load = _full_load
325 325 return render(
326 326 'rhodecode:templates/files/files_browser_tree.mako',
327 327 self._get_template_context(c), self.request, _at_rev)
328 328
329 329 return compute_file_tree(
330 330 self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
331 331
332 332 def create_pure_path(self, *parts):
333 333 # Split paths and sanitize them, removing any ../ etc
334 334 sanitized_path = [
335 335 x for x in pathlib.PurePath(*parts).parts
336 336 if x not in ['.', '..']]
337 337
338 338 pure_path = pathlib.PurePath(*sanitized_path)
339 339 return pure_path
340 340
341 341 def _is_lf_enabled(self, target_repo):
342 342 lf_enabled = False
343 343
344 344 lf_key_for_vcs_map = {
345 345 'hg': 'extensions_largefiles',
346 346 'git': 'vcs_git_lfs_enabled'
347 347 }
348 348
349 349 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
350 350
351 351 if lf_key_for_vcs:
352 352 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
353 353
354 354 return lf_enabled
355 355
356 356 @LoginRequired()
357 357 @HasRepoPermissionAnyDecorator(
358 358 'repository.read', 'repository.write', 'repository.admin')
359 359 def repo_archivefile(self):
360 360 # archive cache config
361 361 from rhodecode import CONFIG
362 362 _ = self.request.translate
363 363 self.load_default_context()
364 364 default_at_path = '/'
365 365 fname = self.request.matchdict['fname']
366 366 subrepos = self.request.GET.get('subrepos') == 'true'
367 367 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
368 368 at_path = self.request.GET.get('at_path') or default_at_path
369 369
370 370 if not self.db_repo.enable_downloads:
371 371 return Response(_('Downloads disabled'))
372 372
373 373 try:
374 374 commit_id, ext, fileformat, content_type = \
375 375 _get_archive_spec(fname)
376 376 except ValueError:
377 377 return Response(_('Unknown archive type for: `{}`').format(
378 378 h.escape(fname)))
379 379
380 380 try:
381 381 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
382 382 except CommitDoesNotExistError:
383 383 return Response(_('Unknown commit_id {}').format(
384 384 h.escape(commit_id)))
385 385 except EmptyRepositoryError:
386 386 return Response(_('Empty repository'))
387 387
388 388 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
389 389 if commit_id != commit.raw_id:
390 390 fname=f'{commit.raw_id}{ext}'
391 391 raise HTTPFound(self.request.current_route_path(fname=fname))
392 392
393 393 try:
394 394 at_path = commit.get_node(at_path).path or default_at_path
395 395 except Exception:
396 396 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
397 397
398 398 path_sha = get_path_sha(at_path)
399 399
400 400 # used for cache etc, consistent unique archive name
401 401 archive_name_key = get_archive_name(
402 402 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
403 403 path_sha=path_sha, with_hash=True)
404 404
405 405 if not with_hash:
406 406 path_sha = ''
407 407
408 408 # what end client gets served
409 409 response_archive_name = get_archive_name(
410 410 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
411 411 path_sha=path_sha, with_hash=with_hash)
412 412
413 413 # remove extension from our archive directory name
414 414 archive_dir_name = response_archive_name[:-len(ext)]
415 415
416 416 archive_cache_disable = self.request.GET.get('no_cache')
417 417
418 418 d_cache = get_archival_cache_store(config=CONFIG)
419 419
420 420 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
421 421 d_cache_conf = get_archival_config(config=CONFIG)
422 422
423 423 # This is also a cache key, and lock key
424 424 reentrant_lock_key = archive_name_key + '.lock'
425 425
426 426 use_cached_archive = False
427 427 if not archive_cache_disable and archive_name_key in d_cache:
428 428 reader, metadata = d_cache.fetch(archive_name_key)
429 429
430 430 use_cached_archive = True
431 431 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
432 432 archive_name_key, metadata, reader.name)
433 433 else:
434 434 reader = None
435 435 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
436 436
437 437 if not reader:
438 438 # generate new archive, as previous was not found in the cache
439 439 try:
440 440 with d_cache.get_lock(reentrant_lock_key):
441 441 try:
442 442 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
443 443 kind=fileformat, subrepos=subrepos,
444 444 archive_at_path=at_path, cache_config=d_cache_conf)
445 445 except ImproperArchiveTypeError:
446 446 return _('Unknown archive type')
447 447
448 448 except ArchiveCacheGenerationLock:
449 449 retry_after = round(random.uniform(0.3, 3.0), 1)
450 450 time.sleep(retry_after)
451 451
452 452 location = self.request.url
453 453 response = Response(
454 454 f"archive {archive_name_key} generation in progress, Retry-After={retry_after}, Location={location}"
455 455 )
456 456 response.headers["Retry-After"] = str(retry_after)
457 457 response.status_code = 307 # temporary redirect
458 458
459 459 response.location = location
460 460 return response
461 461
462 462 reader, metadata = d_cache.fetch(archive_name_key, retry=True, retry_attempts=30)
463 463
464 464 response = Response(app_iter=archive_iterator(reader))
465 465 response.content_disposition = f'attachment; filename={response_archive_name}'
466 466 response.content_type = str(content_type)
467 467
468 468 try:
469 469 return response
470 470 finally:
471 471 # store download action
472 472 audit_logger.store_web(
473 473 'repo.archive.download', action_data={
474 474 'user_agent': self.request.user_agent,
475 475 'archive_name': archive_name_key,
476 476 'archive_spec': fname,
477 477 'archive_cached': use_cached_archive},
478 478 user=self._rhodecode_user,
479 479 repo=self.db_repo,
480 480 commit=True
481 481 )
482 482
483 483 def _get_file_node(self, commit_id, f_path):
484 484 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
485 485 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
486 486 try:
487 487 node = commit.get_node(f_path)
488 488 if node.is_dir():
489 489 raise NodeError(f'{node} path is a {type(node)} not a file')
490 490 except NodeDoesNotExistError:
491 491 commit = EmptyCommit(
492 492 commit_id=commit_id,
493 493 idx=commit.idx,
494 494 repo=commit.repository,
495 495 alias=commit.repository.alias,
496 496 message=commit.message,
497 497 author=commit.author,
498 498 date=commit.date)
499 499 node = FileNode(safe_bytes(f_path), b'', commit=commit)
500 500 else:
501 501 commit = EmptyCommit(
502 502 repo=self.rhodecode_vcs_repo,
503 503 alias=self.rhodecode_vcs_repo.alias)
504 504 node = FileNode(safe_bytes(f_path), b'', commit=commit)
505 505 return node
506 506
507 507 @LoginRequired()
508 508 @HasRepoPermissionAnyDecorator(
509 509 'repository.read', 'repository.write', 'repository.admin')
510 510 def repo_files_diff(self):
511 511 c = self.load_default_context()
512 512 f_path = self._get_f_path(self.request.matchdict)
513 513 diff1 = self.request.GET.get('diff1', '')
514 514 diff2 = self.request.GET.get('diff2', '')
515 515
516 516 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
517 517
518 518 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
519 519 line_context = self.request.GET.get('context', 3)
520 520
521 521 if not any((diff1, diff2)):
522 522 h.flash(
523 523 'Need query parameter "diff1" or "diff2" to generate a diff.',
524 524 category='error')
525 525 raise HTTPBadRequest()
526 526
527 527 c.action = self.request.GET.get('diff')
528 528 if c.action not in ['download', 'raw']:
529 529 compare_url = h.route_path(
530 530 'repo_compare',
531 531 repo_name=self.db_repo_name,
532 532 source_ref_type='rev',
533 533 source_ref=diff1,
534 534 target_repo=self.db_repo_name,
535 535 target_ref_type='rev',
536 536 target_ref=diff2,
537 537 _query=dict(f_path=f_path))
538 538 # redirect to new view if we render diff
539 539 raise HTTPFound(compare_url)
540 540
541 541 try:
542 542 node1 = self._get_file_node(diff1, path1)
543 543 node2 = self._get_file_node(diff2, f_path)
544 544 except (RepositoryError, NodeError):
545 545 log.exception("Exception while trying to get node from repository")
546 546 raise HTTPFound(
547 547 h.route_path('repo_files', repo_name=self.db_repo_name,
548 548 commit_id='tip', f_path=f_path))
549 549
550 550 if all(isinstance(node.commit, EmptyCommit)
551 551 for node in (node1, node2)):
552 552 raise HTTPNotFound()
553 553
554 554 c.commit_1 = node1.commit
555 555 c.commit_2 = node2.commit
556 556
557 557 if c.action == 'download':
558 558 _diff = diffs.get_gitdiff(node1, node2,
559 559 ignore_whitespace=ignore_whitespace,
560 560 context=line_context)
561 561 # NOTE: this was using diff_format='gitdiff'
562 562 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
563 563
564 564 response = Response(self.path_filter.get_raw_patch(diff))
565 565 response.content_type = 'text/plain'
566 566 response.content_disposition = (
567 567 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
568 568 )
569 569 charset = self._get_default_encoding(c)
570 570 if charset:
571 571 response.charset = charset
572 572 return response
573 573
574 574 elif c.action == 'raw':
575 575 _diff = diffs.get_gitdiff(node1, node2,
576 576 ignore_whitespace=ignore_whitespace,
577 577 context=line_context)
578 578 # NOTE: this was using diff_format='gitdiff'
579 579 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
580 580
581 581 response = Response(self.path_filter.get_raw_patch(diff))
582 582 response.content_type = 'text/plain'
583 583 charset = self._get_default_encoding(c)
584 584 if charset:
585 585 response.charset = charset
586 586 return response
587 587
588 588 # in case we ever end up here
589 589 raise HTTPNotFound()
590 590
591 591 @LoginRequired()
592 592 @HasRepoPermissionAnyDecorator(
593 593 'repository.read', 'repository.write', 'repository.admin')
594 594 def repo_files_diff_2way_redirect(self):
595 595 """
596 596 Kept only to make OLD links work
597 597 """
598 598 f_path = self._get_f_path_unchecked(self.request.matchdict)
599 599 diff1 = self.request.GET.get('diff1', '')
600 600 diff2 = self.request.GET.get('diff2', '')
601 601
602 602 if not any((diff1, diff2)):
603 603 h.flash(
604 604 'Need query parameter "diff1" or "diff2" to generate a diff.',
605 605 category='error')
606 606 raise HTTPBadRequest()
607 607
608 608 compare_url = h.route_path(
609 609 'repo_compare',
610 610 repo_name=self.db_repo_name,
611 611 source_ref_type='rev',
612 612 source_ref=diff1,
613 613 target_ref_type='rev',
614 614 target_ref=diff2,
615 615 _query=dict(f_path=f_path, diffmode='sideside',
616 616 target_repo=self.db_repo_name,))
617 617 raise HTTPFound(compare_url)
618 618
619 619 @LoginRequired()
620 620 def repo_files_default_commit_redirect(self):
621 621 """
622 622 Special page that redirects to the landing page of files based on the default
623 623 commit for repository
624 624 """
625 625 c = self.load_default_context()
626 626 ref_name = c.rhodecode_db_repo.landing_ref_name
627 627 landing_url = h.repo_files_by_ref_url(
628 628 c.rhodecode_db_repo.repo_name,
629 629 c.rhodecode_db_repo.repo_type,
630 630 f_path='',
631 631 ref_name=ref_name,
632 632 commit_id='tip',
633 633 query=dict(at=ref_name)
634 634 )
635 635
636 636 raise HTTPFound(landing_url)
637 637
638 638 @LoginRequired()
639 639 @HasRepoPermissionAnyDecorator(
640 640 'repository.read', 'repository.write', 'repository.admin')
641 641 def repo_files(self):
642 642 c = self.load_default_context()
643 643
644 644 view_name = getattr(self.request.matched_route, 'name', None)
645 645
646 646 c.annotate = view_name == 'repo_files:annotated'
647 647 # default is false, but .rst/.md files later are auto rendered, we can
648 648 # overwrite auto rendering by setting this GET flag
649 649 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
650 650
651 651 commit_id, f_path = self._get_commit_and_path()
652 652
653 653 c.commit = self._get_commit_or_redirect(commit_id)
654 654 c.branch = self.request.GET.get('branch', None)
655 655 c.f_path = f_path
656 656 at_rev = self.request.GET.get('at')
657 657
658 658 # files or dirs
659 659 try:
660 660 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
661 661
662 662 c.file_author = True
663 663 c.file_tree = ''
664 664
665 665 # prev link
666 666 try:
667 667 prev_commit = c.commit.prev(c.branch)
668 668 c.prev_commit = prev_commit
669 669 c.url_prev = h.route_path(
670 670 'repo_files', repo_name=self.db_repo_name,
671 671 commit_id=prev_commit.raw_id, f_path=f_path)
672 672 if c.branch:
673 673 c.url_prev += '?branch=%s' % c.branch
674 674 except (CommitDoesNotExistError, VCSError):
675 675 c.url_prev = '#'
676 676 c.prev_commit = EmptyCommit()
677 677
678 678 # next link
679 679 try:
680 680 next_commit = c.commit.next(c.branch)
681 681 c.next_commit = next_commit
682 682 c.url_next = h.route_path(
683 683 'repo_files', repo_name=self.db_repo_name,
684 684 commit_id=next_commit.raw_id, f_path=f_path)
685 685 if c.branch:
686 686 c.url_next += '?branch=%s' % c.branch
687 687 except (CommitDoesNotExistError, VCSError):
688 688 c.url_next = '#'
689 689 c.next_commit = EmptyCommit()
690 690
691 691 # load file content
692 692 if c.file.is_file():
693 693
694 694 c.lf_node = {}
695 695
696 696 has_lf_enabled = self._is_lf_enabled(self.db_repo)
697 697 if has_lf_enabled:
698 698 c.lf_node = c.file.get_largefile_node()
699 699
700 700 c.file_source_page = 'true'
701 701 c.file_last_commit = c.file.last_commit
702 702
703 703 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
704 704
705 705 if not (c.file_size_too_big or c.file.is_binary):
706 706 if c.annotate: # annotation has precedence over renderer
707 707 c.annotated_lines = filenode_as_annotated_lines_tokens(
708 708 c.file
709 709 )
710 710 else:
711 711 c.renderer = (
712 712 c.renderer and h.renderer_from_filename(c.file.path)
713 713 )
714 714 if not c.renderer:
715 715 c.lines = filenode_as_lines_tokens(c.file)
716 716
717 717 _branch_name, _sha_commit_id, is_head = \
718 718 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
719 719 landing_ref=self.db_repo.landing_ref_name)
720 720 c.on_branch_head = is_head
721 721
722 722 branch = c.commit.branch if (
723 723 c.commit.branch and '/' not in c.commit.branch) else None
724 724 c.branch_or_raw_id = branch or c.commit.raw_id
725 725 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
726 726
727 727 author = c.file_last_commit.author
728 728 c.authors = [[
729 729 h.email(author),
730 730 h.person(author, 'username_or_name_or_email'),
731 731 1
732 732 ]]
733 733
734 734 else: # load tree content at path
735 735 c.file_source_page = 'false'
736 736 c.authors = []
737 737 # this loads a simple tree without metadata to speed things up
738 738 # later via ajax we call repo_nodetree_full and fetch whole
739 739 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
740 740
741 741 c.readme_data, c.readme_file = \
742 742 self._get_readme_data(self.db_repo, c.visual.default_renderer,
743 743 c.commit.raw_id, f_path)
744 744
745 745 except RepositoryError as e:
746 746 h.flash(h.escape(safe_str(e)), category='error')
747 747 raise HTTPNotFound()
748 748
749 749 if self.request.environ.get('HTTP_X_PJAX'):
750 750 html = render('rhodecode:templates/files/files_pjax.mako',
751 751 self._get_template_context(c), self.request)
752 752 else:
753 753 html = render('rhodecode:templates/files/files.mako',
754 754 self._get_template_context(c), self.request)
755 755 return Response(html)
756 756
757 757 @HasRepoPermissionAnyDecorator(
758 758 'repository.read', 'repository.write', 'repository.admin')
759 759 def repo_files_annotated_previous(self):
760 760 self.load_default_context()
761 761
762 762 commit_id, f_path = self._get_commit_and_path()
763 763 commit = self._get_commit_or_redirect(commit_id)
764 764 prev_commit_id = commit.raw_id
765 765 line_anchor = self.request.GET.get('line_anchor')
766 766 is_file = False
767 767 try:
768 768 _file = commit.get_node(f_path)
769 769 is_file = _file.is_file()
770 770 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
771 771 pass
772 772
773 773 if is_file:
774 774 history = commit.get_path_history(f_path)
775 775 prev_commit_id = history[1].raw_id \
776 776 if len(history) > 1 else prev_commit_id
777 777 prev_url = h.route_path(
778 778 'repo_files:annotated', repo_name=self.db_repo_name,
779 779 commit_id=prev_commit_id, f_path=f_path,
780 780 _anchor=f'L{line_anchor}')
781 781
782 782 raise HTTPFound(prev_url)
783 783
784 784 @LoginRequired()
785 785 @HasRepoPermissionAnyDecorator(
786 786 'repository.read', 'repository.write', 'repository.admin')
787 787 def repo_nodetree_full(self):
788 788 """
789 789 Returns rendered html of file tree that contains commit date,
790 790 author, commit_id for the specified combination of
791 791 repo, commit_id and file path
792 792 """
793 793 c = self.load_default_context()
794 794
795 795 commit_id, f_path = self._get_commit_and_path()
796 796 commit = self._get_commit_or_redirect(commit_id)
797 797 try:
798 798 dir_node = commit.get_node(f_path)
799 799 except RepositoryError as e:
800 800 return Response(f'error: {h.escape(safe_str(e))}')
801 801
802 802 if dir_node.is_file():
803 803 return Response('')
804 804
805 805 c.file = dir_node
806 806 c.commit = commit
807 807 at_rev = self.request.GET.get('at')
808 808
809 809 html = self._get_tree_at_commit(
810 810 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
811 811
812 812 return Response(html)
813 813
814 814 def _get_attachement_headers(self, f_path):
815 815 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
816 816 safe_path = f_name.replace('"', '\\"')
817 817 encoded_path = urllib.parse.quote(f_name)
818 818
819 819 headers = "attachment; " \
820 820 "filename=\"{}\"; " \
821 821 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
822 822
823 return safe_bytes(headers).decode('latin-1', errors='replace')
823 return header_safe_str(headers)
824 824
825 825 @LoginRequired()
826 826 @HasRepoPermissionAnyDecorator(
827 827 'repository.read', 'repository.write', 'repository.admin')
828 828 def repo_file_raw(self):
829 829 """
830 830 Action for show as raw, some mimetypes are "rendered",
831 831 those include images, icons.
832 832 """
833 833 c = self.load_default_context()
834 834
835 835 commit_id, f_path = self._get_commit_and_path()
836 836 commit = self._get_commit_or_redirect(commit_id)
837 837 file_node = self._get_filenode_or_redirect(commit, f_path)
838 838
839 839 raw_mimetype_mapping = {
840 840 # map original mimetype to a mimetype used for "show as raw"
841 841 # you can also provide a content-disposition to override the
842 842 # default "attachment" disposition.
843 843 # orig_type: (new_type, new_dispo)
844 844
845 845 # show images inline:
846 846 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
847 847 # for example render an SVG with javascript inside or even render
848 848 # HTML.
849 849 'image/x-icon': ('image/x-icon', 'inline'),
850 850 'image/png': ('image/png', 'inline'),
851 851 'image/gif': ('image/gif', 'inline'),
852 852 'image/jpeg': ('image/jpeg', 'inline'),
853 853 'application/pdf': ('application/pdf', 'inline'),
854 854 }
855 855
856 856 mimetype = file_node.mimetype
857 857 try:
858 858 mimetype, disposition = raw_mimetype_mapping[mimetype]
859 859 except KeyError:
860 860 # we don't know anything special about this, handle it safely
861 861 if file_node.is_binary:
862 862 # do same as download raw for binary files
863 863 mimetype, disposition = 'application/octet-stream', 'attachment'
864 864 else:
865 865 # do not just use the original mimetype, but force text/plain,
866 866 # otherwise it would serve text/html and that might be unsafe.
867 867 # Note: underlying vcs library fakes text/plain mimetype if the
868 868 # mimetype can not be determined and it thinks it is not
869 869 # binary.This might lead to erroneous text display in some
870 870 # cases, but helps in other cases, like with text files
871 871 # without extension.
872 872 mimetype, disposition = 'text/plain', 'inline'
873 873
874 874 if disposition == 'attachment':
875 875 disposition = self._get_attachement_headers(f_path)
876 876
877 877 stream_content = file_node.stream_bytes()
878 878
879 879 response = Response(app_iter=stream_content)
880 880 response.content_disposition = disposition
881 881 response.content_type = mimetype
882 882
883 883 charset = self._get_default_encoding(c)
884 884 if charset:
885 885 response.charset = charset
886 886
887 887 return response
888 888
889 889 @LoginRequired()
890 890 @HasRepoPermissionAnyDecorator(
891 891 'repository.read', 'repository.write', 'repository.admin')
892 892 def repo_file_download(self):
893 893 c = self.load_default_context()
894 894
895 895 commit_id, f_path = self._get_commit_and_path()
896 896 commit = self._get_commit_or_redirect(commit_id)
897 897 file_node = self._get_filenode_or_redirect(commit, f_path)
898 898
899 899 if self.request.GET.get('lf'):
900 900 # only if lf get flag is passed, we download this file
901 901 # as LFS/Largefile
902 902 lf_node = file_node.get_largefile_node()
903 903 if lf_node:
904 904 # overwrite our pointer with the REAL large-file
905 905 file_node = lf_node
906 906
907 907 disposition = self._get_attachement_headers(f_path)
908 908
909 909 stream_content = file_node.stream_bytes()
910 910
911 911 response = Response(app_iter=stream_content)
912 912 response.content_disposition = disposition
913 913 response.content_type = file_node.mimetype
914 914
915 915 charset = self._get_default_encoding(c)
916 916 if charset:
917 917 response.charset = charset
918 918
919 919 return response
920 920
921 921 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
922 922
923 923 cache_seconds = safe_int(
924 924 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
925 925 cache_on = cache_seconds > 0
926 926 log.debug(
927 927 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
928 928 'with caching: %s[TTL: %ss]' % (
929 929 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
930 930
931 931 cache_namespace_uid = f'repo.{repo_id}'
932 932 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
933 933
934 934 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
935 935 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
936 936 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
937 937 _repo_id, commit_id, f_path)
938 938 try:
939 939 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
940 940 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
941 941 log.exception(safe_str(e))
942 942 h.flash(h.escape(safe_str(e)), category='error')
943 943 raise HTTPFound(h.route_path(
944 944 'repo_files', repo_name=self.db_repo_name,
945 945 commit_id='tip', f_path='/'))
946 946
947 947 return _d + _f
948 948
949 949 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
950 950 commit_id, f_path)
951 951 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
952 952
953 953 @LoginRequired()
954 954 @HasRepoPermissionAnyDecorator(
955 955 'repository.read', 'repository.write', 'repository.admin')
956 956 def repo_nodelist(self):
957 957 self.load_default_context()
958 958
959 959 commit_id, f_path = self._get_commit_and_path()
960 960 commit = self._get_commit_or_redirect(commit_id)
961 961
962 962 metadata = self._get_nodelist_at_commit(
963 963 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
964 964 return {'nodes': [x for x in metadata]}
965 965
966 966 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
967 967 items = []
968 968 for name, commit_id in branches_or_tags.items():
969 969 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
970 970 items.append((sym_ref, name, ref_type))
971 971 return items
972 972
973 973 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
974 974 return commit_id
975 975
976 976 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
977 977 return commit_id
978 978
979 979 # NOTE(dan): old code we used in "diff" mode compare
980 980 new_f_path = vcspath.join(name, f_path)
981 981 return f'{new_f_path}@{commit_id}'
982 982
983 983 def _get_node_history(self, commit_obj, f_path, commits=None):
984 984 """
985 985 get commit history for given node
986 986
987 987 :param commit_obj: commit to calculate history
988 988 :param f_path: path for node to calculate history for
989 989 :param commits: if passed don't calculate history and take
990 990 commits defined in this list
991 991 """
992 992 _ = self.request.translate
993 993
994 994 # calculate history based on tip
995 995 tip = self.rhodecode_vcs_repo.get_commit()
996 996 if commits is None:
997 997 pre_load = ["author", "branch"]
998 998 try:
999 999 commits = tip.get_path_history(f_path, pre_load=pre_load)
1000 1000 except (NodeDoesNotExistError, CommitError):
1001 1001 # this node is not present at tip!
1002 1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1003 1003
1004 1004 history = []
1005 1005 commits_group = ([], _("Changesets"))
1006 1006 for commit in commits:
1007 1007 branch = ' (%s)' % commit.branch if commit.branch else ''
1008 1008 n_desc = f'r{commit.idx}:{commit.short_id}{branch}'
1009 1009 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1010 1010 history.append(commits_group)
1011 1011
1012 1012 symbolic_reference = self._symbolic_reference
1013 1013
1014 1014 if self.rhodecode_vcs_repo.alias == 'svn':
1015 1015 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1016 1016 f_path, self.rhodecode_vcs_repo)
1017 1017 if adjusted_f_path != f_path:
1018 1018 log.debug(
1019 1019 'Recognized svn tag or branch in file "%s", using svn '
1020 1020 'specific symbolic references', f_path)
1021 1021 f_path = adjusted_f_path
1022 1022 symbolic_reference = self._symbolic_reference_svn
1023 1023
1024 1024 branches = self._create_references(
1025 1025 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1026 1026 branches_group = (branches, _("Branches"))
1027 1027
1028 1028 tags = self._create_references(
1029 1029 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1030 1030 tags_group = (tags, _("Tags"))
1031 1031
1032 1032 history.append(branches_group)
1033 1033 history.append(tags_group)
1034 1034
1035 1035 return history, commits
1036 1036
1037 1037 @LoginRequired()
1038 1038 @HasRepoPermissionAnyDecorator(
1039 1039 'repository.read', 'repository.write', 'repository.admin')
1040 1040 def repo_file_history(self):
1041 1041 self.load_default_context()
1042 1042
1043 1043 commit_id, f_path = self._get_commit_and_path()
1044 1044 commit = self._get_commit_or_redirect(commit_id)
1045 1045 file_node = self._get_filenode_or_redirect(commit, f_path)
1046 1046
1047 1047 if file_node.is_file():
1048 1048 file_history, _hist = self._get_node_history(commit, f_path)
1049 1049
1050 1050 res = []
1051 1051 for section_items, section in file_history:
1052 1052 items = []
1053 1053 for obj_id, obj_text, obj_type in section_items:
1054 1054 at_rev = ''
1055 1055 if obj_type in ['branch', 'bookmark', 'tag']:
1056 1056 at_rev = obj_text
1057 1057 entry = {
1058 1058 'id': obj_id,
1059 1059 'text': obj_text,
1060 1060 'type': obj_type,
1061 1061 'at_rev': at_rev
1062 1062 }
1063 1063
1064 1064 items.append(entry)
1065 1065
1066 1066 res.append({
1067 1067 'text': section,
1068 1068 'children': items
1069 1069 })
1070 1070
1071 1071 data = {
1072 1072 'more': False,
1073 1073 'results': res
1074 1074 }
1075 1075 return data
1076 1076
1077 1077 log.warning('Cannot fetch history for directory')
1078 1078 raise HTTPBadRequest()
1079 1079
1080 1080 @LoginRequired()
1081 1081 @HasRepoPermissionAnyDecorator(
1082 1082 'repository.read', 'repository.write', 'repository.admin')
1083 1083 def repo_file_authors(self):
1084 1084 c = self.load_default_context()
1085 1085
1086 1086 commit_id, f_path = self._get_commit_and_path()
1087 1087 commit = self._get_commit_or_redirect(commit_id)
1088 1088 file_node = self._get_filenode_or_redirect(commit, f_path)
1089 1089
1090 1090 if not file_node.is_file():
1091 1091 raise HTTPBadRequest()
1092 1092
1093 1093 c.file_last_commit = file_node.last_commit
1094 1094 if self.request.GET.get('annotate') == '1':
1095 1095 # use _hist from annotation if annotation mode is on
1096 1096 commit_ids = {x[1] for x in file_node.annotate}
1097 1097 _hist = (
1098 1098 self.rhodecode_vcs_repo.get_commit(commit_id)
1099 1099 for commit_id in commit_ids)
1100 1100 else:
1101 1101 _f_history, _hist = self._get_node_history(commit, f_path)
1102 1102 c.file_author = False
1103 1103
1104 1104 unique = collections.OrderedDict()
1105 1105 for commit in _hist:
1106 1106 author = commit.author
1107 1107 if author not in unique:
1108 1108 unique[commit.author] = [
1109 1109 h.email(author),
1110 1110 h.person(author, 'username_or_name_or_email'),
1111 1111 1 # counter
1112 1112 ]
1113 1113
1114 1114 else:
1115 1115 # increase counter
1116 1116 unique[commit.author][2] += 1
1117 1117
1118 1118 c.authors = [val for val in unique.values()]
1119 1119
1120 1120 return self._get_template_context(c)
1121 1121
1122 1122 @LoginRequired()
1123 1123 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1124 1124 def repo_files_check_head(self):
1125 1125 self.load_default_context()
1126 1126
1127 1127 commit_id, f_path = self._get_commit_and_path()
1128 1128 _branch_name, _sha_commit_id, is_head = \
1129 1129 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1130 1130 landing_ref=self.db_repo.landing_ref_name)
1131 1131
1132 1132 new_path = self.request.POST.get('path')
1133 1133 operation = self.request.POST.get('operation')
1134 1134 path_exist = ''
1135 1135
1136 1136 if new_path and operation in ['create', 'upload']:
1137 1137 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1138 1138 try:
1139 1139 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1140 1140 # NOTE(dan): construct whole path without leading /
1141 1141 file_node = commit_obj.get_node(new_f_path)
1142 1142 if file_node is not None:
1143 1143 path_exist = new_f_path
1144 1144 except EmptyRepositoryError:
1145 1145 pass
1146 1146 except Exception:
1147 1147 pass
1148 1148
1149 1149 return {
1150 1150 'branch': _branch_name,
1151 1151 'sha': _sha_commit_id,
1152 1152 'is_head': is_head,
1153 1153 'path_exists': path_exist
1154 1154 }
1155 1155
1156 1156 @LoginRequired()
1157 1157 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1158 1158 def repo_files_remove_file(self):
1159 1159 _ = self.request.translate
1160 1160 c = self.load_default_context()
1161 1161 commit_id, f_path = self._get_commit_and_path()
1162 1162
1163 1163 self._ensure_not_locked()
1164 1164 _branch_name, _sha_commit_id, is_head = \
1165 1165 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1166 1166 landing_ref=self.db_repo.landing_ref_name)
1167 1167
1168 1168 self.forbid_non_head(is_head, f_path)
1169 1169 self.check_branch_permission(_branch_name)
1170 1170
1171 1171 c.commit = self._get_commit_or_redirect(commit_id)
1172 1172 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1173 1173
1174 1174 c.default_message = _(
1175 1175 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1176 1176 c.f_path = f_path
1177 1177
1178 1178 return self._get_template_context(c)
1179 1179
1180 1180 @LoginRequired()
1181 1181 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1182 1182 @CSRFRequired()
1183 1183 def repo_files_delete_file(self):
1184 1184 _ = self.request.translate
1185 1185
1186 1186 c = self.load_default_context()
1187 1187 commit_id, f_path = self._get_commit_and_path()
1188 1188
1189 1189 self._ensure_not_locked()
1190 1190 _branch_name, _sha_commit_id, is_head = \
1191 1191 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1192 1192 landing_ref=self.db_repo.landing_ref_name)
1193 1193
1194 1194 self.forbid_non_head(is_head, f_path)
1195 1195 self.check_branch_permission(_branch_name)
1196 1196
1197 1197 c.commit = self._get_commit_or_redirect(commit_id)
1198 1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1199 1199
1200 1200 c.default_message = _(
1201 1201 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1202 1202 c.f_path = f_path
1203 1203 node_path = f_path
1204 1204 author = self._rhodecode_db_user.full_contact
1205 1205 message = self.request.POST.get('message') or c.default_message
1206 1206 try:
1207 1207 nodes = {
1208 1208 safe_bytes(node_path): {
1209 1209 'content': b''
1210 1210 }
1211 1211 }
1212 1212 ScmModel().delete_nodes(
1213 1213 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1214 1214 message=message,
1215 1215 nodes=nodes,
1216 1216 parent_commit=c.commit,
1217 1217 author=author,
1218 1218 )
1219 1219
1220 1220 h.flash(
1221 1221 _('Successfully deleted file `{}`').format(
1222 1222 h.escape(f_path)), category='success')
1223 1223 except Exception:
1224 1224 log.exception('Error during commit operation')
1225 1225 h.flash(_('Error occurred during commit'), category='error')
1226 1226 raise HTTPFound(
1227 1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1228 1228 commit_id='tip'))
1229 1229
1230 1230 @LoginRequired()
1231 1231 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1232 1232 def repo_files_edit_file(self):
1233 1233 _ = self.request.translate
1234 1234 c = self.load_default_context()
1235 1235 commit_id, f_path = self._get_commit_and_path()
1236 1236
1237 1237 self._ensure_not_locked()
1238 1238 _branch_name, _sha_commit_id, is_head = \
1239 1239 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1240 1240 landing_ref=self.db_repo.landing_ref_name)
1241 1241
1242 1242 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1243 1243 self.check_branch_permission(_branch_name, commit_id=commit_id)
1244 1244
1245 1245 c.commit = self._get_commit_or_redirect(commit_id)
1246 1246 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1247 1247
1248 1248 if c.file.is_binary:
1249 1249 files_url = h.route_path(
1250 1250 'repo_files',
1251 1251 repo_name=self.db_repo_name,
1252 1252 commit_id=c.commit.raw_id, f_path=f_path)
1253 1253 raise HTTPFound(files_url)
1254 1254
1255 1255 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1256 1256 c.f_path = f_path
1257 1257
1258 1258 return self._get_template_context(c)
1259 1259
1260 1260 @LoginRequired()
1261 1261 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1262 1262 @CSRFRequired()
1263 1263 def repo_files_update_file(self):
1264 1264 _ = self.request.translate
1265 1265 c = self.load_default_context()
1266 1266 commit_id, f_path = self._get_commit_and_path()
1267 1267
1268 1268 self._ensure_not_locked()
1269 1269
1270 1270 c.commit = self._get_commit_or_redirect(commit_id)
1271 1271 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1272 1272
1273 1273 if c.file.is_binary:
1274 1274 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1275 1275 commit_id=c.commit.raw_id, f_path=f_path))
1276 1276
1277 1277 _branch_name, _sha_commit_id, is_head = \
1278 1278 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1279 1279 landing_ref=self.db_repo.landing_ref_name)
1280 1280
1281 1281 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1282 1282 self.check_branch_permission(_branch_name, commit_id=commit_id)
1283 1283
1284 1284 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1285 1285 c.f_path = f_path
1286 1286
1287 1287 old_content = c.file.str_content
1288 1288 sl = old_content.splitlines(1)
1289 1289 first_line = sl[0] if sl else ''
1290 1290
1291 1291 r_post = self.request.POST
1292 1292 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1293 1293 line_ending_mode = detect_mode(first_line, 0)
1294 1294 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1295 1295
1296 1296 message = r_post.get('message') or c.default_message
1297 1297
1298 1298 org_node_path = c.file.str_path
1299 1299 filename = r_post['filename']
1300 1300
1301 1301 root_path = c.file.dir_path
1302 1302 pure_path = self.create_pure_path(root_path, filename)
1303 1303 node_path = pure_path.as_posix()
1304 1304
1305 1305 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1306 1306 commit_id=commit_id)
1307 1307 if content == old_content and node_path == org_node_path:
1308 1308 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1309 1309 category='warning')
1310 1310 raise HTTPFound(default_redirect_url)
1311 1311
1312 1312 try:
1313 1313 mapping = {
1314 1314 c.file.bytes_path: {
1315 1315 'org_filename': org_node_path,
1316 1316 'filename': safe_bytes(node_path),
1317 1317 'content': safe_bytes(content),
1318 1318 'lexer': '',
1319 1319 'op': 'mod',
1320 1320 'mode': c.file.mode
1321 1321 }
1322 1322 }
1323 1323
1324 1324 commit = ScmModel().update_nodes(
1325 1325 user=self._rhodecode_db_user.user_id,
1326 1326 repo=self.db_repo,
1327 1327 message=message,
1328 1328 nodes=mapping,
1329 1329 parent_commit=c.commit,
1330 1330 )
1331 1331
1332 1332 h.flash(_('Successfully committed changes to file `{}`').format(
1333 1333 h.escape(f_path)), category='success')
1334 1334 default_redirect_url = h.route_path(
1335 1335 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1336 1336
1337 1337 except Exception:
1338 1338 log.exception('Error occurred during commit')
1339 1339 h.flash(_('Error occurred during commit'), category='error')
1340 1340
1341 1341 raise HTTPFound(default_redirect_url)
1342 1342
1343 1343 @LoginRequired()
1344 1344 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1345 1345 def repo_files_add_file(self):
1346 1346 _ = self.request.translate
1347 1347 c = self.load_default_context()
1348 1348 commit_id, f_path = self._get_commit_and_path()
1349 1349
1350 1350 self._ensure_not_locked()
1351 1351
1352 1352 # Check if we need to use this page to upload binary
1353 1353 upload_binary = str2bool(self.request.params.get('upload_binary', False))
1354 1354
1355 1355 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1356 1356 if c.commit is None:
1357 1357 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1358 1358
1359 1359 if self.rhodecode_vcs_repo.is_empty():
1360 1360 # for empty repository we cannot check for current branch, we rely on
1361 1361 # c.commit.branch instead
1362 1362 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1363 1363 else:
1364 1364 _branch_name, _sha_commit_id, is_head = \
1365 1365 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1366 1366 landing_ref=self.db_repo.landing_ref_name)
1367 1367
1368 1368 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1369 1369 self.check_branch_permission(_branch_name, commit_id=commit_id)
1370 1370
1371 1371 c.default_message = (_('Added file via RhodeCode Enterprise')) \
1372 1372 if not upload_binary else (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1373 1373 c.f_path = f_path.lstrip('/') # ensure not relative path
1374 1374 c.replace_binary = upload_binary
1375 1375
1376 1376 return self._get_template_context(c)
1377 1377
1378 1378 @LoginRequired()
1379 1379 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1380 1380 @CSRFRequired()
1381 1381 def repo_files_create_file(self):
1382 1382 _ = self.request.translate
1383 1383 c = self.load_default_context()
1384 1384 commit_id, f_path = self._get_commit_and_path()
1385 1385
1386 1386 self._ensure_not_locked()
1387 1387
1388 1388 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1389 1389 if c.commit is None:
1390 1390 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1391 1391
1392 1392 # calculate redirect URL
1393 1393 if self.rhodecode_vcs_repo.is_empty():
1394 1394 default_redirect_url = h.route_path(
1395 1395 'repo_summary', repo_name=self.db_repo_name)
1396 1396 else:
1397 1397 default_redirect_url = h.route_path(
1398 1398 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1399 1399
1400 1400 if self.rhodecode_vcs_repo.is_empty():
1401 1401 # for empty repository we cannot check for current branch, we rely on
1402 1402 # c.commit.branch instead
1403 1403 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1404 1404 else:
1405 1405 _branch_name, _sha_commit_id, is_head = \
1406 1406 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1407 1407 landing_ref=self.db_repo.landing_ref_name)
1408 1408
1409 1409 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1410 1410 self.check_branch_permission(_branch_name, commit_id=commit_id)
1411 1411
1412 1412 c.default_message = (_('Added file via RhodeCode Enterprise'))
1413 1413 c.f_path = f_path
1414 1414
1415 1415 r_post = self.request.POST
1416 1416 message = r_post.get('message') or c.default_message
1417 1417 filename = r_post.get('filename')
1418 1418 unix_mode = 0
1419 1419
1420 1420 if not filename:
1421 1421 # If there's no commit, redirect to repo summary
1422 1422 if type(c.commit) is EmptyCommit:
1423 1423 redirect_url = h.route_path(
1424 1424 'repo_summary', repo_name=self.db_repo_name)
1425 1425 else:
1426 1426 redirect_url = default_redirect_url
1427 1427 h.flash(_('No filename specified'), category='warning')
1428 1428 raise HTTPFound(redirect_url)
1429 1429
1430 1430 root_path = f_path
1431 1431 pure_path = self.create_pure_path(root_path, filename)
1432 1432 node_path = pure_path.as_posix().lstrip('/')
1433 1433
1434 1434 author = self._rhodecode_db_user.full_contact
1435 1435 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1436 1436 nodes = {
1437 1437 safe_bytes(node_path): {
1438 1438 'content': safe_bytes(content)
1439 1439 }
1440 1440 }
1441 1441
1442 1442 try:
1443 1443
1444 1444 commit = ScmModel().create_nodes(
1445 1445 user=self._rhodecode_db_user.user_id,
1446 1446 repo=self.db_repo,
1447 1447 message=message,
1448 1448 nodes=nodes,
1449 1449 parent_commit=c.commit,
1450 1450 author=author,
1451 1451 )
1452 1452
1453 1453 h.flash(_('Successfully committed new file `{}`').format(
1454 1454 h.escape(node_path)), category='success')
1455 1455
1456 1456 default_redirect_url = h.route_path(
1457 1457 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1458 1458
1459 1459 except NonRelativePathError:
1460 1460 log.exception('Non Relative path found')
1461 1461 h.flash(_('The location specified must be a relative path and must not '
1462 1462 'contain .. in the path'), category='warning')
1463 1463 raise HTTPFound(default_redirect_url)
1464 1464 except (NodeError, NodeAlreadyExistsError) as e:
1465 1465 h.flash(h.escape(safe_str(e)), category='error')
1466 1466 except Exception:
1467 1467 log.exception('Error occurred during commit')
1468 1468 h.flash(_('Error occurred during commit'), category='error')
1469 1469
1470 1470 raise HTTPFound(default_redirect_url)
1471 1471
1472 1472 @LoginRequired()
1473 1473 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1474 1474 @CSRFRequired()
1475 1475 def repo_files_upload_file(self):
1476 1476 _ = self.request.translate
1477 1477 c = self.load_default_context()
1478 1478 commit_id, f_path = self._get_commit_and_path()
1479 1479
1480 1480 self._ensure_not_locked()
1481 1481
1482 1482 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1483 1483 if c.commit is None:
1484 1484 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1485 1485
1486 1486 # calculate redirect URL
1487 1487 if self.rhodecode_vcs_repo.is_empty():
1488 1488 default_redirect_url = h.route_path(
1489 1489 'repo_summary', repo_name=self.db_repo_name)
1490 1490 else:
1491 1491 default_redirect_url = h.route_path(
1492 1492 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1493 1493
1494 1494 if self.rhodecode_vcs_repo.is_empty():
1495 1495 # for empty repository we cannot check for current branch, we rely on
1496 1496 # c.commit.branch instead
1497 1497 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1498 1498 else:
1499 1499 _branch_name, _sha_commit_id, is_head = \
1500 1500 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1501 1501 landing_ref=self.db_repo.landing_ref_name)
1502 1502
1503 1503 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1504 1504 if error:
1505 1505 return {
1506 1506 'error': error,
1507 1507 'redirect_url': default_redirect_url
1508 1508 }
1509 1509 error = self.check_branch_permission(_branch_name, json_mode=True)
1510 1510 if error:
1511 1511 return {
1512 1512 'error': error,
1513 1513 'redirect_url': default_redirect_url
1514 1514 }
1515 1515
1516 1516 c.default_message = (_('Added file via RhodeCode Enterprise'))
1517 1517 c.f_path = f_path
1518 1518
1519 1519 r_post = self.request.POST
1520 1520
1521 1521 message = c.default_message
1522 1522 user_message = r_post.getall('message')
1523 1523 if isinstance(user_message, list) and user_message:
1524 1524 # we take the first from duplicated results if it's not empty
1525 1525 message = user_message[0] if user_message[0] else message
1526 1526
1527 1527 nodes = {}
1528 1528
1529 1529 for file_obj in r_post.getall('files_upload') or []:
1530 1530 content = file_obj.file
1531 1531 filename = file_obj.filename
1532 1532
1533 1533 root_path = f_path
1534 1534 pure_path = self.create_pure_path(root_path, filename)
1535 1535 node_path = pure_path.as_posix().lstrip('/')
1536 1536
1537 1537 nodes[safe_bytes(node_path)] = {
1538 1538 'content': content
1539 1539 }
1540 1540
1541 1541 if not nodes:
1542 1542 error = 'missing files'
1543 1543 return {
1544 1544 'error': error,
1545 1545 'redirect_url': default_redirect_url
1546 1546 }
1547 1547
1548 1548 author = self._rhodecode_db_user.full_contact
1549 1549
1550 1550 try:
1551 1551 commit = ScmModel().create_nodes(
1552 1552 user=self._rhodecode_db_user.user_id,
1553 1553 repo=self.db_repo,
1554 1554 message=message,
1555 1555 nodes=nodes,
1556 1556 parent_commit=c.commit,
1557 1557 author=author,
1558 1558 )
1559 1559 if len(nodes) == 1:
1560 1560 flash_message = _('Successfully committed {} new files').format(len(nodes))
1561 1561 else:
1562 1562 flash_message = _('Successfully committed 1 new file')
1563 1563
1564 1564 h.flash(flash_message, category='success')
1565 1565
1566 1566 default_redirect_url = h.route_path(
1567 1567 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1568 1568
1569 1569 except NonRelativePathError:
1570 1570 log.exception('Non Relative path found')
1571 1571 error = _('The location specified must be a relative path and must not '
1572 1572 'contain .. in the path')
1573 1573 h.flash(error, category='warning')
1574 1574
1575 1575 return {
1576 1576 'error': error,
1577 1577 'redirect_url': default_redirect_url
1578 1578 }
1579 1579 except (NodeError, NodeAlreadyExistsError) as e:
1580 1580 error = h.escape(e)
1581 1581 h.flash(error, category='error')
1582 1582
1583 1583 return {
1584 1584 'error': error,
1585 1585 'redirect_url': default_redirect_url
1586 1586 }
1587 1587 except Exception:
1588 1588 log.exception('Error occurred during commit')
1589 1589 error = _('Error occurred during commit')
1590 1590 h.flash(error, category='error')
1591 1591 return {
1592 1592 'error': error,
1593 1593 'redirect_url': default_redirect_url
1594 1594 }
1595 1595
1596 1596 return {
1597 1597 'error': None,
1598 1598 'redirect_url': default_redirect_url
1599 1599 }
1600 1600
1601 1601 @LoginRequired()
1602 1602 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1603 1603 @CSRFRequired()
1604 1604 def repo_files_replace_file(self):
1605 1605 _ = self.request.translate
1606 1606 c = self.load_default_context()
1607 1607 commit_id, f_path = self._get_commit_and_path()
1608 1608
1609 1609 self._ensure_not_locked()
1610 1610
1611 1611 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1612 1612 if c.commit is None:
1613 1613 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1614 1614
1615 1615 if self.rhodecode_vcs_repo.is_empty():
1616 1616 default_redirect_url = h.route_path(
1617 1617 'repo_summary', repo_name=self.db_repo_name)
1618 1618 else:
1619 1619 default_redirect_url = h.route_path(
1620 1620 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1621 1621
1622 1622 if self.rhodecode_vcs_repo.is_empty():
1623 1623 # for empty repository we cannot check for current branch, we rely on
1624 1624 # c.commit.branch instead
1625 1625 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1626 1626 else:
1627 1627 _branch_name, _sha_commit_id, is_head = \
1628 1628 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1629 1629 landing_ref=self.db_repo.landing_ref_name)
1630 1630
1631 1631 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1632 1632 if error:
1633 1633 return {
1634 1634 'error': error,
1635 1635 'redirect_url': default_redirect_url
1636 1636 }
1637 1637 error = self.check_branch_permission(_branch_name, json_mode=True)
1638 1638 if error:
1639 1639 return {
1640 1640 'error': error,
1641 1641 'redirect_url': default_redirect_url
1642 1642 }
1643 1643
1644 1644 c.default_message = (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1645 1645 c.f_path = f_path
1646 1646
1647 1647 r_post = self.request.POST
1648 1648
1649 1649 message = c.default_message
1650 1650 user_message = r_post.getall('message')
1651 1651 if isinstance(user_message, list) and user_message:
1652 1652 # we take the first from duplicated results if it's not empty
1653 1653 message = user_message[0] if user_message[0] else message
1654 1654
1655 1655 data_for_replacement = r_post.getall('files_upload') or []
1656 1656 if (objects_count := len(data_for_replacement)) > 1:
1657 1657 return {
1658 1658 'error': 'too many files for replacement',
1659 1659 'redirect_url': default_redirect_url
1660 1660 }
1661 1661 elif not objects_count:
1662 1662 return {
1663 1663 'error': 'missing files',
1664 1664 'redirect_url': default_redirect_url
1665 1665 }
1666 1666
1667 1667 content = data_for_replacement[0].file
1668 1668 retrieved_filename = data_for_replacement[0].filename
1669 1669
1670 1670 if retrieved_filename.split('.')[-1] != f_path.split('.')[-1]:
1671 1671 return {
1672 1672 'error': 'file extension of uploaded file doesn\'t match an original file\'s extension',
1673 1673 'redirect_url': default_redirect_url
1674 1674 }
1675 1675
1676 1676 author = self._rhodecode_db_user.full_contact
1677 1677
1678 1678 try:
1679 1679 commit = ScmModel().update_binary_node(
1680 1680 user=self._rhodecode_db_user.user_id,
1681 1681 repo=self.db_repo,
1682 1682 message=message,
1683 1683 node={
1684 1684 'content': content,
1685 1685 'file_path': f_path.encode(),
1686 1686 },
1687 1687 parent_commit=c.commit,
1688 1688 author=author,
1689 1689 )
1690 1690
1691 1691 h.flash(_('Successfully committed 1 new file'), category='success')
1692 1692
1693 1693 default_redirect_url = h.route_path(
1694 1694 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1695 1695
1696 1696 except (NodeError, NodeAlreadyExistsError) as e:
1697 1697 error = h.escape(e)
1698 1698 h.flash(error, category='error')
1699 1699
1700 1700 return {
1701 1701 'error': error,
1702 1702 'redirect_url': default_redirect_url
1703 1703 }
1704 1704 except Exception:
1705 1705 log.exception('Error occurred during commit')
1706 1706 error = _('Error occurred during commit')
1707 1707 h.flash(error, category='error')
1708 1708 return {
1709 1709 'error': error,
1710 1710 'redirect_url': default_redirect_url
1711 1711 }
1712 1712
1713 1713 return {
1714 1714 'error': None,
1715 1715 'redirect_url': default_redirect_url
1716 1716 }
@@ -1,302 +1,309 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21
22 22 from pyramid.httpexceptions import HTTPFound
23 23 from packaging.version import Version
24 24
25 25 from rhodecode import events
26 26 from rhodecode.apps._base import RepoAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
31 31 HasRepoPermissionAny)
32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
32 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError
33 33 from rhodecode.lib.utils2 import safe_int
34 34 from rhodecode.lib.vcs import RepositoryError
35 35 from rhodecode.model.db import Session, UserFollowing, User, Repository
36 36 from rhodecode.model.permission import PermissionModel
37 37 from rhodecode.model.repo import RepoModel
38 38 from rhodecode.model.scm import ScmModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class RepoSettingsAdvancedView(RepoAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 return c
48 48
49 49 def _get_users_with_permissions(self):
50 50 user_permissions = {}
51 51 for perm in self.db_repo.permissions():
52 52 user_permissions[perm.user_id] = perm
53 53
54 54 return user_permissions
55 55
56 56 @LoginRequired()
57 57 @HasRepoPermissionAnyDecorator('repository.admin')
58 58 def edit_advanced(self):
59 59 _ = self.request.translate
60 60 c = self.load_default_context()
61 61 c.active = 'advanced'
62 62
63 63 c.default_user_id = User.get_default_user_id()
64 64 c.in_public_journal = UserFollowing.query() \
65 65 .filter(UserFollowing.user_id == c.default_user_id) \
66 66 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
67 67
68 68 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
69 69 c.hooks_outdated = False
70 70
71 71 try:
72 72 if Version(c.ver_info_dict['pre_version']) < Version(c.rhodecode_version):
73 73 c.hooks_outdated = True
74 74 except Exception:
75 75 pass
76 76
77 77 # update commit cache if GET flag is present
78 78 if self.request.GET.get('update_commit_cache'):
79 79 self.db_repo.update_commit_cache()
80 80 h.flash(_('updated commit cache'), category='success')
81 81
82 82 return self._get_template_context(c)
83 83
84 84 @LoginRequired()
85 85 @HasRepoPermissionAnyDecorator('repository.admin')
86 86 @CSRFRequired()
87 87 def edit_advanced_archive(self):
88 88 """
89 89 Archives the repository. It will become read-only, and not visible in search
90 90 or other queries. But still visible for super-admins.
91 91 """
92 92
93 93 _ = self.request.translate
94 94
95 95 try:
96 96 old_data = self.db_repo.get_api_data()
97 97 RepoModel().archive(self.db_repo)
98 98
99 99 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
100 100 audit_logger.store_web(
101 101 'repo.archive', action_data={'old_data': old_data},
102 102 user=self._rhodecode_user, repo=repo)
103 103
104 104 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
105 105 h.flash(
106 106 _('Archived repository `%s`') % self.db_repo_name,
107 107 category='success')
108 108 Session().commit()
109 109 except Exception:
110 110 log.exception("Exception during archiving of repository")
111 111 h.flash(_('An error occurred during archiving of `%s`')
112 112 % self.db_repo_name, category='error')
113 113 # redirect to advanced for more deletion options
114 114 raise HTTPFound(
115 115 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
116 116 _anchor='advanced-archive'))
117 117
118 118 # flush permissions for all users defined in permissions
119 119 affected_user_ids = self._get_users_with_permissions().keys()
120 120 PermissionModel().trigger_permission_flush(affected_user_ids)
121 121
122 122 raise HTTPFound(h.route_path('home'))
123 123
124 124 @LoginRequired()
125 125 @HasRepoPermissionAnyDecorator('repository.admin')
126 126 @CSRFRequired()
127 127 def edit_advanced_delete(self):
128 128 """
129 129 Deletes the repository, or shows warnings if deletion is not possible
130 130 because of attached forks or other errors.
131 131 """
132 132 _ = self.request.translate
133 133 handle_forks = self.request.POST.get('forks', None)
134 134 if handle_forks == 'detach_forks':
135 135 handle_forks = 'detach'
136 136 elif handle_forks == 'delete_forks':
137 137 handle_forks = 'delete'
138 138
139 repo_advanced_url = h.route_path(
140 'edit_repo_advanced', repo_name=self.db_repo_name,
141 _anchor='advanced-delete')
139 142 try:
140 143 old_data = self.db_repo.get_api_data()
141 144 RepoModel().delete(self.db_repo, forks=handle_forks)
142 145
143 146 _forks = self.db_repo.forks.count()
144 147 if _forks and handle_forks:
145 148 if handle_forks == 'detach_forks':
146 149 h.flash(_('Detached %s forks') % _forks, category='success')
147 150 elif handle_forks == 'delete_forks':
148 151 h.flash(_('Deleted %s forks') % _forks, category='success')
149 152
150 153 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
151 154 audit_logger.store_web(
152 155 'repo.delete', action_data={'old_data': old_data},
153 156 user=self._rhodecode_user, repo=repo)
154 157
155 158 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
156 159 h.flash(
157 160 _('Deleted repository `%s`') % self.db_repo_name,
158 161 category='success')
159 162 Session().commit()
160 163 except AttachedForksError:
161 repo_advanced_url = h.route_path(
162 'edit_repo_advanced', repo_name=self.db_repo_name,
163 _anchor='advanced-delete')
164 164 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
165 165 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
166 166 'Try using {delete_or_detach} option.')
167 167 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
168 168 category='warning')
169 169
170 170 # redirect to advanced for forks handle action ?
171 171 raise HTTPFound(repo_advanced_url)
172 172
173 173 except AttachedPullRequestsError:
174 repo_advanced_url = h.route_path(
175 'edit_repo_advanced', repo_name=self.db_repo_name,
176 _anchor='advanced-delete')
177 174 attached_prs = len(self.db_repo.pull_requests_source +
178 175 self.db_repo.pull_requests_target)
179 176 h.flash(
180 177 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
181 178 'Consider archiving the repository instead.').format(
182 179 repo=self.db_repo_name, num=attached_prs), category='warning')
183 180
184 181 # redirect to advanced for forks handle action ?
185 182 raise HTTPFound(repo_advanced_url)
186 183
184 except AttachedArtifactsError:
185
186 attached_artifacts = len(self.db_repo.artifacts)
187 h.flash(
188 _('Cannot delete `{repo}` it still contains {num} attached artifacts. '
189 'Consider archiving the repository instead.').format(
190 repo=self.db_repo_name, num=attached_artifacts), category='warning')
191
192 # redirect to advanced for forks handle action ?
193 raise HTTPFound(repo_advanced_url)
187 194 except Exception:
188 195 log.exception("Exception during deletion of repository")
189 196 h.flash(_('An error occurred during deletion of `%s`')
190 197 % self.db_repo_name, category='error')
191 198 # redirect to advanced for more deletion options
192 199 raise HTTPFound(
193 200 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
194 201 _anchor='advanced-delete'))
195 202
196 203 raise HTTPFound(h.route_path('home'))
197 204
198 205 @LoginRequired()
199 206 @HasRepoPermissionAnyDecorator('repository.admin')
200 207 @CSRFRequired()
201 208 def edit_advanced_journal(self):
202 209 """
203 210 Set's this repository to be visible in public journal,
204 211 in other words making default user to follow this repo
205 212 """
206 213 _ = self.request.translate
207 214
208 215 try:
209 216 user_id = User.get_default_user_id()
210 217 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
211 218 h.flash(_('Updated repository visibility in public journal'),
212 219 category='success')
213 220 Session().commit()
214 221 except Exception:
215 222 h.flash(_('An error occurred during setting this '
216 223 'repository in public journal'),
217 224 category='error')
218 225
219 226 raise HTTPFound(
220 227 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
221 228
222 229 @LoginRequired()
223 230 @HasRepoPermissionAnyDecorator('repository.admin')
224 231 @CSRFRequired()
225 232 def edit_advanced_fork(self):
226 233 """
227 234 Mark given repository as a fork of another
228 235 """
229 236 _ = self.request.translate
230 237
231 238 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
232 239
233 240 # valid repo, re-check permissions
234 241 if new_fork_id:
235 242 repo = Repository.get(new_fork_id)
236 243 # ensure we have at least read access to the repo we mark
237 244 perm_check = HasRepoPermissionAny(
238 245 'repository.read', 'repository.write', 'repository.admin')
239 246
240 247 if repo and perm_check(repo_name=repo.repo_name):
241 248 new_fork_id = repo.repo_id
242 249 else:
243 250 new_fork_id = None
244 251
245 252 try:
246 253 repo = ScmModel().mark_as_fork(
247 254 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
248 255 fork = repo.fork.repo_name if repo.fork else _('Nothing')
249 256 Session().commit()
250 257 h.flash(
251 258 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
252 259 category='success')
253 260 except RepositoryError as e:
254 261 log.exception("Repository Error occurred")
255 262 h.flash(str(e), category='error')
256 263 except Exception:
257 264 log.exception("Exception while editing fork")
258 265 h.flash(_('An error occurred during this operation'),
259 266 category='error')
260 267
261 268 raise HTTPFound(
262 269 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
263 270
264 271 @LoginRequired()
265 272 @HasRepoPermissionAnyDecorator('repository.admin')
266 273 @CSRFRequired()
267 274 def edit_advanced_toggle_locking(self):
268 275 """
269 276 Toggle locking of repository
270 277 """
271 278 _ = self.request.translate
272 279 set_lock = self.request.POST.get('set_lock')
273 280 set_unlock = self.request.POST.get('set_unlock')
274 281
275 282 try:
276 283 if set_lock:
277 284 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
278 285 lock_reason=Repository.LOCK_WEB)
279 286 h.flash(_('Locked repository'), category='success')
280 287 elif set_unlock:
281 288 Repository.unlock(self.db_repo)
282 289 h.flash(_('Unlocked repository'), category='success')
283 290 except Exception as e:
284 291 log.exception("Exception during unlocking")
285 292 h.flash(_('An error occurred during unlocking'), category='error')
286 293
287 294 raise HTTPFound(
288 295 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
289 296
290 297 @LoginRequired()
291 298 @HasRepoPermissionAnyDecorator('repository.admin')
292 299 def edit_advanced_install_hooks(self):
293 300 """
294 301 Install Hooks for repository
295 302 """
296 303 _ = self.request.translate
297 304 self.load_default_context()
298 305 self.rhodecode_vcs_repo.install_hooks(force=True)
299 306 h.flash(_('installed updated hooks into this repository'),
300 307 category='success')
301 308 raise HTTPFound(
302 309 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,228 +1,228 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import tempfile
21 21 import logging
22 22
23 23 from pyramid.settings import asbool
24 24
25 25 from rhodecode.config.settings_maker import SettingsMaker
26 26 from rhodecode.config import utils as config_utils
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def sanitize_settings_and_apply_defaults(global_config, settings):
32 32 """
33 33 Applies settings defaults and does all type conversion.
34 34
35 35 We would move all settings parsing and preparation into this place, so that
36 36 we have only one place left which deals with this part. The remaining parts
37 37 of the application would start to rely fully on well-prepared settings.
38 38
39 39 This piece would later be split up per topic to avoid a big fat monster
40 40 function.
41 41 """
42 42 jn = os.path.join
43 43
44 44 global_settings_maker = SettingsMaker(global_config)
45 45 global_settings_maker.make_setting('debug', default=False, parser='bool')
46 46 debug_enabled = asbool(global_config.get('debug'))
47 47
48 48 settings_maker = SettingsMaker(settings)
49 49
50 50 settings_maker.make_setting(
51 51 'logging.autoconfigure',
52 52 default=False,
53 53 parser='bool')
54 54
55 55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
56 56 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
57 57
58 58 # Default includes, possible to change as a user
59 59 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
60 60 log.debug(
61 61 "Using the following pyramid.includes: %s",
62 62 pyramid_includes)
63 63
64 64 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
65 65 settings_maker.make_setting('rhodecode.edition_id', 'CE')
66 66
67 67 if 'mako.default_filters' not in settings:
68 68 # set custom default filters if we don't have it defined
69 69 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
70 70 settings['mako.default_filters'] = 'h_filter'
71 71
72 72 if 'mako.directories' not in settings:
73 73 mako_directories = settings.setdefault('mako.directories', [
74 74 # Base templates of the original application
75 75 'rhodecode:templates',
76 76 ])
77 77 log.debug(
78 78 "Using the following Mako template directories: %s",
79 79 mako_directories)
80 80
81 81 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
82 82 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
83 83 raw_url = settings['beaker.session.url']
84 84 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
85 85 settings['beaker.session.url'] = 'redis://' + raw_url
86 86
87 87 settings_maker.make_setting('__file__', global_config.get('__file__'))
88 88
89 89 # TODO: johbo: Re-think this, usually the call to config.include
90 90 # should allow to pass in a prefix.
91 91 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
92 92
93 93 # Sanitize generic settings.
94 94 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
95 95 settings_maker.make_setting('gzip_responses', False, parser='bool')
96 96 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
97 97
98 98 # statsd
99 99 settings_maker.make_setting('statsd.enabled', False, parser='bool')
100 100 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
101 101 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
102 102 settings_maker.make_setting('statsd.statsd_prefix', '')
103 103 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
104 104
105 105 settings_maker.make_setting('vcs.svn.compatible_version', '')
106 106 settings_maker.make_setting('vcs.svn.redis_conn', 'redis://redis:6379/0')
107 107 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
108 108 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
109 109 settings_maker.make_setting('vcs.hooks.protocol.v2', 'celery')
110 110 settings_maker.make_setting('vcs.hooks.host', '*')
111 111 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
112 112 settings_maker.make_setting('vcs.server', '')
113 113 settings_maker.make_setting('vcs.server.protocol', 'http')
114 114 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
115 115 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
116 116 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
117 117 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
118 118 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
119 119
120 120 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
121 121
122 122 # repo_store path
123 123 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
124 124 # Support legacy values of vcs.scm_app_implementation. Legacy
125 125 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
126 126 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
127 127 scm_app_impl = settings['vcs.scm_app_implementation']
128 128 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
129 129 settings['vcs.scm_app_implementation'] = 'http'
130 130
131 131 settings_maker.make_setting('appenlight', False, parser='bool')
132 132
133 133 temp_store = tempfile.gettempdir()
134 134 tmp_cache_dir = jn(temp_store, 'rc_cache')
135 135
136 136 # save default, cache dir, and use it for all backends later.
137 137 default_cache_dir = settings_maker.make_setting(
138 138 'cache_dir',
139 139 default=tmp_cache_dir, default_when_empty=True,
140 140 parser='dir:ensured')
141 141
142 142 # exception store cache
143 143 settings_maker.make_setting(
144 144 'exception_tracker.store_path',
145 145 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
146 146 parser='dir:ensured'
147 147 )
148 148
149 149 settings_maker.make_setting(
150 150 'celerybeat-schedule.path',
151 151 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
152 152 parser='file:ensured'
153 153 )
154 154
155 155 # celery
156 156 broker_url = settings_maker.make_setting('celery.broker_url', 'redis://redis:6379/8')
157 157 settings_maker.make_setting('celery.result_backend', broker_url)
158 158
159 159 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
160 160 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
161 161
162 162 # sessions, ensure file since no-value is memory
163 163 settings_maker.make_setting('beaker.session.type', 'file')
164 164 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
165 165
166 166 # cache_general
167 167 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
168 168 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
169 169 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
170 170
171 171 # cache_perms
172 172 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
173 173 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
174 174 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
175 175
176 176 # cache_repo
177 177 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
178 178 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
179 179 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
180 180
181 181 # cache_license
182 182 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
183 183 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
184 184 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
185 185
186 186 # cache_repo_longterm memory, 96H
187 187 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
188 188 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
189 189 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
190 190
191 191 # sql_cache_short
192 192 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
193 193 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
194 194 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
195 195
196 196 # archive_cache
197 197 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
198 198 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
199 199
200 200 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
201 201 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
202 202 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
203 203 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
204 204
205 205 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
206 206 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
207 207 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
208 208
209 settings_maker.make_setting('archive_cache.objectstore.url', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
209 settings_maker.make_setting('archive_cache.objectstore.url', 'http://s3-minio:9000', default_when_empty=True,)
210 210 settings_maker.make_setting('archive_cache.objectstore.key', '')
211 211 settings_maker.make_setting('archive_cache.objectstore.secret', '')
212 212 settings_maker.make_setting('archive_cache.objectstore.region', 'eu-central-1')
213 213 settings_maker.make_setting('archive_cache.objectstore.bucket', 'rhodecode-archive-cache', default_when_empty=True,)
214 214 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
215 215
216 216 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
217 217 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
218 218
219 219 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
220 220 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
221 221 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
222 222
223 223 settings_maker.env_expand()
224 224
225 225 # configure instance id
226 226 config_utils.set_instance_id(settings)
227 227
228 228 return settings
@@ -1,87 +1,98 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 import os
20 19 import logging
21 20 import rhodecode
22 21 import collections
23 22
24 23 from rhodecode.config import utils
25 24
26 25 from rhodecode.lib.utils import load_rcextensions
27 26 from rhodecode.lib.utils2 import str2bool
28 27 from rhodecode.lib.vcs import connect_vcs
29 28
30 29 log = logging.getLogger(__name__)
31 30
32 31
32 def propagate_rhodecode_config(global_config, settings, config):
33 # Store the settings to make them available to other modules.
34 settings_merged = global_config.copy()
35 settings_merged.update(settings)
36 if config:
37 settings_merged.update(config)
38
39 rhodecode.PYRAMID_SETTINGS = settings_merged
40 rhodecode.CONFIG = settings_merged
41
42 if 'default_user_id' not in rhodecode.CONFIG:
43 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
44 log.debug('set rhodecode.CONFIG data')
45
46
33 47 def load_pyramid_environment(global_config, settings):
34 48 # Some parts of the code expect a merge of global and app settings.
35 49 settings_merged = global_config.copy()
36 50 settings_merged.update(settings)
37 51
38 52 # TODO(marcink): probably not required anymore
39 53 # configure channelstream,
40 54 settings_merged['channelstream_config'] = {
41 55 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
42 56 'server': settings_merged.get('channelstream.server'),
43 57 'secret': settings_merged.get('channelstream.secret')
44 58 }
45 59
46 60 # If this is a test run we prepare the test environment like
47 61 # creating a test database, test search index and test repositories.
48 62 # This has to be done before the database connection is initialized.
49 63 if rhodecode.is_test:
50 64 rhodecode.disable_error_handler = True
51 65 from rhodecode import authentication
52 66 authentication.plugin_default_auth_ttl = 0
53 67
54 68 utils.initialize_test_environment(settings_merged)
55 69
56 70 # Initialize the database connection.
57 71 utils.initialize_database(settings_merged)
58 72
59 73 load_rcextensions(root_path=settings_merged['here'])
60 74
61 75 # Limit backends to `vcs.backends` from configuration, and preserve the order
62 76 for alias in rhodecode.BACKENDS.keys():
63 77 if alias not in settings['vcs.backends']:
64 78 del rhodecode.BACKENDS[alias]
65 79
66 80 _sorted_backend = sorted(rhodecode.BACKENDS.items(),
67 81 key=lambda item: settings['vcs.backends'].index(item[0]))
68 82 rhodecode.BACKENDS = collections.OrderedDict(_sorted_backend)
69 83
70 84 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
71 85
72 86 # initialize vcs client and optionally run the server if enabled
73 87 vcs_server_uri = settings['vcs.server']
74 88 vcs_server_enabled = settings['vcs.server.enable']
75 89
76 90 utils.configure_vcs(settings)
77 91
78 # Store the settings to make them available to other modules.
79
80 rhodecode.PYRAMID_SETTINGS = settings_merged
81 rhodecode.CONFIG = settings_merged
82 rhodecode.CONFIG['default_user_id'] = utils.get_default_user_id()
92 # first run, to store data...
93 propagate_rhodecode_config(global_config, settings, {})
83 94
84 95 if vcs_server_enabled:
85 96 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
86 97 else:
87 98 log.warning('vcs-server not enabled, vcs connection unavailable')
@@ -1,467 +1,470 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import sys
21 21 import collections
22 22
23 23 import time
24 24 import logging.config
25 25
26 26 from paste.gzipper import make_gzip_middleware
27 27 import pyramid.events
28 28 from pyramid.wsgi import wsgiapp
29 29 from pyramid.config import Configurator
30 30 from pyramid.settings import asbool, aslist
31 31 from pyramid.httpexceptions import (
32 32 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
33 33 from pyramid.renderers import render_to_response
34 34
35 35 from rhodecode.model import meta
36 36 from rhodecode.config import patches
37 37
38 38 from rhodecode.config.environment import load_pyramid_environment, propagate_rhodecode_config
39 39
40 40 import rhodecode.events
41 41 from rhodecode.config.config_maker import sanitize_settings_and_apply_defaults
42 42 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 43 from rhodecode.lib.request import Request
44 44 from rhodecode.lib.vcs import VCSCommunicationError
45 45 from rhodecode.lib.exceptions import VCSServerUnavailable
46 46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 47 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 48 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
49 49 from rhodecode.lib.utils2 import AttributeDict
50 50 from rhodecode.lib.exc_tracking import store_exception, format_exc
51 51 from rhodecode.subscribers import (
52 52 scan_repositories_if_enabled, write_js_routes_if_enabled,
53 53 write_metadata_if_needed, write_usage_data)
54 54 from rhodecode.lib.statsd_client import StatsdClient
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def is_http_error(response):
60 60 # error which should have traceback
61 61 return response.status_code > 499
62 62
63 63
64 64 def should_load_all():
65 65 """
66 66 Returns if all application components should be loaded. In some cases it's
67 67 desired to skip apps loading for faster shell script execution
68 68 """
69 69 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
70 70 if ssh_cmd:
71 71 return False
72 72
73 73 return True
74 74
75 75
76 76 def make_pyramid_app(global_config, **settings):
77 77 """
78 78 Constructs the WSGI application based on Pyramid.
79 79
80 80 Specials:
81 81
82 82 * The application can also be integrated like a plugin via the call to
83 83 `includeme`. This is accompanied with the other utility functions which
84 84 are called. Changing this should be done with great care to not break
85 85 cases when these fragments are assembled from another place.
86 86
87 87 """
88 88 start_time = time.time()
89 89 log.info('Pyramid app config starting')
90 90
91 91 sanitize_settings_and_apply_defaults(global_config, settings)
92 92
93 93 # init and bootstrap StatsdClient
94 94 StatsdClient.setup(settings)
95 95
96 96 config = Configurator(settings=settings)
97 97 # Init our statsd at very start
98 98 config.registry.statsd = StatsdClient.statsd
99 99
100 100 # Apply compatibility patches
101 101 patches.inspect_getargspec()
102 102 patches.repoze_sendmail_lf_fix()
103 103
104 104 load_pyramid_environment(global_config, settings)
105 105
106 106 # Static file view comes first
107 107 includeme_first(config)
108 108
109 109 includeme(config)
110 110
111 111 pyramid_app = config.make_wsgi_app()
112 112 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
113 113 pyramid_app.config = config
114 114
115 115 celery_settings = get_celery_config(settings)
116 116 config.configure_celery(celery_settings)
117 117
118 # final config set...
119 propagate_rhodecode_config(global_config, settings, config.registry.settings)
120
118 121 # creating the app uses a connection - return it after we are done
119 122 meta.Session.remove()
120 123
121 124 total_time = time.time() - start_time
122 125 log.info('Pyramid app created and configured in %.2fs', total_time)
123 126 return pyramid_app
124 127
125 128
126 129 def get_celery_config(settings):
127 130 """
128 131 Converts basic ini configuration into celery 4.X options
129 132 """
130 133
131 134 def key_converter(key_name):
132 135 pref = 'celery.'
133 136 if key_name.startswith(pref):
134 137 return key_name[len(pref):].replace('.', '_').lower()
135 138
136 139 def type_converter(parsed_key, value):
137 140 # cast to int
138 141 if value.isdigit():
139 142 return int(value)
140 143
141 144 # cast to bool
142 145 if value.lower() in ['true', 'false', 'True', 'False']:
143 146 return value.lower() == 'true'
144 147 return value
145 148
146 149 celery_config = {}
147 150 for k, v in settings.items():
148 151 pref = 'celery.'
149 152 if k.startswith(pref):
150 153 celery_config[key_converter(k)] = type_converter(key_converter(k), v)
151 154
152 155 # TODO:rethink if we want to support celerybeat based file config, probably NOT
153 156 # beat_config = {}
154 157 # for section in parser.sections():
155 158 # if section.startswith('celerybeat:'):
156 159 # name = section.split(':', 1)[1]
157 160 # beat_config[name] = get_beat_config(parser, section)
158 161
159 162 # final compose of settings
160 163 celery_settings = {}
161 164
162 165 if celery_config:
163 166 celery_settings.update(celery_config)
164 167 # if beat_config:
165 168 # celery_settings.update({'beat_schedule': beat_config})
166 169
167 170 return celery_settings
168 171
169 172
170 173 def not_found_view(request):
171 174 """
172 175 This creates the view which should be registered as not-found-view to
173 176 pyramid.
174 177 """
175 178
176 179 if not getattr(request, 'vcs_call', None):
177 180 # handle like regular case with our error_handler
178 181 return error_handler(HTTPNotFound(), request)
179 182
180 183 # handle not found view as a vcs call
181 184 settings = request.registry.settings
182 185 ae_client = getattr(request, 'ae_client', None)
183 186 vcs_app = VCSMiddleware(
184 187 HTTPNotFound(), request.registry, settings,
185 188 appenlight_client=ae_client)
186 189
187 190 return wsgiapp(vcs_app)(None, request)
188 191
189 192
190 193 def error_handler(exception, request):
191 194 import rhodecode
192 195 from rhodecode.lib import helpers
193 196
194 197 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
195 198
196 199 base_response = HTTPInternalServerError()
197 200 # prefer original exception for the response since it may have headers set
198 201 if isinstance(exception, HTTPException):
199 202 base_response = exception
200 203 elif isinstance(exception, VCSCommunicationError):
201 204 base_response = VCSServerUnavailable()
202 205
203 206 if is_http_error(base_response):
204 207 traceback_info = format_exc(request.exc_info)
205 208 log.error(
206 209 'error occurred handling this request for path: %s, \n%s',
207 210 request.path, traceback_info)
208 211
209 212 error_explanation = base_response.explanation or str(base_response)
210 213 if base_response.status_code == 404:
211 214 error_explanation += " Optionally you don't have permission to access this page."
212 215 c = AttributeDict()
213 216 c.error_message = base_response.status
214 217 c.error_explanation = error_explanation
215 218 c.visual = AttributeDict()
216 219
217 220 c.visual.rhodecode_support_url = (
218 221 request.registry.settings.get('rhodecode_support_url') or
219 222 request.route_url('rhodecode_support')
220 223 )
221 224 c.redirect_time = 0
222 225 c.rhodecode_name = rhodecode_title
223 226 if not c.rhodecode_name:
224 227 c.rhodecode_name = 'Rhodecode'
225 228
226 229 c.causes = []
227 230 if is_http_error(base_response):
228 231 c.causes.append('Server is overloaded.')
229 232 c.causes.append('Server database connection is lost.')
230 233 c.causes.append('Server expected unhandled error.')
231 234
232 235 if hasattr(base_response, 'causes'):
233 236 c.causes = base_response.causes
234 237
235 238 c.messages = helpers.flash.pop_messages(request=request)
236 239 exc_info = sys.exc_info()
237 240 c.exception_id = id(exc_info)
238 241 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
239 242 or base_response.status_code > 499
240 243 c.exception_id_url = request.route_url(
241 244 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
242 245
243 246 debug_mode = rhodecode.ConfigGet().get_bool('debug')
244 247 if c.show_exception_id:
245 248 store_exception(c.exception_id, exc_info)
246 249 c.exception_debug = debug_mode
247 250 c.exception_config_ini = rhodecode.CONFIG.get('__file__')
248 251
249 252 if debug_mode:
250 253 try:
251 254 from rich.traceback import install
252 255 install(show_locals=True)
253 256 log.debug('Installing rich tracebacks...')
254 257 except ImportError:
255 258 pass
256 259
257 260 response = render_to_response(
258 261 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
259 262 response=base_response)
260 263
261 264 response.headers["X-RC-Exception-Id"] = str(c.exception_id)
262 265
263 266 statsd = request.registry.statsd
264 267 if statsd and base_response.status_code > 499:
265 268 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
266 269 statsd.incr('rhodecode_exception_total',
267 270 tags=["exc_source:web",
268 271 f"http_code:{base_response.status_code}",
269 272 f"type:{exc_type}"])
270 273
271 274 return response
272 275
273 276
274 277 def includeme_first(config):
275 278 # redirect automatic browser favicon.ico requests to correct place
276 279 def favicon_redirect(context, request):
277 280 return HTTPFound(
278 281 request.static_path('rhodecode:public/images/favicon.ico'))
279 282
280 283 config.add_view(favicon_redirect, route_name='favicon')
281 284 config.add_route('favicon', '/favicon.ico')
282 285
283 286 def robots_redirect(context, request):
284 287 return HTTPFound(
285 288 request.static_path('rhodecode:public/robots.txt'))
286 289
287 290 config.add_view(robots_redirect, route_name='robots')
288 291 config.add_route('robots', '/robots.txt')
289 292
290 293 config.add_static_view(
291 294 '_static/deform', 'deform:static')
292 295 config.add_static_view(
293 296 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
294 297
295 298
296 299 ce_auth_resources = [
297 300 'rhodecode.authentication.plugins.auth_crowd',
298 301 'rhodecode.authentication.plugins.auth_headers',
299 302 'rhodecode.authentication.plugins.auth_jasig_cas',
300 303 'rhodecode.authentication.plugins.auth_ldap',
301 304 'rhodecode.authentication.plugins.auth_pam',
302 305 'rhodecode.authentication.plugins.auth_rhodecode',
303 306 'rhodecode.authentication.plugins.auth_token',
304 307 ]
305 308
306 309
307 310 def includeme(config, auth_resources=None):
308 311 from rhodecode.lib.celerylib.loader import configure_celery
309 312 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
310 313 settings = config.registry.settings
311 314 config.set_request_factory(Request)
312 315
313 316 # plugin information
314 317 config.registry.rhodecode_plugins = collections.OrderedDict()
315 318
316 319 config.add_directive(
317 320 'register_rhodecode_plugin', register_rhodecode_plugin)
318 321
319 322 config.add_directive('configure_celery', configure_celery)
320 323
321 324 if settings.get('appenlight', False):
322 325 config.include('appenlight_client.ext.pyramid_tween')
323 326
324 327 load_all = should_load_all()
325 328
326 329 # Includes which are required. The application would fail without them.
327 330 config.include('pyramid_mako')
328 331 config.include('rhodecode.lib.rc_beaker')
329 332 config.include('rhodecode.lib.rc_cache')
330 333 config.include('rhodecode.lib.archive_cache')
331 334
332 335 config.include('rhodecode.apps._base.navigation')
333 336 config.include('rhodecode.apps._base.subscribers')
334 337 config.include('rhodecode.tweens')
335 338 config.include('rhodecode.authentication')
336 339
337 340 if load_all:
338 341
339 342 # load CE authentication plugins
340 343
341 344 if auth_resources:
342 345 ce_auth_resources.extend(auth_resources)
343 346
344 347 for resource in ce_auth_resources:
345 348 config.include(resource)
346 349
347 350 # Auto discover authentication plugins and include their configuration.
348 351 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
349 352 from rhodecode.authentication import discover_legacy_plugins
350 353 discover_legacy_plugins(config)
351 354
352 355 # apps
353 356 if load_all:
354 357 log.debug('Starting config.include() calls')
355 358 config.include('rhodecode.api.includeme')
356 359 config.include('rhodecode.apps._base.includeme')
357 360 config.include('rhodecode.apps._base.navigation.includeme')
358 361 config.include('rhodecode.apps._base.subscribers.includeme')
359 362 config.include('rhodecode.apps.hovercards.includeme')
360 363 config.include('rhodecode.apps.ops.includeme')
361 364 config.include('rhodecode.apps.channelstream.includeme')
362 365 config.include('rhodecode.apps.file_store.includeme')
363 366 config.include('rhodecode.apps.admin.includeme')
364 367 config.include('rhodecode.apps.login.includeme')
365 368 config.include('rhodecode.apps.home.includeme')
366 369 config.include('rhodecode.apps.journal.includeme')
367 370
368 371 config.include('rhodecode.apps.repository.includeme')
369 372 config.include('rhodecode.apps.repo_group.includeme')
370 373 config.include('rhodecode.apps.user_group.includeme')
371 374 config.include('rhodecode.apps.search.includeme')
372 375 config.include('rhodecode.apps.user_profile.includeme')
373 376 config.include('rhodecode.apps.user_group_profile.includeme')
374 377 config.include('rhodecode.apps.my_account.includeme')
375 378 config.include('rhodecode.apps.gist.includeme')
376 379
377 380 config.include('rhodecode.apps.svn_support.includeme')
378 381 config.include('rhodecode.apps.ssh_support.includeme')
379 382 config.include('rhodecode.apps.debug_style')
380 383
381 384 if load_all:
382 385 config.include('rhodecode.integrations.includeme')
383 386 config.include('rhodecode.integrations.routes.includeme')
384 387
385 388 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
386 389 settings['default_locale_name'] = settings.get('lang', 'en')
387 390 config.add_translation_dirs('rhodecode:i18n/')
388 391
389 392 # Add subscribers.
390 393 if load_all:
391 394 log.debug('Adding subscribers...')
392 395 config.add_subscriber(scan_repositories_if_enabled,
393 396 pyramid.events.ApplicationCreated)
394 397 config.add_subscriber(write_metadata_if_needed,
395 398 pyramid.events.ApplicationCreated)
396 399 config.add_subscriber(write_usage_data,
397 400 pyramid.events.ApplicationCreated)
398 401 config.add_subscriber(write_js_routes_if_enabled,
399 402 pyramid.events.ApplicationCreated)
400 403
401 404
402 405 # Set the default renderer for HTML templates to mako.
403 406 config.add_mako_renderer('.html')
404 407
405 408 config.add_renderer(
406 409 name='json_ext',
407 410 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
408 411
409 412 config.add_renderer(
410 413 name='string_html',
411 414 factory='rhodecode.lib.string_renderer.html')
412 415
413 416 # include RhodeCode plugins
414 417 includes = aslist(settings.get('rhodecode.includes', []))
415 418 log.debug('processing rhodecode.includes data...')
416 419 for inc in includes:
417 420 config.include(inc)
418 421
419 422 # custom not found view, if our pyramid app doesn't know how to handle
420 423 # the request pass it to potential VCS handling ap
421 424 config.add_notfound_view(not_found_view)
422 425 if not settings.get('debugtoolbar.enabled', False):
423 426 # disabled debugtoolbar handle all exceptions via the error_handlers
424 427 config.add_view(error_handler, context=Exception)
425 428
426 429 # all errors including 403/404/50X
427 430 config.add_view(error_handler, context=HTTPError)
428 431
429 432
430 433 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
431 434 """
432 435 Apply outer WSGI middlewares around the application.
433 436 """
434 437 registry = config.registry
435 438 settings = registry.settings
436 439
437 440 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
438 441 pyramid_app = HttpsFixup(pyramid_app, settings)
439 442
440 443 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
441 444 pyramid_app, settings)
442 445 registry.ae_client = _ae_client
443 446
444 447 if settings['gzip_responses']:
445 448 pyramid_app = make_gzip_middleware(
446 449 pyramid_app, settings, compress_level=1)
447 450
448 451 # this should be the outer most middleware in the wsgi stack since
449 452 # middleware like Routes make database calls
450 453 def pyramid_app_with_cleanup(environ, start_response):
451 454 start = time.time()
452 455 try:
453 456 return pyramid_app(environ, start_response)
454 457 finally:
455 458 # Dispose current database session and rollback uncommitted
456 459 # transactions.
457 460 meta.Session.remove()
458 461
459 462 # In a single threaded mode server, on non sqlite db we should have
460 463 # '0 Current Checked out connections' at the end of a request,
461 464 # if not, then something, somewhere is leaving a connection open
462 465 pool = meta.get_engine().pool
463 466 log.debug('sa pool status: %s', pool.status())
464 467 total = time.time() - start
465 468 log.debug('Request processing finalized: %.4fs', total)
466 469
467 470 return pyramid_app_with_cleanup
@@ -1,205 +1,209 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Set of custom exceptions used in RhodeCode
21 21 """
22 22
23 23 from webob.exc import HTTPClientError
24 24 from pyramid.httpexceptions import HTTPBadGateway
25 25
26 26
27 27 class LdapUsernameError(Exception):
28 28 pass
29 29
30 30
31 31 class LdapPasswordError(Exception):
32 32 pass
33 33
34 34
35 35 class LdapConnectionError(Exception):
36 36 pass
37 37
38 38
39 39 class LdapImportError(Exception):
40 40 pass
41 41
42 42
43 43 class DefaultUserException(Exception):
44 44 pass
45 45
46 46
47 47 class UserOwnsReposException(Exception):
48 48 pass
49 49
50 50
51 51 class UserOwnsRepoGroupsException(Exception):
52 52 pass
53 53
54 54
55 55 class UserOwnsUserGroupsException(Exception):
56 56 pass
57 57
58 58
59 59 class UserOwnsPullRequestsException(Exception):
60 60 pass
61 61
62 62
63 63 class UserOwnsArtifactsException(Exception):
64 64 pass
65 65
66 66
67 67 class UserGroupAssignedException(Exception):
68 68 pass
69 69
70 70
71 71 class StatusChangeOnClosedPullRequestError(Exception):
72 72 pass
73 73
74 74
75 75 class AttachedForksError(Exception):
76 76 pass
77 77
78 78
79 79 class AttachedPullRequestsError(Exception):
80 80 pass
81 81
82 82
83 class AttachedArtifactsError(Exception):
84 pass
85
86
83 87 class RepoGroupAssignmentError(Exception):
84 88 pass
85 89
86 90
87 91 class NonRelativePathError(Exception):
88 92 pass
89 93
90 94
91 95 class HTTPRequirementError(HTTPClientError):
92 96 title = explanation = 'Repository Requirement Missing'
93 97 reason = None
94 98
95 99 def __init__(self, message, *args, **kwargs):
96 100 self.title = self.explanation = message
97 101 super().__init__(*args, **kwargs)
98 102 self.args = (message, )
99 103
100 104
101 105 class HTTPLockedRC(HTTPClientError):
102 106 """
103 107 Special Exception For locked Repos in RhodeCode, the return code can
104 108 be overwritten by _code keyword argument passed into constructors
105 109 """
106 110 code = 423
107 111 title = explanation = 'Repository Locked'
108 112 reason = None
109 113
110 114 def __init__(self, message, *args, **kwargs):
111 115 import rhodecode
112 116
113 117 self.code = rhodecode.ConfigGet().get_int('lock_ret_code', missing=self.code)
114 118
115 119 self.title = self.explanation = message
116 120 super().__init__(*args, **kwargs)
117 121 self.args = (message, )
118 122
119 123
120 124 class HTTPBranchProtected(HTTPClientError):
121 125 """
122 126 Special Exception For Indicating that branch is protected in RhodeCode, the
123 127 return code can be overwritten by _code keyword argument passed into constructors
124 128 """
125 129 code = 403
126 130 title = explanation = 'Branch Protected'
127 131 reason = None
128 132
129 133 def __init__(self, message, *args, **kwargs):
130 134 self.title = self.explanation = message
131 135 super().__init__(*args, **kwargs)
132 136 self.args = (message, )
133 137
134 138
135 139 class IMCCommitError(Exception):
136 140 pass
137 141
138 142
139 143 class UserCreationError(Exception):
140 144 pass
141 145
142 146
143 147 class NotAllowedToCreateUserError(Exception):
144 148 pass
145 149
146 150
147 151 class DuplicateUpdateUserError(Exception):
148 152 pass
149 153
150 154
151 155 class RepositoryCreationError(Exception):
152 156 pass
153 157
154 158
155 159 class VCSServerUnavailable(HTTPBadGateway):
156 160 """ HTTP Exception class for VCS Server errors """
157 161 code = 502
158 162 title = 'VCS Server Error'
159 163 causes = [
160 164 'VCS Server is not running',
161 165 'Incorrect vcs.server=host:port',
162 166 'Incorrect vcs.server.protocol',
163 167 ]
164 168
165 169 def __init__(self, message=''):
166 170 self.explanation = 'Could not connect to VCS Server'
167 171 if message:
168 172 self.explanation += ': ' + message
169 173 super().__init__()
170 174
171 175
172 176 class ArtifactMetadataDuplicate(ValueError):
173 177
174 178 def __init__(self, *args, **kwargs):
175 179 self.err_section = kwargs.pop('err_section', None)
176 180 self.err_key = kwargs.pop('err_key', None)
177 181 super().__init__(*args, **kwargs)
178 182
179 183
180 184 class ArtifactMetadataBadValueType(ValueError):
181 185 pass
182 186
183 187
184 188 class CommentVersionMismatch(ValueError):
185 189 pass
186 190
187 191
188 192 class SignatureVerificationError(ValueError):
189 193 pass
190 194
191 195
192 196 def signature_verification_error(msg):
193 197 details = """
194 198 Encryption signature verification failed.
195 199 Please check your value of secret key, and/or encrypted value stored.
196 200 Secret key stored inside .ini file:
197 201 `rhodecode.encrypted_values.secret` or defaults to
198 202 `beaker.session.secret`
199 203
200 204 Probably the stored values were encrypted using a different secret then currently set in .ini file
201 205 """
202 206
203 207 final_msg = f'{msg}\n{details}'
204 208 return SignatureVerificationError(final_msg)
205 209
@@ -1,2230 +1,2182 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Helper functions
21 21
22 22 Consists of functions to typically be used within templates, but also
23 23 available to Controllers. This module is available to both as 'h'.
24 24 """
25 25 import base64
26 26 import collections
27 27
28 28 import os
29 29 import random
30 30 import hashlib
31 31 import io
32 32 import textwrap
33 33 import urllib.request
34 34 import urllib.parse
35 35 import urllib.error
36 36 import math
37 37 import logging
38 38 import re
39 39 import time
40 40 import string
41 41 import regex
42 42 from collections import OrderedDict
43 43
44 44 import pygments
45 45 import itertools
46 46 import fnmatch
47 47
48 48 from datetime import datetime
49 49 from functools import partial
50 50 from pygments.formatters.html import HtmlFormatter
51 51 from pygments.lexers import (
52 52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 53
54 54 from pyramid.threadlocal import get_current_request
55 55 from tempita import looper
56 56 from webhelpers2.html import literal, HTML, escape
57 57 from webhelpers2.html._autolink import _auto_link_urls
58 58 from webhelpers2.html.tools import (
59 59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 60
61 61 from webhelpers2.text import (
62 62 chop_at, collapse, convert_accented_entities,
63 63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 65 from webhelpers2.date import time_ago_in_words
66 66
67 67 from webhelpers2.html.tags import (
68 68 _input, NotGiven, _make_safe_id_component as safeid,
69 69 form as insecure_form,
70 70 auto_discovery_link, checkbox, end_form, file,
71 71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 72 stylesheet_link, submit, text, password, textarea,
73 73 ul, radio, Options)
74 74
75 75 from webhelpers2.number import format_byte_size
76 76 # python3.11 backport fixes for webhelpers2
77 77 from rhodecode import ConfigGet
78 78 from rhodecode.lib._vendor.webhelpers_backports import raw_select
79 79
80 80 from rhodecode.lib.action_parser import action_parser
81 81 from rhodecode.lib.html_filters import sanitize_html
82 82 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
83 83 from rhodecode.lib import ext_json
84 from rhodecode.lib.ext_json import json
84 from rhodecode.lib.ext_json import json, formatted_str_json
85 85 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars, base64_to_str
86 86 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
87 87 from rhodecode.lib.str_utils import safe_str
88 88 from rhodecode.lib.utils2 import (
89 89 str2bool,
90 90 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
91 91 AttributeDict, safe_int, md5, md5_safe, get_host_info)
92 92 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
93 93 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
94 94 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
95 95 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
96 96 from rhodecode.lib.index.search_utils import get_matching_line_offsets
97 97 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
98 98 from rhodecode.model.changeset_status import ChangesetStatusModel
99 99 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
100 100 from rhodecode.model.repo_group import RepoGroupModel
101 101 from rhodecode.model.settings import IssueTrackerSettingsModel
102 102
103 103
104 104 log = logging.getLogger(__name__)
105 105
106 106
107 107 DEFAULT_USER = User.DEFAULT_USER
108 108 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
109 109
110 110
111 111 def asset(path, ver=None, **kwargs):
112 112 """
113 113 Helper to generate a static asset file path for rhodecode assets
114 114
115 115 eg. h.asset('images/image.png', ver='3923')
116 116
117 117 :param path: path of asset
118 118 :param ver: optional version query param to append as ?ver=
119 119 """
120 120 request = get_current_request()
121 121 query = {}
122 122 query.update(kwargs)
123 123 if ver:
124 124 query = {'ver': ver}
125 125 return request.static_path(
126 126 f'rhodecode:public/{path}', _query=query)
127 127
128 128
129 129 default_html_escape_table = {
130 130 ord('&'): '&amp;',
131 131 ord('<'): '&lt;',
132 132 ord('>'): '&gt;',
133 133 ord('"'): '&quot;',
134 134 ord("'"): '&#39;',
135 135 }
136 136
137 137
138 138 def html_escape(text, html_escape_table=default_html_escape_table):
139 139 """Produce entities within text."""
140 140 return text.translate(html_escape_table)
141 141
142 142
143 143 def str_json(*args, **kwargs):
144 144 return ext_json.str_json(*args, **kwargs)
145 145
146 146
147 147 def formatted_str_json(*args, **kwargs):
148 148 return ext_json.formatted_str_json(*args, **kwargs)
149 149
150 150
151 151 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
152 152 """
153 153 Truncate string ``s`` at the first occurrence of ``sub``.
154 154
155 155 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
156 156 """
157 157 suffix_if_chopped = suffix_if_chopped or ''
158 158 pos = s.find(sub)
159 159 if pos == -1:
160 160 return s
161 161
162 162 if inclusive:
163 163 pos += len(sub)
164 164
165 165 chopped = s[:pos]
166 166 left = s[pos:].strip()
167 167
168 168 if left and suffix_if_chopped:
169 169 chopped += suffix_if_chopped
170 170
171 171 return chopped
172 172
173 173
174 174 def shorter(text, size=20, prefix=False):
175 175 postfix = '...'
176 176 if len(text) > size:
177 177 if prefix:
178 178 # shorten in front
179 179 return postfix + text[-(size - len(postfix)):]
180 180 else:
181 181 return text[:size - len(postfix)] + postfix
182 182 return text
183 183
184 184
185 185 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
186 186 """
187 187 Reset button
188 188 """
189 189 return _input(type, name, value, id, attrs)
190 190
191 191
192 192 def select(name, selected_values, options, id=NotGiven, **attrs):
193 193
194 194 if isinstance(options, (list, tuple)):
195 195 options_iter = options
196 196 # Handle old value,label lists ... where value also can be value,label lists
197 197 options = Options()
198 198 for opt in options_iter:
199 199 if isinstance(opt, tuple) and len(opt) == 2:
200 200 value, label = opt
201 201 elif isinstance(opt, str):
202 202 value = label = opt
203 203 else:
204 204 raise ValueError('invalid select option type %r' % type(opt))
205 205
206 206 if isinstance(value, (list, tuple)):
207 207 option_group = options.add_optgroup(label)
208 208 for opt2 in value:
209 209 if isinstance(opt2, tuple) and len(opt2) == 2:
210 210 group_value, group_label = opt2
211 211 elif isinstance(opt2, str):
212 212 group_value = group_label = opt2
213 213 else:
214 214 raise ValueError('invalid select option type %r' % type(opt2))
215 215
216 216 option_group.add_option(group_label, group_value)
217 217 else:
218 218 options.add_option(label, value)
219 219
220 220 return raw_select(name, selected_values, options, id=id, **attrs)
221 221
222 222
223 223 def branding(name, length=40):
224 224 return truncate(name, length, indicator="")
225 225
226 226
227 227 def FID(raw_id, path):
228 228 """
229 229 Creates a unique ID for filenode based on it's hash of path and commit
230 230 it's safe to use in urls
231 231
232 232 :param raw_id:
233 233 :param path:
234 234 """
235 235
236 236 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
237 237
238 238
239 239 class _GetError(object):
240 240 """Get error from form_errors, and represent it as span wrapped error
241 241 message
242 242
243 243 :param field_name: field to fetch errors for
244 244 :param form_errors: form errors dict
245 245 """
246 246
247 247 def __call__(self, field_name, form_errors):
248 248 tmpl = """<span class="error_msg">%s</span>"""
249 249 if form_errors and field_name in form_errors:
250 250 return literal(tmpl % form_errors.get(field_name))
251 251
252 252
253 253 get_error = _GetError()
254 254
255 255
256 256 class _ToolTip(object):
257 257
258 258 def __call__(self, tooltip_title, trim_at=50):
259 259 """
260 260 Special function just to wrap our text into nice formatted
261 261 autowrapped text
262 262
263 263 :param tooltip_title:
264 264 """
265 265 tooltip_title = escape(tooltip_title)
266 266 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
267 267 return tooltip_title
268 268
269 269
270 270 tooltip = _ToolTip()
271 271
272 272 files_icon = '<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
273 273
274 274
275 275 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
276 276 limit_items=False, linkify_last_item=False, hide_last_item=False,
277 277 copy_path_icon=True):
278 278
279 279 if at_ref:
280 280 route_qry = {'at': at_ref}
281 281 default_landing_ref = at_ref or landing_ref_name or commit_id
282 282 else:
283 283 route_qry = None
284 284 default_landing_ref = commit_id
285 285
286 286 # first segment is a `HOME` link to repo files root location
287 287 root_name = literal('<i class="icon-home"></i>')
288 288
289 289 url_segments = [
290 290 link_to(
291 291 root_name,
292 292 repo_files_by_ref_url(
293 293 repo_name,
294 294 repo_type,
295 295 f_path=None, # None here is a special case for SVN repos,
296 296 # that won't prefix with a ref
297 297 ref_name=default_landing_ref,
298 298 commit_id=commit_id,
299 299 query=route_qry
300 300 )
301 301 )]
302 302
303 303 path_segments = file_path.split('/')
304 304 last_cnt = len(path_segments) - 1
305 305 for cnt, segment in enumerate(path_segments):
306 306 if not segment:
307 307 continue
308 308 segment_html = escape(segment)
309 309
310 310 last_item = cnt == last_cnt
311 311
312 312 if last_item and hide_last_item:
313 313 # iterate over and hide last element
314 314 continue
315 315
316 316 if last_item and linkify_last_item is False:
317 317 # plain version
318 318 url_segments.append(segment_html)
319 319 else:
320 320 url_segments.append(
321 321 link_to(
322 322 segment_html,
323 323 repo_files_by_ref_url(
324 324 repo_name,
325 325 repo_type,
326 326 f_path='/'.join(path_segments[:cnt + 1]),
327 327 ref_name=default_landing_ref,
328 328 commit_id=commit_id,
329 329 query=route_qry
330 330 ),
331 331 ))
332 332
333 333 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
334 334 if limit_items and len(limited_url_segments) < len(url_segments):
335 335 url_segments = limited_url_segments
336 336
337 337 full_path = file_path
338 338 if copy_path_icon:
339 339 icon = files_icon.format(escape(full_path))
340 340 else:
341 341 icon = ''
342 342
343 343 if file_path == '':
344 344 return root_name
345 345 else:
346 346 return literal(' / '.join(url_segments) + icon)
347 347
348 348
349 349 def files_url_data(request):
350 350 matchdict = request.matchdict
351 351
352 352 if 'f_path' not in matchdict:
353 353 matchdict['f_path'] = ''
354 354 else:
355 355 matchdict['f_path'] = urllib.parse.quote(safe_str(matchdict['f_path']))
356 356 if 'commit_id' not in matchdict:
357 357 matchdict['commit_id'] = 'tip'
358 358
359 359 return ext_json.str_json(matchdict)
360 360
361 361
362 362 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
363 363 _is_svn = is_svn(db_repo_type)
364 364 final_f_path = f_path
365 365
366 366 if _is_svn:
367 367 """
368 368 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
369 369 actually commit_id followed by the ref_name. This should be done only in case
370 370 This is a initial landing url, without additional paths.
371 371
372 372 like: /1000/tags/1.0.0/?at=tags/1.0.0
373 373 """
374 374
375 375 if ref_name and ref_name != 'tip':
376 376 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
377 377 # for SVN we only do this magic prefix if it's root, .eg landing revision
378 378 # of files link. If we are in the tree we don't need this since we traverse the url
379 379 # that has everything stored
380 380 if f_path in ['', '/']:
381 381 final_f_path = '/'.join([ref_name, f_path])
382 382
383 383 # SVN always needs a commit_id explicitly, without a named REF
384 384 default_commit_id = commit_id
385 385 else:
386 386 """
387 387 For git and mercurial we construct a new URL using the names instead of commit_id
388 388 like: /master/some_path?at=master
389 389 """
390 390 # We currently do not support branches with slashes
391 391 if '/' in ref_name:
392 392 default_commit_id = commit_id
393 393 else:
394 394 default_commit_id = ref_name
395 395
396 396 # sometimes we pass f_path as None, to indicate explicit no prefix,
397 397 # we translate it to string to not have None
398 398 final_f_path = final_f_path or ''
399 399
400 400 files_url = route_path(
401 401 'repo_files',
402 402 repo_name=db_repo_name,
403 403 commit_id=default_commit_id,
404 404 f_path=final_f_path,
405 405 _query=query
406 406 )
407 407 return files_url
408 408
409 409
410 410 def code_highlight(code, lexer, formatter, use_hl_filter=False):
411 411 """
412 412 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
413 413
414 414 If ``outfile`` is given and a valid file object (an object
415 415 with a ``write`` method), the result will be written to it, otherwise
416 416 it is returned as a string.
417 417 """
418 418 if use_hl_filter:
419 419 # add HL filter
420 420 from rhodecode.lib.index import search_utils
421 421 lexer.add_filter(search_utils.ElasticSearchHLFilter())
422 422 return pygments.format(pygments.lex(code, lexer), formatter)
423 423
424 424
425 425 class CodeHtmlFormatter(HtmlFormatter):
426 426 """
427 427 My code Html Formatter for source codes
428 428 """
429 429
430 430 def wrap(self, source):
431 431 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
432 432
433 433 def _wrap_code(self, source):
434 434 for cnt, it in enumerate(source):
435 435 i, t = it
436 436 t = f'<div id="L{cnt+1}">{t}</div>'
437 437 yield i, t
438 438
439 439 def _wrap_tablelinenos(self, inner):
440 440 dummyoutfile = io.StringIO()
441 441 lncount = 0
442 442 for t, line in inner:
443 443 if t:
444 444 lncount += 1
445 445 dummyoutfile.write(line)
446 446
447 447 fl = self.linenostart
448 448 mw = len(str(lncount + fl - 1))
449 449 sp = self.linenospecial
450 450 st = self.linenostep
451 451 la = self.lineanchors
452 452 aln = self.anchorlinenos
453 453 nocls = self.noclasses
454 454 if sp:
455 455 lines = []
456 456
457 457 for i in range(fl, fl + lncount):
458 458 if i % st == 0:
459 459 if i % sp == 0:
460 460 if aln:
461 461 lines.append('<a href="#%s%d" class="special">%*d</a>' %
462 462 (la, i, mw, i))
463 463 else:
464 464 lines.append('<span class="special">%*d</span>' % (mw, i))
465 465 else:
466 466 if aln:
467 467 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
468 468 else:
469 469 lines.append('%*d' % (mw, i))
470 470 else:
471 471 lines.append('')
472 472 ls = '\n'.join(lines)
473 473 else:
474 474 lines = []
475 475 for i in range(fl, fl + lncount):
476 476 if i % st == 0:
477 477 if aln:
478 478 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
479 479 else:
480 480 lines.append('%*d' % (mw, i))
481 481 else:
482 482 lines.append('')
483 483 ls = '\n'.join(lines)
484 484
485 485 # in case you wonder about the seemingly redundant <div> here: since the
486 486 # content in the other cell also is wrapped in a div, some browsers in
487 487 # some configurations seem to mess up the formatting...
488 488 if nocls:
489 489 yield 0, ('<table class="%stable">' % self.cssclass +
490 490 '<tr><td><div class="linenodiv" '
491 491 'style="background-color: #f0f0f0; padding-right: 10px">'
492 492 '<pre style="line-height: 125%">' +
493 493 ls + '</pre></div></td><td id="hlcode" class="code">')
494 494 else:
495 495 yield 0, ('<table class="%stable">' % self.cssclass +
496 496 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
497 497 ls + '</pre></div></td><td id="hlcode" class="code">')
498 498 yield 0, dummyoutfile.getvalue()
499 499 yield 0, '</td></tr></table>'
500 500
501 501
502 502 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
503 503 def __init__(self, **kw):
504 504 # only show these line numbers if set
505 505 self.only_lines = kw.pop('only_line_numbers', [])
506 506 self.query_terms = kw.pop('query_terms', [])
507 507 self.max_lines = kw.pop('max_lines', 5)
508 508 self.line_context = kw.pop('line_context', 3)
509 509 self.url = kw.pop('url', None)
510 510
511 511 super(CodeHtmlFormatter, self).__init__(**kw)
512 512
513 513 def _wrap_code(self, source):
514 514 for cnt, it in enumerate(source):
515 515 i, t = it
516 516 t = '<pre>%s</pre>' % t
517 517 yield i, t
518 518
519 519 def _wrap_tablelinenos(self, inner):
520 520 yield 0, '<table class="code-highlight %stable">' % self.cssclass
521 521
522 522 last_shown_line_number = 0
523 523 current_line_number = 1
524 524
525 525 for t, line in inner:
526 526 if not t:
527 527 yield t, line
528 528 continue
529 529
530 530 if current_line_number in self.only_lines:
531 531 if last_shown_line_number + 1 != current_line_number:
532 532 yield 0, '<tr>'
533 533 yield 0, '<td class="line">...</td>'
534 534 yield 0, '<td id="hlcode" class="code"></td>'
535 535 yield 0, '</tr>'
536 536
537 537 yield 0, '<tr>'
538 538 if self.url:
539 539 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
540 540 self.url, current_line_number, current_line_number)
541 541 else:
542 542 yield 0, '<td class="line"><a href="">%i</a></td>' % (
543 543 current_line_number)
544 544 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
545 545 yield 0, '</tr>'
546 546
547 547 last_shown_line_number = current_line_number
548 548
549 549 current_line_number += 1
550 550
551 551 yield 0, '</table>'
552 552
553 553
554 554 def hsv_to_rgb(h, s, v):
555 555 """ Convert hsv color values to rgb """
556 556
557 557 if s == 0.0:
558 558 return v, v, v
559 559 i = int(h * 6.0) # XXX assume int() truncates!
560 560 f = (h * 6.0) - i
561 561 p = v * (1.0 - s)
562 562 q = v * (1.0 - s * f)
563 563 t = v * (1.0 - s * (1.0 - f))
564 564 i = i % 6
565 565 if i == 0:
566 566 return v, t, p
567 567 if i == 1:
568 568 return q, v, p
569 569 if i == 2:
570 570 return p, v, t
571 571 if i == 3:
572 572 return p, q, v
573 573 if i == 4:
574 574 return t, p, v
575 575 if i == 5:
576 576 return v, p, q
577 577
578 578
579 579 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
580 580 """
581 581 Generator for getting n of evenly distributed colors using
582 582 hsv color and golden ratio. It always return same order of colors
583 583
584 584 :param n: number of colors to generate
585 585 :param saturation: saturation of returned colors
586 586 :param lightness: lightness of returned colors
587 587 :returns: RGB tuple
588 588 """
589 589
590 590 golden_ratio = 0.618033988749895
591 591 h = 0.22717784590367374
592 592
593 593 for _ in range(n):
594 594 h += golden_ratio
595 595 h %= 1
596 596 HSV_tuple = [h, saturation, lightness]
597 597 RGB_tuple = hsv_to_rgb(*HSV_tuple)
598 598 yield [str(int(x * 256)) for x in RGB_tuple]
599 599
600 600
601 601 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
602 602 """
603 603 Returns a function which when called with an argument returns a unique
604 604 color for that argument, eg.
605 605
606 606 :param n: number of colors to generate
607 607 :param saturation: saturation of returned colors
608 608 :param lightness: lightness of returned colors
609 609 :returns: css RGB string
610 610
611 611 >>> color_hash = color_hasher()
612 612 >>> color_hash('hello')
613 613 'rgb(34, 12, 59)'
614 614 >>> color_hash('hello')
615 615 'rgb(34, 12, 59)'
616 616 >>> color_hash('other')
617 617 'rgb(90, 224, 159)'
618 618 """
619 619
620 620 color_dict = {}
621 621 cgenerator = unique_color_generator(
622 622 saturation=saturation, lightness=lightness)
623 623
624 624 def get_color_string(thing):
625 625 if thing in color_dict:
626 626 col = color_dict[thing]
627 627 else:
628 628 col = color_dict[thing] = next(cgenerator)
629 629 return "rgb(%s)" % (', '.join(col))
630 630
631 631 return get_color_string
632 632
633 633
634 634 def get_lexer_safe(mimetype=None, filepath=None):
635 635 """
636 636 Tries to return a relevant pygments lexer using mimetype/filepath name,
637 637 defaulting to plain text if none could be found
638 638 """
639 639 lexer = None
640 640 try:
641 641 if mimetype:
642 642 lexer = get_lexer_for_mimetype(mimetype)
643 643 if not lexer:
644 644 lexer = get_lexer_for_filename(filepath)
645 645 except pygments.util.ClassNotFound:
646 646 pass
647 647
648 648 if not lexer:
649 649 lexer = get_lexer_by_name('text')
650 650
651 651 return lexer
652 652
653 653
654 654 def get_lexer_for_filenode(filenode):
655 655 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
656 656 return lexer
657 657
658 658
659 659 def pygmentize(filenode, **kwargs):
660 660 """
661 661 pygmentize function using pygments
662 662
663 663 :param filenode:
664 664 """
665 665 lexer = get_lexer_for_filenode(filenode)
666 666 return literal(code_highlight(filenode.content, lexer,
667 667 CodeHtmlFormatter(**kwargs)))
668 668
669 669
670 670 def is_following_repo(repo_name, user_id):
671 671 from rhodecode.model.scm import ScmModel
672 672 return ScmModel().is_following_repo(repo_name, user_id)
673 673
674 674
675 675 class _Message(object):
676 676 """A message returned by ``Flash.pop_messages()``.
677 677
678 678 Converting the message to a string returns the message text. Instances
679 679 also have the following attributes:
680 680
681 681 * ``message``: the message text.
682 682 * ``category``: the category specified when the message was created.
683 683 """
684 684
685 685 def __init__(self, category, message, sub_data=None):
686 686 self.category = category
687 687 self.message = message
688 688 self.sub_data = sub_data or {}
689 689
690 690 def __str__(self):
691 691 return self.message
692 692
693 693 __unicode__ = __str__
694 694
695 695 def __html__(self):
696 696 return escape(safe_str(self.message))
697 697
698 698
699 699 class Flash(object):
700 700 # List of allowed categories. If None, allow any category.
701 701 categories = ["warning", "notice", "error", "success"]
702 702
703 703 # Default category if none is specified.
704 704 default_category = "notice"
705 705
706 706 def __init__(self, session_key="flash", categories=None,
707 707 default_category=None):
708 708 """
709 709 Instantiate a ``Flash`` object.
710 710
711 711 ``session_key`` is the key to save the messages under in the user's
712 712 session.
713 713
714 714 ``categories`` is an optional list which overrides the default list
715 715 of categories.
716 716
717 717 ``default_category`` overrides the default category used for messages
718 718 when none is specified.
719 719 """
720 720 self.session_key = session_key
721 721 if categories is not None:
722 722 self.categories = categories
723 723 if default_category is not None:
724 724 self.default_category = default_category
725 725 if self.categories and self.default_category not in self.categories:
726 726 raise ValueError(
727 727 "unrecognized default category %r" % (self.default_category,))
728 728
729 729 def pop_messages(self, session=None, request=None):
730 730 """
731 731 Return all accumulated messages and delete them from the session.
732 732
733 733 The return value is a list of ``Message`` objects.
734 734 """
735 735 messages = []
736 736
737 737 if not session:
738 738 if not request:
739 739 request = get_current_request()
740 740 session = request.session
741 741
742 742 # Pop the 'old' pylons flash messages. They are tuples of the form
743 743 # (category, message)
744 744 for cat, msg in session.pop(self.session_key, []):
745 745 messages.append(_Message(cat, msg))
746 746
747 747 # Pop the 'new' pyramid flash messages for each category as list
748 748 # of strings.
749 749 for cat in self.categories:
750 750 for msg in session.pop_flash(queue=cat):
751 751 sub_data = {}
752 752 if hasattr(msg, 'rsplit'):
753 753 flash_data = msg.rsplit('|DELIM|', 1)
754 754 org_message = flash_data[0]
755 755 if len(flash_data) > 1:
756 756 sub_data = json.loads(flash_data[1])
757 757 else:
758 758 org_message = msg
759 759
760 760 messages.append(_Message(cat, org_message, sub_data=sub_data))
761 761
762 762 # Map messages from the default queue to the 'notice' category.
763 763 for msg in session.pop_flash():
764 764 messages.append(_Message('notice', msg))
765 765
766 766 session.save()
767 767 return messages
768 768
769 769 def json_alerts(self, session=None, request=None):
770 770 payloads = []
771 771 messages = flash.pop_messages(session=session, request=request) or []
772 772 for message in messages:
773 773 payloads.append({
774 774 'message': {
775 775 'message': '{}'.format(message.message),
776 776 'level': message.category,
777 777 'force': True,
778 778 'subdata': message.sub_data
779 779 }
780 780 })
781 781 return safe_str(json.dumps(payloads))
782 782
783 783 def __call__(self, message, category=None, ignore_duplicate=True,
784 784 session=None, request=None):
785 785
786 786 if not session:
787 787 if not request:
788 788 request = get_current_request()
789 789 session = request.session
790 790
791 791 session.flash(
792 792 message, queue=category, allow_duplicate=not ignore_duplicate)
793 793
794 794
795 795 flash = Flash()
796 796
797 797 #==============================================================================
798 798 # SCM FILTERS available via h.
799 799 #==============================================================================
800 800 from rhodecode.lib.vcs.utils import author_name, author_email
801 801 from rhodecode.lib.utils2 import age, age_from_seconds
802 802 from rhodecode.model.db import User, ChangesetStatus
803 803
804 804
805 805 email = author_email
806 806
807 807
808 808 def capitalize(raw_text):
809 809 return raw_text.capitalize()
810 810
811 811
812 812 def short_id(long_id):
813 813 return long_id[:12]
814 814
815 815
816 816 def hide_credentials(url):
817 817 from rhodecode.lib.utils2 import credentials_filter
818 818 return credentials_filter(url)
819 819
820 820 import zoneinfo
821 821 import tzlocal
822 822 local_timezone = tzlocal.get_localzone()
823 823
824 824
825 825 def get_timezone(datetime_iso, time_is_local=False):
826 826 tzinfo = '+00:00'
827 827
828 828 # detect if we have a timezone info, otherwise, add it
829 829 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
830 830 force_timezone = os.environ.get('RC_TIMEZONE', '')
831 831 if force_timezone:
832 832 force_timezone = zoneinfo.ZoneInfo(force_timezone)
833 833 timezone = force_timezone or local_timezone
834 834
835 835 offset = datetime_iso.replace(tzinfo=timezone).strftime('%z')
836 836 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
837 837 return tzinfo
838 838
839 839
840 840 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
841 841 title = value or format_date(datetime_iso)
842 842 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
843 843
844 844 return literal(
845 845 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
846 846 cls='tooltip' if tooltip else '',
847 847 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
848 848 title=title, dt=datetime_iso, tzinfo=tzinfo
849 849 ))
850 850
851 851
852 852 def _shorten_commit_id(commit_id, commit_len=None):
853 853 if commit_len is None:
854 854 request = get_current_request()
855 855 commit_len = request.call_context.visual.show_sha_length
856 856 return commit_id[:commit_len]
857 857
858 858
859 859 def show_id(commit, show_idx=None, commit_len=None):
860 860 """
861 861 Configurable function that shows ID
862 862 by default it's r123:fffeeefffeee
863 863
864 864 :param commit: commit instance
865 865 """
866 866 if show_idx is None:
867 867 request = get_current_request()
868 868 show_idx = request.call_context.visual.show_revision_number
869 869
870 870 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
871 871 if show_idx:
872 872 return 'r%s:%s' % (commit.idx, raw_id)
873 873 else:
874 874 return '%s' % (raw_id, )
875 875
876 876
877 877 def format_date(date):
878 878 """
879 879 use a standardized formatting for dates used in RhodeCode
880 880
881 881 :param date: date/datetime object
882 882 :return: formatted date
883 883 """
884 884
885 885 if date:
886 886 _fmt = "%a, %d %b %Y %H:%M:%S"
887 887 return safe_str(date.strftime(_fmt))
888 888
889 889 return ""
890 890
891 891
892 892 class _RepoChecker(object):
893 893
894 894 def __init__(self, backend_alias):
895 895 self._backend_alias = backend_alias
896 896
897 897 def __call__(self, repository):
898 898 if hasattr(repository, 'alias'):
899 899 _type = repository.alias
900 900 elif hasattr(repository, 'repo_type'):
901 901 _type = repository.repo_type
902 902 else:
903 903 _type = repository
904 904 return _type == self._backend_alias
905 905
906 906
907 907 is_git = _RepoChecker('git')
908 908 is_hg = _RepoChecker('hg')
909 909 is_svn = _RepoChecker('svn')
910 910
911 911
912 912 def get_repo_type_by_name(repo_name):
913 913 repo = Repository.get_by_repo_name(repo_name)
914 914 if repo:
915 915 return repo.repo_type
916 916
917 917
918 918 def is_svn_without_proxy(repository):
919 919 if is_svn(repository):
920 920 return not ConfigGet().get_bool('vcs.svn.proxy.enabled')
921 921 return False
922 922
923 923
924 924 def discover_user(author):
925 925 """
926 926 Tries to discover RhodeCode User based on the author string. Author string
927 927 is typically `FirstName LastName <email@address.com>`
928 928 """
929 929
930 930 # if author is already an instance use it for extraction
931 931 if isinstance(author, User):
932 932 return author
933 933
934 934 # Valid email in the attribute passed, see if they're in the system
935 935 _email = author_email(author)
936 936 if _email != '':
937 937 user = User.get_by_email(_email, case_insensitive=True, cache=True)
938 938 if user is not None:
939 939 return user
940 940
941 941 # Maybe it's a username, we try to extract it and fetch by username ?
942 942 _author = author_name(author)
943 943 user = User.get_by_username(_author, case_insensitive=True, cache=True)
944 944 if user is not None:
945 945 return user
946 946
947 947 return None
948 948
949 949
950 950 def email_or_none(author):
951 951 # extract email from the commit string
952 952 _email = author_email(author)
953 953
954 954 # If we have an email, use it, otherwise
955 955 # see if it contains a username we can get an email from
956 956 if _email != '':
957 957 return _email
958 958 else:
959 959 user = User.get_by_username(
960 960 author_name(author), case_insensitive=True, cache=True)
961 961
962 962 if user is not None:
963 963 return user.email
964 964
965 965 # No valid email, not a valid user in the system, none!
966 966 return None
967 967
968 968
969 969 def link_to_user(author, length=0, **kwargs):
970 970 user = discover_user(author)
971 971 # user can be None, but if we have it already it means we can re-use it
972 972 # in the person() function, so we save 1 intensive-query
973 973 if user:
974 974 author = user
975 975
976 976 display_person = person(author, 'username_or_name_or_email')
977 977 if length:
978 978 display_person = shorter(display_person, length)
979 979
980 980 if user and user.username != user.DEFAULT_USER:
981 981 return link_to(
982 982 escape(display_person),
983 983 route_path('user_profile', username=user.username),
984 984 **kwargs)
985 985 else:
986 986 return escape(display_person)
987 987
988 988
989 989 def link_to_group(users_group_name, **kwargs):
990 990 return link_to(
991 991 escape(users_group_name),
992 992 route_path('user_group_profile', user_group_name=users_group_name),
993 993 **kwargs)
994 994
995 995
996 996 def person(author, show_attr="username_and_name"):
997 997 user = discover_user(author)
998 998 if user:
999 999 return getattr(user, show_attr)
1000 1000 else:
1001 1001 _author = author_name(author)
1002 1002 _email = email(author)
1003 1003 return _author or _email
1004 1004
1005 1005
1006 1006 def author_string(email):
1007 1007 if email:
1008 1008 user = User.get_by_email(email, case_insensitive=True, cache=True)
1009 1009 if user:
1010 1010 if user.first_name or user.last_name:
1011 1011 return '%s %s &lt;%s&gt;' % (
1012 1012 user.first_name, user.last_name, email)
1013 1013 else:
1014 1014 return email
1015 1015 else:
1016 1016 return email
1017 1017 else:
1018 1018 return None
1019 1019
1020 1020
1021 1021 def person_by_id(id_, show_attr="username_and_name"):
1022 1022 # attr to return from fetched user
1023 1023 def person_getter(usr):
1024 1024 return getattr(usr, show_attr)
1025 1025
1026 1026 #maybe it's an ID ?
1027 1027 if str(id_).isdigit() or isinstance(id_, int):
1028 1028 id_ = int(id_)
1029 1029 user = User.get(id_)
1030 1030 if user is not None:
1031 1031 return person_getter(user)
1032 1032 return id_
1033 1033
1034 1034
1035 1035 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1036 1036 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1037 1037 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1038 1038
1039 1039
1040 1040 tags_patterns = OrderedDict(
1041 1041 (
1042 1042 (
1043 1043 "lang",
1044 1044 (
1045 1045 re.compile(r"\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]"),
1046 1046 '<div class="metatag" tag="lang">\\2</div>',
1047 1047 ),
1048 1048 ),
1049 1049 (
1050 1050 "see",
1051 1051 (
1052 1052 re.compile(r"\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]"),
1053 1053 '<div class="metatag" tag="see">see: \\1 </div>',
1054 1054 ),
1055 1055 ),
1056 1056 (
1057 1057 "url",
1058 1058 (
1059 1059 re.compile(
1060 1060 r"\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]"
1061 1061 ),
1062 1062 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>',
1063 1063 ),
1064 1064 ),
1065 1065 (
1066 1066 "license",
1067 1067 (
1068 1068 re.compile(
1069 1069 r"\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]"
1070 1070 ),
1071 1071 # don't make it a raw string here...
1072 1072 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>',
1073 1073 ),
1074 1074 ),
1075 1075 (
1076 1076 "ref",
1077 1077 (
1078 1078 re.compile(
1079 1079 r"\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]"
1080 1080 ),
1081 1081 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>',
1082 1082 ),
1083 1083 ),
1084 1084 (
1085 1085 "state",
1086 1086 (
1087 1087 re.compile(r"\[(stable|featured|stale|dead|dev|deprecated)\]"),
1088 1088 '<div class="metatag" tag="state \\1">\\1</div>',
1089 1089 ),
1090 1090 ),
1091 1091 # label in grey
1092 1092 (
1093 1093 "label",
1094 1094 (re.compile(r"\[([a-z]+)\]"), '<div class="metatag" tag="label">\\1</div>'),
1095 1095 ),
1096 1096 # generic catch all in grey
1097 1097 (
1098 1098 "generic",
1099 1099 (
1100 1100 re.compile(r"\[([a-zA-Z0-9\.\-\_]+)\]"),
1101 1101 '<div class="metatag" tag="generic">\\1</div>',
1102 1102 ),
1103 1103 ),
1104 1104 )
1105 1105 )
1106 1106
1107 1107
1108 1108 def extract_metatags(value):
1109 1109 """
1110 1110 Extract supported meta-tags from given text value
1111 1111 """
1112 1112 tags = []
1113 1113 if not value:
1114 1114 return tags, ''
1115 1115
1116 1116 for key, val in list(tags_patterns.items()):
1117 1117 pat, replace_html = val
1118 1118 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1119 1119 value = pat.sub('', value)
1120 1120
1121 1121 return tags, value
1122 1122
1123 1123
1124 1124 def style_metatag(tag_type, value):
1125 1125 """
1126 1126 converts tags from value into html equivalent
1127 1127 """
1128 1128 if not value:
1129 1129 return ''
1130 1130
1131 1131 html_value = value
1132 1132 tag_data = tags_patterns.get(tag_type)
1133 1133 if tag_data:
1134 1134 pat, replace_html = tag_data
1135 1135 # convert to plain `str` instead of a markup tag to be used in
1136 1136 # regex expressions. safe_str doesn't work here
1137 1137 html_value = pat.sub(replace_html, value)
1138 1138
1139 1139 return html_value
1140 1140
1141 1141
1142 1142 def bool2icon(value, show_at_false=True):
1143 1143 """
1144 1144 Returns boolean value of a given value, represented as html element with
1145 1145 classes that will represent icons
1146 1146
1147 1147 :param value: given value to convert to html node
1148 1148 """
1149 1149
1150 1150 if value: # does bool conversion
1151 1151 return HTML.tag('i', class_="icon-true", title='True')
1152 1152 else: # not true as bool
1153 1153 if show_at_false:
1154 1154 return HTML.tag('i', class_="icon-false", title='False')
1155 1155 return HTML.tag('i')
1156 1156
1157 1157
1158 1158 def b64(inp):
1159 1159 return base64.b64encode(safe_bytes(inp))
1160 1160
1161 1161 #==============================================================================
1162 1162 # PERMS
1163 1163 #==============================================================================
1164 1164 from rhodecode.lib.auth import (
1165 1165 HasPermissionAny, HasPermissionAll,
1166 1166 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1167 1167 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1168 1168 csrf_token_key, AuthUser)
1169 1169
1170 1170
1171 1171 #==============================================================================
1172 1172 # GRAVATAR URL
1173 1173 #==============================================================================
1174 1174 class InitialsGravatar(object):
1175 1175 def __init__(self, email_address, first_name, last_name, size=30,
1176 1176 background=None, text_color='#fff'):
1177 1177 self.size = size
1178 1178 self.first_name = first_name
1179 1179 self.last_name = last_name
1180 1180 self.email_address = email_address
1181 1181 self.background = background or self.str2color(email_address)
1182 1182 self.text_color = text_color
1183 1183
1184 1184 def get_color_bank(self):
1185 1185 """
1186 1186 returns a predefined list of colors that gravatars can use.
1187 1187 Those are randomized distinct colors that guarantee readability and
1188 1188 uniqueness.
1189 1189
1190 1190 generated with: http://phrogz.net/css/distinct-colors.html
1191 1191 """
1192 1192 return [
1193 1193 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1194 1194 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1195 1195 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1196 1196 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1197 1197 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1198 1198 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1199 1199 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1200 1200 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1201 1201 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1202 1202 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1203 1203 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1204 1204 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1205 1205 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1206 1206 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1207 1207 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1208 1208 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1209 1209 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1210 1210 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1211 1211 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1212 1212 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1213 1213 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1214 1214 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1215 1215 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1216 1216 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1217 1217 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1218 1218 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1219 1219 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1220 1220 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1221 1221 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1222 1222 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1223 1223 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1224 1224 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1225 1225 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1226 1226 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1227 1227 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1228 1228 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1229 1229 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1230 1230 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1231 1231 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1232 1232 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1233 1233 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1234 1234 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1235 1235 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1236 1236 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1237 1237 '#4f8c46', '#368dd9', '#5c0073'
1238 1238 ]
1239 1239
1240 1240 def rgb_to_hex_color(self, rgb_tuple):
1241 1241 """
1242 1242 Converts an rgb_tuple passed to an hex color.
1243 1243
1244 1244 :param rgb_tuple: tuple with 3 ints represents rgb color space
1245 1245 """
1246 1246 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1247 1247
1248 1248 def email_to_int_list(self, email_str):
1249 1249 """
1250 1250 Get every byte of the hex digest value of email and turn it to integer.
1251 1251 It's going to be always between 0-255
1252 1252 """
1253 1253 digest = md5_safe(email_str.lower())
1254 1254 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1255 1255
1256 1256 def pick_color_bank_index(self, email_str, color_bank):
1257 1257 return self.email_to_int_list(email_str)[0] % len(color_bank)
1258 1258
1259 1259 def str2color(self, email_str):
1260 1260 """
1261 1261 Tries to map in a stable algorithm an email to color
1262 1262
1263 1263 :param email_str:
1264 1264 """
1265 1265 color_bank = self.get_color_bank()
1266 1266 # pick position (module it's length so we always find it in the
1267 1267 # bank even if it's smaller than 256 values
1268 1268 pos = self.pick_color_bank_index(email_str, color_bank)
1269 1269 return color_bank[pos]
1270 1270
1271 1271 def normalize_email(self, email_address):
1272 1272 # default host used to fill in the fake/missing email
1273 1273 default_host = 'localhost'
1274 1274
1275 1275 if not email_address:
1276 1276 email_address = f'{User.DEFAULT_USER}@{default_host}'
1277 1277
1278 1278 email_address = safe_str(email_address)
1279 1279
1280 1280 if '@' not in email_address:
1281 1281 email_address = f'{email_address}@{default_host}'
1282 1282
1283 1283 if email_address.endswith('@'):
1284 1284 email_address = f'{email_address}{default_host}'
1285 1285
1286 1286 email_address = convert_special_chars(email_address)
1287 1287
1288 1288 return email_address
1289 1289
1290 1290 def get_initials(self):
1291 1291 """
1292 1292 Returns 2 letter initials calculated based on the input.
1293 1293 The algorithm picks first given email address, and takes first letter
1294 1294 of part before @, and then the first letter of server name. In case
1295 1295 the part before @ is in a format of `somestring.somestring2` it replaces
1296 1296 the server letter with first letter of somestring2
1297 1297
1298 1298 In case function was initialized with both first and lastname, this
1299 1299 overrides the extraction from email by first letter of the first and
1300 1300 last name. We add special logic to that functionality, In case Full name
1301 1301 is compound, like Guido Von Rossum, we use last part of the last name
1302 1302 (Von Rossum) picking `R`.
1303 1303
1304 1304 Function also normalizes the non-ascii characters to they ascii
1305 1305 representation, eg Ą => A
1306 1306 """
1307 1307 # replace non-ascii to ascii
1308 1308 first_name = convert_special_chars(self.first_name)
1309 1309 last_name = convert_special_chars(self.last_name)
1310 1310 # multi word last names, Guido Von Rossum, we take the last part only
1311 1311 last_name = last_name.split(' ', 1)[-1]
1312 1312
1313 1313 # do NFKD encoding, and also make sure email has proper format
1314 1314 email_address = self.normalize_email(self.email_address)
1315 1315
1316 1316 # first push the email initials
1317 1317 prefix, server = email_address.split('@', 1)
1318 1318
1319 1319 # check if prefix is maybe a 'first_name.last_name' syntax
1320 1320 _dot_split = prefix.rsplit('.', 1)
1321 1321 if len(_dot_split) == 2 and _dot_split[1]:
1322 1322 initials = [_dot_split[0][0], _dot_split[1][0]]
1323 1323 else:
1324 1324 initials = [prefix[0], server[0]]
1325 1325
1326 1326 # get first letter of first and last names to create initials
1327 1327 fn_letter = (first_name or " ")[0].strip()
1328 1328 ln_letter = (last_name or " ")[0].strip()
1329 1329
1330 1330 if fn_letter:
1331 1331 initials[0] = fn_letter
1332 1332
1333 1333 if ln_letter:
1334 1334 initials[1] = ln_letter
1335 1335
1336 1336 return ''.join(initials).upper()
1337 1337
1338 1338 def get_img_data_by_type(self, font_family, img_type):
1339 1339 default_user = """
1340 1340 <svg xmlns="http://www.w3.org/2000/svg"
1341 1341 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1342 1342 viewBox="-15 -10 439.165 429.164"
1343 1343
1344 1344 xml:space="preserve"
1345 1345 font-family="{font_family}"
1346 1346 style="background:{background};" >
1347 1347
1348 1348 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1349 1349 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1350 1350 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1351 1351 168.596,153.916,216.671,
1352 1352 204.583,216.671z" fill="{text_color}"/>
1353 1353 <path d="M407.164,374.717L360.88,
1354 1354 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1355 1355 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1356 1356 15.366-44.203,23.488-69.076,23.488c-24.877,
1357 1357 0-48.762-8.122-69.078-23.488
1358 1358 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1359 1359 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1360 1360 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1361 1361 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1362 1362 19.402-10.527 C409.699,390.129,
1363 1363 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1364 1364 </svg>""".format(
1365 1365 size=self.size,
1366 1366 background='#979797', # @grey4
1367 1367 text_color=self.text_color,
1368 1368 font_family=font_family)
1369 1369
1370 1370 return {
1371 1371 "default_user": default_user
1372 1372 }[img_type]
1373 1373
1374 1374 def get_img_data(self, svg_type=None):
1375 1375 """
1376 1376 generates the svg metadata for image
1377 1377 """
1378 1378 fonts = [
1379 1379 '-apple-system',
1380 1380 'BlinkMacSystemFont',
1381 1381 'Segoe UI',
1382 1382 'Roboto',
1383 1383 'Oxygen-Sans',
1384 1384 'Ubuntu',
1385 1385 'Cantarell',
1386 1386 'Helvetica Neue',
1387 1387 'sans-serif'
1388 1388 ]
1389 1389 font_family = ','.join(fonts)
1390 1390 if svg_type:
1391 1391 return self.get_img_data_by_type(font_family, svg_type)
1392 1392
1393 1393 initials = self.get_initials()
1394 1394 img_data = """
1395 1395 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1396 1396 width="{size}" height="{size}"
1397 1397 style="width: 100%; height: 100%; background-color: {background}"
1398 1398 viewBox="0 0 {size} {size}">
1399 1399 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1400 1400 pointer-events="auto" fill="{text_color}"
1401 1401 font-family="{font_family}"
1402 1402 style="font-weight: 400; font-size: {f_size}px;">{text}
1403 1403 </text>
1404 1404 </svg>""".format(
1405 1405 size=self.size,
1406 1406 f_size=self.size/2.05, # scale the text inside the box nicely
1407 1407 background=self.background,
1408 1408 text_color=self.text_color,
1409 1409 text=initials.upper(),
1410 1410 font_family=font_family)
1411 1411
1412 1412 return img_data
1413 1413
1414 1414 def generate_svg(self, svg_type=None):
1415 1415 img_data = base64_to_str(self.get_img_data(svg_type))
1416 1416 return "data:image/svg+xml;base64,{}".format(img_data)
1417 1417
1418 1418
1419 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1419 def initials_gravatar(request, email_address, first_name, last_name, size=30):
1420 1420
1421 1421 svg_type = None
1422 1422 if email_address == User.DEFAULT_USER_EMAIL:
1423 1423 svg_type = 'default_user'
1424 1424
1425 1425 klass = InitialsGravatar(email_address, first_name, last_name, size)
1426
1427 if store_on_disk:
1428 from rhodecode.apps.file_store import utils as store_utils
1429 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1430 FileOverSizeException
1431 from rhodecode.model.db import Session
1432
1433 image_key = md5_safe(email_address.lower()
1434 + first_name.lower() + last_name.lower())
1435
1436 storage = store_utils.get_file_storage(request.registry.settings)
1437 filename = '{}.svg'.format(image_key)
1438 subdir = 'gravatars'
1439 # since final name has a counter, we apply the 0
1440 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1441 store_uid = os.path.join(subdir, uid)
1442
1443 db_entry = FileStore.get_by_store_uid(store_uid)
1444 if db_entry:
1445 return request.route_path('download_file', fid=store_uid)
1446
1447 img_data = klass.get_img_data(svg_type=svg_type)
1448 img_file = store_utils.bytes_to_file_obj(img_data)
1449
1450 try:
1451 store_uid, metadata = storage.save_file(
1452 img_file, filename, directory=subdir,
1453 extensions=['.svg'], randomized_name=False)
1454 except (FileNotAllowedException, FileOverSizeException):
1455 raise
1456
1457 try:
1458 entry = FileStore.create(
1459 file_uid=store_uid, filename=metadata["filename"],
1460 file_hash=metadata["sha256"], file_size=metadata["size"],
1461 file_display_name=filename,
1462 file_description=f'user gravatar `{safe_str(filename)}`',
1463 hidden=True, check_acl=False, user_id=1
1464 )
1465 Session().add(entry)
1466 Session().commit()
1467 log.debug('Stored upload in DB as %s', entry)
1468 except Exception:
1469 raise
1470
1471 return request.route_path('download_file', fid=store_uid)
1472
1473 else:
1474 return klass.generate_svg(svg_type=svg_type)
1426 return klass.generate_svg(svg_type=svg_type)
1475 1427
1476 1428
1477 1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1478 1430 return safe_str(gravatar_url_tmpl)\
1479 1431 .replace('{email}', email_address) \
1480 1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1481 1433 .replace('{netloc}', request.host) \
1482 1434 .replace('{scheme}', request.scheme) \
1483 1435 .replace('{size}', safe_str(size))
1484 1436
1485 1437
1486 1438 def gravatar_url(email_address, size=30, request=None):
1487 1439 request = request or get_current_request()
1488 1440 _use_gravatar = request.call_context.visual.use_gravatar
1489 1441
1490 1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1491 1443 if isinstance(email_address, str):
1492 1444 # hashlib crashes on unicode items
1493 1445 email_address = safe_str(email_address)
1494 1446
1495 1447 # empty email or default user
1496 1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1497 1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1498 1450
1499 1451 if _use_gravatar:
1500 1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1501 1453 or User.DEFAULT_GRAVATAR_URL
1502 1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1503 1455
1504 1456 else:
1505 1457 return initials_gravatar(request, email_address, '', '', size=size)
1506 1458
1507 1459
1508 1460 def breadcrumb_repo_link(repo):
1509 1461 """
1510 1462 Makes a breadcrumbs path link to repo
1511 1463
1512 1464 ex::
1513 1465 group >> subgroup >> repo
1514 1466
1515 1467 :param repo: a Repository instance
1516 1468 """
1517 1469
1518 1470 path = [
1519 1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1520 1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1521 1473 for group in repo.groups_with_parents
1522 1474 ] + [
1523 1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1524 1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1525 1477 ]
1526 1478
1527 1479 return literal(' &raquo; '.join(path))
1528 1480
1529 1481
1530 1482 def breadcrumb_repo_group_link(repo_group):
1531 1483 """
1532 1484 Makes a breadcrumbs path link to repo
1533 1485
1534 1486 ex::
1535 1487 group >> subgroup
1536 1488
1537 1489 :param repo_group: a Repository Group instance
1538 1490 """
1539 1491
1540 1492 path = [
1541 1493 link_to(group.name,
1542 1494 route_path('repo_group_home', repo_group_name=group.group_name),
1543 1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1544 1496 for group in repo_group.parents
1545 1497 ] + [
1546 1498 link_to(repo_group.name,
1547 1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1548 1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1549 1501 ]
1550 1502
1551 1503 return literal(' &raquo; '.join(path))
1552 1504
1553 1505
1554 1506 def format_byte_size_binary(file_size):
1555 1507 """
1556 1508 Formats file/folder sizes to standard.
1557 1509 """
1558 1510 if file_size is None:
1559 1511 file_size = 0
1560 1512
1561 1513 formatted_size = format_byte_size(file_size, binary=True)
1562 1514 return formatted_size
1563 1515
1564 1516
1565 1517 def urlify_text(text_, safe=True, **href_attrs):
1566 1518 """
1567 1519 Extract urls from text and make html links out of them
1568 1520 """
1569 1521
1570 1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1571 1523 r'''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1572 1524
1573 1525 def url_func(match_obj):
1574 1526 url_full = match_obj.groups()[0]
1575 1527 a_options = dict(href_attrs)
1576 1528 a_options['href'] = url_full
1577 1529 a_text = url_full
1578 1530 return HTML.tag("a", a_text, **a_options)
1579 1531
1580 1532 _new_text = url_pat.sub(url_func, text_)
1581 1533
1582 1534 if safe:
1583 1535 return literal(_new_text)
1584 1536 return _new_text
1585 1537
1586 1538
1587 1539 def urlify_commits(text_, repo_name):
1588 1540 """
1589 1541 Extract commit ids from text and make link from them
1590 1542
1591 1543 :param text_:
1592 1544 :param repo_name: repo name to build the URL with
1593 1545 """
1594 1546
1595 1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1596 1548
1597 1549 def url_func(match_obj):
1598 1550 commit_id = match_obj.groups()[1]
1599 1551 pref = match_obj.groups()[0]
1600 1552 suf = match_obj.groups()[2]
1601 1553
1602 1554 tmpl = (
1603 1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1604 1556 '%(commit_id)s</a>%(suf)s'
1605 1557 )
1606 1558 return tmpl % {
1607 1559 'pref': pref,
1608 1560 'cls': 'revision-link',
1609 1561 'url': route_url(
1610 1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1611 1563 'commit_id': commit_id,
1612 1564 'suf': suf,
1613 1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1614 1566 'hovercard_url': route_url(
1615 1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1616 1568 }
1617 1569
1618 1570 new_text = url_pat.sub(url_func, text_)
1619 1571
1620 1572 return new_text
1621 1573
1622 1574
1623 1575 def _process_url_func(match_obj, repo_name, uid, entry,
1624 1576 return_raw_data=False, link_format='html'):
1625 1577 pref = ''
1626 1578 if match_obj.group().startswith(' '):
1627 1579 pref = ' '
1628 1580
1629 1581 issue_id = ''.join(match_obj.groups())
1630 1582
1631 1583 if link_format == 'html':
1632 1584 tmpl = (
1633 1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1634 1586 '%(issue-prefix)s%(id-repr)s'
1635 1587 '</a>')
1636 1588 elif link_format == 'html+hovercard':
1637 1589 tmpl = (
1638 1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1639 1591 '%(issue-prefix)s%(id-repr)s'
1640 1592 '</a>')
1641 1593 elif link_format in ['rst', 'rst+hovercard']:
1642 1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1643 1595 elif link_format in ['markdown', 'markdown+hovercard']:
1644 1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1645 1597 else:
1646 1598 raise ValueError('Bad link_format:{}'.format(link_format))
1647 1599
1648 1600 (repo_name_cleaned,
1649 1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1650 1602
1651 1603 # variables replacement
1652 1604 named_vars = {
1653 1605 'id': issue_id,
1654 1606 'repo': repo_name,
1655 1607 'repo_name': repo_name_cleaned,
1656 1608 'group_name': parent_group_name,
1657 1609 # set dummy keys so we always have them
1658 1610 'hostname': '',
1659 1611 'netloc': '',
1660 1612 'scheme': ''
1661 1613 }
1662 1614
1663 1615 request = get_current_request()
1664 1616 if request:
1665 1617 # exposes, hostname, netloc, scheme
1666 1618 host_data = get_host_info(request)
1667 1619 named_vars.update(host_data)
1668 1620
1669 1621 # named regex variables
1670 1622 named_vars.update(match_obj.groupdict())
1671 1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1672 1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1673 1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1674 1626
1675 1627 def quote_cleaner(input_str):
1676 1628 """Remove quotes as it's HTML"""
1677 1629 return input_str.replace('"', '')
1678 1630
1679 1631 data = {
1680 1632 'pref': pref,
1681 1633 'cls': quote_cleaner('issue-tracker-link'),
1682 1634 'url': quote_cleaner(_url),
1683 1635 'id-repr': issue_id,
1684 1636 'issue-prefix': entry['pref'],
1685 1637 'serv': entry['url'],
1686 1638 'title': sanitize_html(desc, strip=True),
1687 1639 'hovercard_url': hovercard_url
1688 1640 }
1689 1641
1690 1642 if return_raw_data:
1691 1643 return {
1692 1644 'id': issue_id,
1693 1645 'url': _url
1694 1646 }
1695 1647 return tmpl % data
1696 1648
1697 1649
1698 1650 def get_active_pattern_entries(repo_name):
1699 1651 repo = None
1700 1652 if repo_name:
1701 1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1702 1654 # IssueTrackerSettingsModel but still passing invalid name further down
1703 1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1704 1656
1705 1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1706 1658 active_entries = settings_model.get_settings(cache=True)
1707 1659 return active_entries
1708 1660
1709 1661
1710 1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1711 1663
1712 1664 allowed_link_formats = [
1713 1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1714 1666
1715 1667 compile_cache = {
1716 1668
1717 1669 }
1718 1670
1719 1671
1720 1672 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1721 1673
1722 1674 if link_format not in allowed_link_formats:
1723 1675 raise ValueError('Link format can be only one of:{} got {}'.format(
1724 1676 allowed_link_formats, link_format))
1725 1677 issues_data = []
1726 1678 errors = []
1727 1679 new_text = text_string
1728 1680
1729 1681 if active_entries is None:
1730 1682 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1731 1683 active_entries = get_active_pattern_entries(repo_name)
1732 1684
1733 1685 log.debug('Got %s pattern entries to process', len(active_entries))
1734 1686
1735 1687 for uid, entry in list(active_entries.items()):
1736 1688
1737 1689 if not (entry['pat'] and entry['url']):
1738 1690 log.debug('skipping due to missing data')
1739 1691 continue
1740 1692
1741 1693 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1742 1694 uid, entry['pat'], entry['url'], entry['pref'])
1743 1695
1744 1696 if entry.get('pat_compiled'):
1745 1697 pattern = entry['pat_compiled']
1746 1698 elif entry['pat'] in compile_cache:
1747 1699 pattern = compile_cache[entry['pat']]
1748 1700 else:
1749 1701 try:
1750 1702 pattern = regex.compile(r'%s' % entry['pat'])
1751 1703 except regex.error as e:
1752 1704 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1753 1705 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1754 1706 errors.append(regex_err)
1755 1707 continue
1756 1708 compile_cache[entry['pat']] = pattern
1757 1709
1758 1710 data_func = partial(
1759 1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1760 1712 return_raw_data=True)
1761 1713
1762 1714 for match_obj in pattern.finditer(text_string):
1763 1715 issues_data.append(data_func(match_obj))
1764 1716
1765 1717 url_func = partial(
1766 1718 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1767 1719 link_format=link_format)
1768 1720
1769 1721 new_text = pattern.sub(url_func, new_text)
1770 1722 log.debug('processed prefix:uid `%s`', uid)
1771 1723
1772 1724 # finally use global replace, eg !123 -> pr-link, those will not catch
1773 1725 # if already similar pattern exists
1774 1726 server_url = '${scheme}://${netloc}'
1775 1727 pr_entry = {
1776 1728 'pref': '!',
1777 1729 'url': server_url + '/_admin/pull-requests/${id}',
1778 1730 'desc': 'Pull Request !${id}',
1779 1731 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1780 1732 }
1781 1733 pr_url_func = partial(
1782 1734 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1783 1735 link_format=link_format+'+hovercard')
1784 1736 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1785 1737 log.debug('processed !pr pattern')
1786 1738
1787 1739 return new_text, issues_data, errors
1788 1740
1789 1741
1790 1742 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1791 1743 issues_container_callback=None, error_container=None):
1792 1744 """
1793 1745 Parses given text message and makes proper links.
1794 1746 issues are linked to given issue-server, and rest is a commit link
1795 1747 """
1796 1748
1797 1749 def escaper(_text):
1798 1750 return _text.replace('<', '&lt;').replace('>', '&gt;')
1799 1751
1800 1752 new_text = escaper(commit_text)
1801 1753
1802 1754 # extract http/https links and make them real urls
1803 1755 new_text = urlify_text(new_text, safe=False)
1804 1756
1805 1757 # urlify commits - extract commit ids and make link out of them, if we have
1806 1758 # the scope of repository present.
1807 1759 if repository:
1808 1760 new_text = urlify_commits(new_text, repository)
1809 1761
1810 1762 # process issue tracker patterns
1811 1763 new_text, issues, errors = process_patterns(
1812 1764 new_text, repository or '', active_entries=active_pattern_entries)
1813 1765
1814 1766 if issues_container_callback is not None:
1815 1767 for issue in issues:
1816 1768 issues_container_callback(issue)
1817 1769
1818 1770 if error_container is not None:
1819 1771 error_container.extend(errors)
1820 1772
1821 1773 return literal(new_text)
1822 1774
1823 1775
1824 1776 def render_binary(repo_name, file_obj):
1825 1777 """
1826 1778 Choose how to render a binary file
1827 1779 """
1828 1780
1829 1781 # unicode
1830 1782 filename = file_obj.name
1831 1783
1832 1784 # images
1833 1785 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1834 1786 if fnmatch.fnmatch(filename, pat=ext):
1835 1787 src = route_path(
1836 1788 'repo_file_raw', repo_name=repo_name,
1837 1789 commit_id=file_obj.commit.raw_id,
1838 1790 f_path=file_obj.path)
1839 1791
1840 1792 return literal(
1841 1793 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1842 1794
1843 1795
1844 1796 def renderer_from_filename(filename, exclude=None):
1845 1797 """
1846 1798 choose a renderer based on filename, this works only for text based files
1847 1799 """
1848 1800
1849 1801 # ipython
1850 1802 for ext in ['*.ipynb']:
1851 1803 if fnmatch.fnmatch(filename, pat=ext):
1852 1804 return 'jupyter'
1853 1805
1854 1806 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1855 1807 if is_markup:
1856 1808 return is_markup
1857 1809 return None
1858 1810
1859 1811
1860 1812 def render(source, renderer='rst', mentions=False, relative_urls=None,
1861 1813 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1862 1814
1863 1815 def maybe_convert_relative_links(html_source):
1864 1816 if relative_urls:
1865 1817 return relative_links(html_source, relative_urls)
1866 1818 return html_source
1867 1819
1868 1820 if renderer == 'plain':
1869 1821 return literal(
1870 1822 MarkupRenderer.plain(source, leading_newline=False))
1871 1823
1872 1824 elif renderer == 'rst':
1873 1825 if repo_name:
1874 1826 # process patterns on comments if we pass in repo name
1875 1827 source, issues, errors = process_patterns(
1876 1828 source, repo_name, link_format='rst',
1877 1829 active_entries=active_pattern_entries)
1878 1830 if issues_container_callback is not None:
1879 1831 for issue in issues:
1880 1832 issues_container_callback(issue)
1881 1833
1882 1834 rendered_block = maybe_convert_relative_links(
1883 1835 MarkupRenderer.rst(source, mentions=mentions))
1884 1836
1885 1837 return literal(f'<div class="rst-block">{rendered_block}</div>')
1886 1838
1887 1839 elif renderer == 'markdown':
1888 1840 if repo_name:
1889 1841 # process patterns on comments if we pass in repo name
1890 1842 source, issues, errors = process_patterns(
1891 1843 source, repo_name, link_format='markdown',
1892 1844 active_entries=active_pattern_entries)
1893 1845 if issues_container_callback is not None:
1894 1846 for issue in issues:
1895 1847 issues_container_callback(issue)
1896 1848
1897 1849 rendered_block = maybe_convert_relative_links(
1898 1850 MarkupRenderer.markdown(source, flavored=True, mentions=mentions))
1899 1851 return literal(f'<div class="markdown-block">{rendered_block}</div>')
1900 1852
1901 1853 elif renderer == 'jupyter':
1902 1854 rendered_block = maybe_convert_relative_links(
1903 1855 MarkupRenderer.jupyter(source))
1904 1856 return literal(f'<div class="ipynb">{rendered_block}</div>')
1905 1857
1906 1858 # None means just show the file-source
1907 1859 return None
1908 1860
1909 1861
1910 1862 def commit_status(repo, commit_id):
1911 1863 return ChangesetStatusModel().get_status(repo, commit_id)
1912 1864
1913 1865
1914 1866 def commit_status_lbl(commit_status):
1915 1867 return dict(ChangesetStatus.STATUSES).get(commit_status)
1916 1868
1917 1869
1918 1870 def commit_time(repo_name, commit_id):
1919 1871 repo = Repository.get_by_repo_name(repo_name)
1920 1872 commit = repo.get_commit(commit_id=commit_id)
1921 1873 return commit.date
1922 1874
1923 1875
1924 1876 def get_permission_name(key):
1925 1877 return dict(Permission.PERMS).get(key)
1926 1878
1927 1879
1928 1880 def journal_filter_help(request):
1929 1881 _ = request.translate
1930 1882 from rhodecode.lib.audit_logger import ACTIONS
1931 1883 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1932 1884
1933 1885 return _(
1934 1886 'Example filter terms:\n' +
1935 1887 ' repository:vcs\n' +
1936 1888 ' username:marcin\n' +
1937 1889 ' username:(NOT marcin)\n' +
1938 1890 ' action:*push*\n' +
1939 1891 ' ip:127.0.0.1\n' +
1940 1892 ' date:20120101\n' +
1941 1893 ' date:[20120101100000 TO 20120102]\n' +
1942 1894 '\n' +
1943 1895 'Actions: {actions}\n' +
1944 1896 '\n' +
1945 1897 'Generate wildcards using \'*\' character:\n' +
1946 1898 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1947 1899 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1948 1900 '\n' +
1949 1901 'Optional AND / OR operators in queries\n' +
1950 1902 ' "repository:vcs OR repository:test"\n' +
1951 1903 ' "username:test AND repository:test*"\n'
1952 1904 ).format(actions=actions)
1953 1905
1954 1906
1955 1907 def not_mapped_error(repo_name):
1956 1908 from rhodecode.translation import _
1957 1909 flash(_('%s repository is not mapped to db perhaps'
1958 1910 ' it was created or renamed from the filesystem'
1959 1911 ' please run the application again'
1960 1912 ' in order to rescan repositories') % repo_name, category='error')
1961 1913
1962 1914
1963 1915 def ip_range(ip_addr):
1964 1916 from rhodecode.model.db import UserIpMap
1965 1917 s, e = UserIpMap._get_ip_range(ip_addr)
1966 1918 return '%s - %s' % (s, e)
1967 1919
1968 1920
1969 1921 def form(url, method='post', needs_csrf_token=True, **attrs):
1970 1922 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1971 1923 if method.lower() != 'get' and needs_csrf_token:
1972 1924 raise Exception(
1973 1925 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1974 1926 'CSRF token. If the endpoint does not require such token you can ' +
1975 1927 'explicitly set the parameter needs_csrf_token to false.')
1976 1928
1977 1929 return insecure_form(url, method=method, **attrs)
1978 1930
1979 1931
1980 1932 def secure_form(form_url, method="POST", multipart=False, **attrs):
1981 1933 """Start a form tag that points the action to an url. This
1982 1934 form tag will also include the hidden field containing
1983 1935 the auth token.
1984 1936
1985 1937 The url options should be given either as a string, or as a
1986 1938 ``url()`` function. The method for the form defaults to POST.
1987 1939
1988 1940 Options:
1989 1941
1990 1942 ``multipart``
1991 1943 If set to True, the enctype is set to "multipart/form-data".
1992 1944 ``method``
1993 1945 The method to use when submitting the form, usually either
1994 1946 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1995 1947 hidden input with name _method is added to simulate the verb
1996 1948 over POST.
1997 1949
1998 1950 """
1999 1951
2000 1952 if 'request' in attrs:
2001 1953 session = attrs['request'].session
2002 1954 del attrs['request']
2003 1955 else:
2004 1956 raise ValueError(
2005 1957 'Calling this form requires request= to be passed as argument')
2006 1958
2007 1959 _form = insecure_form(form_url, method, multipart, **attrs)
2008 1960 token = literal(
2009 1961 '<input type="hidden" name="{}" value="{}">'.format(
2010 1962 csrf_token_key, get_csrf_token(session)))
2011 1963
2012 1964 return literal("%s\n%s" % (_form, token))
2013 1965
2014 1966
2015 1967 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
2016 1968 select_html = select(name, selected, options, **attrs)
2017 1969
2018 1970 select2 = """
2019 1971 <script>
2020 1972 $(document).ready(function() {
2021 1973 $('#%s').select2({
2022 1974 containerCssClass: 'drop-menu %s',
2023 1975 dropdownCssClass: 'drop-menu-dropdown',
2024 1976 dropdownAutoWidth: true%s
2025 1977 });
2026 1978 });
2027 1979 </script>
2028 1980 """
2029 1981
2030 1982 filter_option = """,
2031 1983 minimumResultsForSearch: -1
2032 1984 """
2033 1985 input_id = attrs.get('id') or name
2034 1986 extra_classes = ' '.join(attrs.pop('extra_classes', []))
2035 1987 filter_enabled = "" if enable_filter else filter_option
2036 1988 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
2037 1989
2038 1990 return literal(select_html+select_script)
2039 1991
2040 1992
2041 1993 def get_visual_attr(tmpl_context_var, attr_name):
2042 1994 """
2043 1995 A safe way to get a variable from visual variable of template context
2044 1996
2045 1997 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2046 1998 :param attr_name: name of the attribute we fetch from the c.visual
2047 1999 """
2048 2000 visual = getattr(tmpl_context_var, 'visual', None)
2049 2001 if not visual:
2050 2002 return
2051 2003 else:
2052 2004 return getattr(visual, attr_name, None)
2053 2005
2054 2006
2055 2007 def get_last_path_part(file_node):
2056 2008 if not file_node.path:
2057 2009 return '/'
2058 2010
2059 2011 path = safe_str(file_node.path.split('/')[-1])
2060 2012 return '../' + path
2061 2013
2062 2014
2063 2015 def route_url(*args, **kwargs):
2064 2016 """
2065 2017 Wrapper around pyramids `route_url` (fully qualified url) function.
2066 2018 """
2067 2019 req = get_current_request()
2068 2020 return req.route_url(*args, **kwargs)
2069 2021
2070 2022
2071 2023 def route_path(*args, **kwargs):
2072 2024 """
2073 2025 Wrapper around pyramids `route_path` function.
2074 2026 """
2075 2027 req = get_current_request()
2076 2028 return req.route_path(*args, **kwargs)
2077 2029
2078 2030
2079 2031 def route_path_or_none(*args, **kwargs):
2080 2032 try:
2081 2033 return route_path(*args, **kwargs)
2082 2034 except KeyError:
2083 2035 return None
2084 2036
2085 2037
2086 2038 def current_route_path(request, **kw):
2087 2039 new_args = request.GET.mixed()
2088 2040 new_args.update(kw)
2089 2041 return request.current_route_path(_query=new_args)
2090 2042
2091 2043
2092 2044 def curl_api_example(method, args):
2093 2045 args_json = json.dumps(OrderedDict([
2094 2046 ('id', 1),
2095 2047 ('auth_token', 'SECRET'),
2096 2048 ('method', method),
2097 2049 ('args', args)
2098 2050 ]))
2099 2051
2100 2052 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2101 2053 api_url=route_url('apiv2'),
2102 2054 args_json=args_json
2103 2055 )
2104 2056
2105 2057
2106 2058 def api_call_example(method, args):
2107 2059 """
2108 2060 Generates an API call example via CURL
2109 2061 """
2110 2062 curl_call = curl_api_example(method, args)
2111 2063
2112 2064 return literal(
2113 2065 curl_call +
2114 2066 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2115 2067 "and needs to be of `api calls` role."
2116 2068 .format(token_url=route_url('my_account_auth_tokens')))
2117 2069
2118 2070
2119 2071 def notification_description(notification, request):
2120 2072 """
2121 2073 Generate notification human readable description based on notification type
2122 2074 """
2123 2075 from rhodecode.model.notification import NotificationModel
2124 2076 return NotificationModel().make_description(
2125 2077 notification, translate=request.translate)
2126 2078
2127 2079
2128 2080 def go_import_header(request, db_repo=None):
2129 2081 """
2130 2082 Creates a header for go-import functionality in Go Lang
2131 2083 """
2132 2084
2133 2085 if not db_repo:
2134 2086 return
2135 2087 if 'go-get' not in request.GET:
2136 2088 return
2137 2089
2138 2090 clone_url = db_repo.clone_url()
2139 2091 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2140 2092 # we have a repo and go-get flag,
2141 2093 return literal('<meta name="go-import" content="{} {} {}">'.format(
2142 2094 prefix, db_repo.repo_type, clone_url))
2143 2095
2144 2096
2145 2097 def reviewer_as_json(*args, **kwargs):
2146 2098 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2147 2099 return _reviewer_as_json(*args, **kwargs)
2148 2100
2149 2101
2150 2102 def get_repo_view_type(request):
2151 2103 route_name = request.matched_route.name
2152 2104 route_to_view_type = {
2153 2105 'repo_changelog': 'commits',
2154 2106 'repo_commits': 'commits',
2155 2107 'repo_files': 'files',
2156 2108 'repo_summary': 'summary',
2157 2109 'repo_commit': 'commit'
2158 2110 }
2159 2111
2160 2112 return route_to_view_type.get(route_name)
2161 2113
2162 2114
2163 2115 def is_active(menu_entry, selected):
2164 2116 """
2165 2117 Returns active class for selecting menus in templates
2166 2118 <li class=${h.is_active('settings', current_active)}></li>
2167 2119 """
2168 2120 if not isinstance(menu_entry, list):
2169 2121 menu_entry = [menu_entry]
2170 2122
2171 2123 if selected in menu_entry:
2172 2124 return "active"
2173 2125
2174 2126
2175 2127 class IssuesRegistry(object):
2176 2128 """
2177 2129 issue_registry = IssuesRegistry()
2178 2130 some_func(issues_callback=issues_registry(...))
2179 2131 """
2180 2132
2181 2133 def __init__(self):
2182 2134 self.issues = []
2183 2135 self.unique_issues = collections.defaultdict(lambda: [])
2184 2136
2185 2137 def __call__(self, commit_dict=None):
2186 2138 def callback(issue):
2187 2139 if commit_dict and issue:
2188 2140 issue['commit'] = commit_dict
2189 2141 self.issues.append(issue)
2190 2142 self.unique_issues[issue['id']].append(issue)
2191 2143 return callback
2192 2144
2193 2145 def get_issues(self):
2194 2146 return self.issues
2195 2147
2196 2148 @property
2197 2149 def issues_unique_count(self):
2198 2150 return len(set(i['id'] for i in self.issues))
2199 2151
2200 2152
2201 2153 def get_directory_statistics(start_path):
2202 2154 """
2203 2155 total_files, total_size, directory_stats = get_directory_statistics(start_path)
2204 2156
2205 2157 print(f"Directory statistics for: {start_path}\n")
2206 2158 print(f"Total files: {total_files}")
2207 2159 print(f"Total size: {format_size(total_size)}\n")
2208 2160
2209 2161 :param start_path:
2210 2162 :return:
2211 2163 """
2212 2164
2213 2165 total_files = 0
2214 2166 total_size = 0
2215 2167 directory_stats = {}
2216 2168
2217 2169 for dir_path, dir_names, file_names in os.walk(start_path):
2218 2170 dir_size = 0
2219 2171 file_count = len(file_names)
2220 2172
2221 2173 for fname in file_names:
2222 2174 filepath = os.path.join(dir_path, fname)
2223 2175 file_size = os.path.getsize(filepath)
2224 2176 dir_size += file_size
2225 2177
2226 2178 directory_stats[dir_path] = {'file_count': file_count, 'size': dir_size}
2227 2179 total_files += file_count
2228 2180 total_size += dir_size
2229 2181
2230 2182 return total_files, total_size, directory_stats
@@ -1,105 +1,104 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import sys
20 20 import logging
21 21
22 22 import click
23 23
24 24 from rhodecode.lib.pyramid_utils import bootstrap
25 25 from rhodecode.model.db import Session, User, Repository
26 26 from rhodecode.model.user import UserModel
27 27 from rhodecode.apps.file_store import utils as store_utils
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 @click.command()
33 33 @click.argument('ini_path', type=click.Path(exists=True))
34 34 @click.option(
35 35 '--filename',
36 36 required=True,
37 37 help='Filename for artifact.')
38 38 @click.option(
39 39 '--file-path',
40 40 required=True,
41 41 type=click.Path(exists=True, dir_okay=False, readable=True),
42 42 help='Path to a file to be added as artifact')
43 43 @click.option(
44 44 '--repo-id',
45 45 required=True,
46 46 type=int,
47 47 help='ID of repository to add this artifact to.')
48 48 @click.option(
49 49 '--user-id',
50 50 default=None,
51 51 type=int,
52 52 help='User ID for creator of artifact. '
53 53 'Default would be first super admin.')
54 54 @click.option(
55 55 '--description',
56 56 default=None,
57 57 type=str,
58 58 help='Add description to this artifact')
59 59 def main(ini_path, filename, file_path, repo_id, user_id, description):
60 60 return command(ini_path, filename, file_path, repo_id, user_id, description)
61 61
62 62
63 63 def command(ini_path, filename, file_path, repo_id, user_id, description):
64 64 with bootstrap(ini_path, env={'RC_CMD_SETUP_RC': '1'}) as env:
65 65 try:
66 66 from rc_ee.api.views.store_api import _store_file
67 67 except ImportError:
68 68 click.secho('ERROR: Unable to import store_api. '
69 69 'store_api is only available in EE edition of RhodeCode',
70 70 fg='red')
71 71 sys.exit(-1)
72 72
73 73 request = env['request']
74 74
75 75 repo = Repository.get(repo_id)
76 76 if not repo:
77 77 click.secho(f'ERROR: Unable to find repository with id `{repo_id}`',
78 78 fg='red')
79 79 sys.exit(-1)
80 80
81 81 # if we don't give user, or it's "DEFAULT" user we pick super-admin
82 82 if user_id is not None or user_id == 1:
83 83 db_user = User.get(user_id)
84 84 else:
85 85 db_user = User.get_first_super_admin()
86 86
87 87 if not db_user:
88 88 click.secho(f'ERROR: Unable to find user with id/username `{user_id}`',
89 89 fg='red')
90 90 sys.exit(-1)
91 91
92 92 auth_user = db_user.AuthUser(ip_addr='127.0.0.1')
93 93
94 storage = store_utils.get_file_storage(request.registry.settings)
94 f_store = store_utils.get_filestore_backend(request.registry.settings)
95 95
96 96 with open(file_path, 'rb') as f:
97 97 click.secho(f'Adding new artifact from path: `{file_path}`',
98 98 fg='green')
99 99
100 100 file_data = _store_file(
101 storage, auth_user, filename, content=None, check_acl=True,
101 f_store, auth_user, filename, content=None, check_acl=True,
102 102 file_obj=f, description=description,
103 103 scope_repo_id=repo.repo_id)
104 click.secho(f'File Data: {file_data}',
105 fg='green')
104 click.secho(f'File Data: {file_data}', fg='green')
@@ -1,183 +1,187 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import typing
20 20 import base64
21 21 import logging
22 22 from unidecode import unidecode
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.type_utils import aslist
26 26
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def safe_int(val, default=None) -> int:
32 32 """
33 33 Returns int() of val if val is not convertable to int use default
34 34 instead
35 35
36 36 :param val:
37 37 :param default:
38 38 """
39 39
40 40 try:
41 41 val = int(val)
42 42 except (ValueError, TypeError):
43 43 val = default
44 44
45 45 return val
46 46
47 47
48 48 def safe_float(val, default=None) -> float:
49 49 """
50 50 Returns float() of val if val is not convertable to float use default
51 51 instead
52 52
53 53 :param val:
54 54 :param default:
55 55 """
56 56
57 57 try:
58 58 val = float(val)
59 59 except (ValueError, TypeError):
60 60 val = default
61 61
62 62 return val
63 63
64 64
65 65 def base64_to_str(text: str | bytes) -> str:
66 66 return safe_str(base64.encodebytes(safe_bytes(text))).strip()
67 67
68 68
69 69 def get_default_encodings() -> list[str]:
70 70 return aslist(rhodecode.CONFIG.get('default_encoding', 'utf8'), sep=',')
71 71
72 72
73 73 DEFAULT_ENCODINGS = get_default_encodings()
74 74
75 75
76 76 def safe_str(str_, to_encoding=None) -> str:
77 77 """
78 78 safe str function. Does few trick to turn unicode_ into string
79 79
80 80 :param str_: str to encode
81 81 :param to_encoding: encode to this type UTF8 default
82 82 """
83 83 if isinstance(str_, str):
84 84 return str_
85 85
86 86 # if it's bytes cast to str
87 87 if not isinstance(str_, bytes):
88 88 return str(str_)
89 89
90 90 to_encoding = to_encoding or DEFAULT_ENCODINGS
91 91 if not isinstance(to_encoding, (list, tuple)):
92 92 to_encoding = [to_encoding]
93 93
94 94 for enc in to_encoding:
95 95 try:
96 96 return str(str_, enc)
97 97 except UnicodeDecodeError:
98 98 pass
99 99
100 100 return str(str_, to_encoding[0], 'replace')
101 101
102 102
103 103 def safe_bytes(str_, from_encoding=None) -> bytes:
104 104 """
105 105 safe bytes function. Does few trick to turn str_ into bytes string:
106 106
107 107 :param str_: string to decode
108 108 :param from_encoding: encode from this type UTF8 default
109 109 """
110 110 if isinstance(str_, bytes):
111 111 return str_
112 112
113 113 if not isinstance(str_, str):
114 114 raise ValueError(f'safe_bytes cannot convert other types than str: got: {type(str_)}')
115 115
116 116 from_encoding = from_encoding or get_default_encodings()
117 117 if not isinstance(from_encoding, (list, tuple)):
118 118 from_encoding = [from_encoding]
119 119
120 120 for enc in from_encoding:
121 121 try:
122 122 return str_.encode(enc)
123 123 except UnicodeDecodeError:
124 124 pass
125 125
126 126 return str_.encode(from_encoding[0], 'replace')
127 127
128 128
129 129 def ascii_bytes(str_, allow_bytes=False) -> bytes:
130 130 """
131 131 Simple conversion from str to bytes, with assumption that str_ is pure ASCII.
132 132 Fails with UnicodeError on invalid input.
133 133 This should be used where encoding and "safe" ambiguity should be avoided.
134 134 Where strings already have been encoded in other ways but still are unicode
135 135 string - for example to hex, base64, json, urlencoding, or are known to be
136 136 identifiers.
137 137 """
138 138 if allow_bytes and isinstance(str_, bytes):
139 139 return str_
140 140
141 141 if not isinstance(str_, str):
142 142 raise ValueError(f'ascii_bytes cannot convert other types than str: got: {type(str_)}')
143 143 return str_.encode('ascii')
144 144
145 145
146 146 def ascii_str(str_) -> str:
147 147 """
148 148 Simple conversion from bytes to str, with assumption that str_ is pure ASCII.
149 149 Fails with UnicodeError on invalid input.
150 150 This should be used where encoding and "safe" ambiguity should be avoided.
151 151 Where strings are encoded but also in other ways are known to be ASCII, and
152 152 where a unicode string is wanted without caring about encoding. For example
153 153 to hex, base64, urlencoding, or are known to be identifiers.
154 154 """
155 155
156 156 if not isinstance(str_, bytes):
157 157 raise ValueError(f'ascii_str cannot convert other types than bytes: got: {type(str_)}')
158 158 return str_.decode('ascii')
159 159
160 160
161 161 def convert_special_chars(str_) -> str:
162 162 """
163 163 trie to replace non-ascii letters to their ascii representation eg::
164 164
165 165 `żołw` converts into `zolw`
166 166 """
167 167 value = safe_str(str_)
168 168 converted_value = unidecode(value)
169 169 return converted_value
170 170
171 171
172 172 def splitnewlines(text: bytes):
173 173 """
174 174 like splitlines, but only split on newlines.
175 175 """
176 176
177 177 lines = [_l + b'\n' for _l in text.split(b'\n')]
178 178 if lines:
179 179 if lines[-1] == b'\n':
180 180 lines.pop()
181 181 else:
182 182 lines[-1] = lines[-1][:-1]
183 183 return lines
184
185
186 def header_safe_str(val):
187 return safe_bytes(val).decode('latin-1', errors='replace')
@@ -1,827 +1,866 b''
1 1 # Copyright (C) 2017-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19
20 20 import os
21 21 import sys
22 22 import time
23 23 import platform
24 24 import collections
25 25 import psutil
26 26 from functools import wraps
27 27
28 28 import pkg_resources
29 29 import logging
30 30 import resource
31 31
32 32 import configparser
33 33
34 34 from rc_license.models import LicenseModel
35 35 from rhodecode.lib.str_utils import safe_str
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 _NA = 'NOT AVAILABLE'
41 41 _NA_FLOAT = 0.0
42 42
43 43 STATE_OK = 'ok'
44 44 STATE_ERR = 'error'
45 45 STATE_WARN = 'warning'
46 46
47 47 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
48 48
49 49
50 50 registered_helpers = {}
51 51
52 52
53 53 def register_sysinfo(func):
54 54 """
55 55 @register_helper
56 56 def db_check():
57 57 pass
58 58
59 59 db_check == registered_helpers['db_check']
60 60 """
61 61 global registered_helpers
62 62 registered_helpers[func.__name__] = func
63 63
64 64 @wraps(func)
65 65 def _wrapper(*args, **kwargs):
66 66 return func(*args, **kwargs)
67 67 return _wrapper
68 68
69 69
70 70 # HELPERS
71 71 def percentage(part: (int, float), whole: (int, float)):
72 72 whole = float(whole)
73 73 if whole > 0:
74 74 return round(100 * float(part) / whole, 1)
75 75 return 0.0
76 76
77 77
78 78 def get_storage_size(storage_path):
79 79 sizes = []
80 80 for file_ in os.listdir(storage_path):
81 81 storage_file = os.path.join(storage_path, file_)
82 82 if os.path.isfile(storage_file):
83 83 try:
84 84 sizes.append(os.path.getsize(storage_file))
85 85 except OSError:
86 86 log.exception('Failed to get size of storage file %s', storage_file)
87 87 pass
88 88
89 89 return sum(sizes)
90 90
91 91
92 92 def get_resource(resource_type):
93 93 try:
94 94 return resource.getrlimit(resource_type)
95 95 except Exception:
96 96 return 'NOT_SUPPORTED'
97 97
98 98
99 99 def get_cert_path(ini_path):
100 100 default = '/etc/ssl/certs/ca-certificates.crt'
101 101 control_ca_bundle = os.path.join(
102 102 os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(ini_path)))),
103 103 '/etc/ssl/certs/ca-certificates.crt')
104 104 if os.path.isfile(control_ca_bundle):
105 105 default = control_ca_bundle
106 106
107 107 return default
108 108
109 109
110 110 class SysInfoRes(object):
111 111 def __init__(self, value, state=None, human_value=None):
112 112 self.value = value
113 113 self.state = state or STATE_OK_DEFAULT
114 114 self.human_value = human_value or value
115 115
116 116 def __json__(self):
117 117 return {
118 118 'value': self.value,
119 119 'state': self.state,
120 120 'human_value': self.human_value,
121 121 }
122 122
123 123 def get_value(self):
124 124 return self.__json__()
125 125
126 126 def __str__(self):
127 127 return f'<SysInfoRes({self.__json__()})>'
128 128
129 129
130 130 class SysInfo(object):
131 131
132 132 def __init__(self, func_name, **kwargs):
133 133 self.function_name = func_name
134 134 self.value = _NA
135 135 self.state = None
136 136 self.kwargs = kwargs or {}
137 137
138 138 def __call__(self):
139 139 computed = self.compute(**self.kwargs)
140 140 if not isinstance(computed, SysInfoRes):
141 141 raise ValueError(
142 142 'computed value for {} is not instance of '
143 143 '{}, got {} instead'.format(
144 144 self.function_name, SysInfoRes, type(computed)))
145 145 return computed.__json__()
146 146
147 147 def __str__(self):
148 148 return f'<SysInfo({self.function_name})>'
149 149
150 150 def compute(self, **kwargs):
151 151 return self.function_name(**kwargs)
152 152
153 153
154 154 # SysInfo functions
155 155 @register_sysinfo
156 156 def python_info():
157 157 value = dict(version=f'{platform.python_version()}:{platform.python_implementation()}',
158 158 executable=sys.executable)
159 159 return SysInfoRes(value=value)
160 160
161 161
162 162 @register_sysinfo
163 163 def py_modules():
164 164 mods = dict([(p.project_name, {'version': p.version, 'location': p.location})
165 165 for p in pkg_resources.working_set])
166 166
167 167 value = sorted(mods.items(), key=lambda k: k[0].lower())
168 168 return SysInfoRes(value=value)
169 169
170 170
171 171 @register_sysinfo
172 172 def platform_type():
173 173 from rhodecode.lib.utils import generate_platform_uuid
174 174
175 175 value = dict(
176 176 name=safe_str(platform.platform()),
177 177 uuid=generate_platform_uuid()
178 178 )
179 179 return SysInfoRes(value=value)
180 180
181 181
182 182 @register_sysinfo
183 183 def locale_info():
184 184 import locale
185 185
186 186 def safe_get_locale(locale_name):
187 187 try:
188 188 locale.getlocale(locale_name)
189 189 except TypeError:
190 190 return f'FAILED_LOCALE_GET:{locale_name}'
191 191
192 192 value = dict(
193 193 locale_default=locale.getlocale(),
194 194 locale_lc_all=safe_get_locale(locale.LC_ALL),
195 195 locale_lc_ctype=safe_get_locale(locale.LC_CTYPE),
196 196 lang_env=os.environ.get('LANG'),
197 197 lc_all_env=os.environ.get('LC_ALL'),
198 198 local_archive_env=os.environ.get('LOCALE_ARCHIVE'),
199 199 )
200 200 human_value = \
201 201 f"LANG: {value['lang_env']}, \
202 202 locale LC_ALL: {value['locale_lc_all']}, \
203 203 locale LC_CTYPE: {value['locale_lc_ctype']}, \
204 204 Default locales: {value['locale_default']}"
205 205
206 206 return SysInfoRes(value=value, human_value=human_value)
207 207
208 208
209 209 @register_sysinfo
210 210 def ulimit_info():
211 211 data = collections.OrderedDict([
212 212 ('cpu time (seconds)', get_resource(resource.RLIMIT_CPU)),
213 213 ('file size', get_resource(resource.RLIMIT_FSIZE)),
214 214 ('stack size', get_resource(resource.RLIMIT_STACK)),
215 215 ('core file size', get_resource(resource.RLIMIT_CORE)),
216 216 ('address space size', get_resource(resource.RLIMIT_AS)),
217 217 ('locked in mem size', get_resource(resource.RLIMIT_MEMLOCK)),
218 218 ('heap size', get_resource(resource.RLIMIT_DATA)),
219 219 ('rss size', get_resource(resource.RLIMIT_RSS)),
220 220 ('number of processes', get_resource(resource.RLIMIT_NPROC)),
221 221 ('open files', get_resource(resource.RLIMIT_NOFILE)),
222 222 ])
223 223
224 224 text = ', '.join(f'{k}:{v}' for k, v in data.items())
225 225
226 226 value = {
227 227 'limits': data,
228 228 'text': text,
229 229 }
230 230 return SysInfoRes(value=value)
231 231
232 232
233 233 @register_sysinfo
234 234 def uptime():
235 235 from rhodecode.lib.helpers import age, time_to_datetime
236 236 from rhodecode.translation import TranslationString
237 237
238 238 value = dict(boot_time=0, uptime=0, text='')
239 239 state = STATE_OK_DEFAULT
240 240
241 241 boot_time = psutil.boot_time()
242 242 value['boot_time'] = boot_time
243 243 value['uptime'] = time.time() - boot_time
244 244
245 245 date_or_age = age(time_to_datetime(boot_time))
246 246 if isinstance(date_or_age, TranslationString):
247 247 date_or_age = date_or_age.interpolate()
248 248
249 249 human_value = value.copy()
250 250 human_value['boot_time'] = time_to_datetime(boot_time)
251 251 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
252 252
253 253 human_value['text'] = f'Server started {date_or_age}'
254 254 return SysInfoRes(value=value, human_value=human_value)
255 255
256 256
257 257 @register_sysinfo
258 258 def memory():
259 259 from rhodecode.lib.helpers import format_byte_size_binary
260 260 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
261 261 percent_used=0, free=0, inactive=0, active=0, shared=0,
262 262 total=0, buffers=0, text='')
263 263
264 264 state = STATE_OK_DEFAULT
265 265
266 266 value.update(dict(psutil.virtual_memory()._asdict()))
267 267 value['used_real'] = value['total'] - value['available']
268 268 value['percent_used'] = psutil._common.usage_percent(value['used_real'], value['total'], 1)
269 269
270 270 human_value = value.copy()
271 271 human_value['text'] = '{}/{}, {}% used'.format(
272 272 format_byte_size_binary(value['used_real']),
273 273 format_byte_size_binary(value['total']),
274 274 value['percent_used'])
275 275
276 276 keys = list(value.keys())[::]
277 277 keys.pop(keys.index('percent'))
278 278 keys.pop(keys.index('percent_used'))
279 279 keys.pop(keys.index('text'))
280 280 for k in keys:
281 281 human_value[k] = format_byte_size_binary(value[k])
282 282
283 283 if state['type'] == STATE_OK and value['percent_used'] > 90:
284 284 msg = 'Critical: your available RAM memory is very low.'
285 285 state = {'message': msg, 'type': STATE_ERR}
286 286
287 287 elif state['type'] == STATE_OK and value['percent_used'] > 70:
288 288 msg = 'Warning: your available RAM memory is running low.'
289 289 state = {'message': msg, 'type': STATE_WARN}
290 290
291 291 return SysInfoRes(value=value, state=state, human_value=human_value)
292 292
293 293
294 294 @register_sysinfo
295 295 def machine_load():
296 296 value = {'1_min': _NA_FLOAT, '5_min': _NA_FLOAT, '15_min': _NA_FLOAT, 'text': ''}
297 297 state = STATE_OK_DEFAULT
298 298
299 299 # load averages
300 300 if hasattr(psutil.os, 'getloadavg'):
301 301 value.update(dict(
302 302 list(zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg()))
303 303 ))
304 304
305 305 human_value = value.copy()
306 306 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
307 307 value['1_min'], value['5_min'], value['15_min'])
308 308
309 309 if state['type'] == STATE_OK and value['15_min'] > 5.0:
310 310 msg = 'Warning: your machine load is very high.'
311 311 state = {'message': msg, 'type': STATE_WARN}
312 312
313 313 return SysInfoRes(value=value, state=state, human_value=human_value)
314 314
315 315
316 316 @register_sysinfo
317 317 def cpu():
318 318 value = {'cpu': 0, 'cpu_count': 0, 'cpu_usage': []}
319 319 state = STATE_OK_DEFAULT
320 320
321 321 value['cpu'] = psutil.cpu_percent(0.5)
322 322 value['cpu_usage'] = psutil.cpu_percent(0.5, percpu=True)
323 323 value['cpu_count'] = psutil.cpu_count()
324 324
325 325 human_value = value.copy()
326 326 human_value['text'] = f'{value["cpu_count"]} cores at {value["cpu"]} %'
327 327
328 328 return SysInfoRes(value=value, state=state, human_value=human_value)
329 329
330 330
331 331 @register_sysinfo
332 332 def storage():
333 333 from rhodecode.lib.helpers import format_byte_size_binary
334 334 from rhodecode.lib.utils import get_rhodecode_repo_store_path
335 335 path = get_rhodecode_repo_store_path()
336 336
337 337 value = dict(percent=0, used=0, total=0, path=path, text='')
338 338 state = STATE_OK_DEFAULT
339 339
340 340 try:
341 341 value.update(dict(psutil.disk_usage(path)._asdict()))
342 342 except Exception as e:
343 343 log.exception('Failed to fetch disk info')
344 344 state = {'message': str(e), 'type': STATE_ERR}
345 345
346 346 human_value = value.copy()
347 347 human_value['used'] = format_byte_size_binary(value['used'])
348 348 human_value['total'] = format_byte_size_binary(value['total'])
349 349 human_value['text'] = "{}/{}, {}% used".format(
350 350 format_byte_size_binary(value['used']),
351 351 format_byte_size_binary(value['total']),
352 352 value['percent'])
353 353
354 354 if state['type'] == STATE_OK and value['percent'] > 90:
355 355 msg = 'Critical: your disk space is very low.'
356 356 state = {'message': msg, 'type': STATE_ERR}
357 357
358 358 elif state['type'] == STATE_OK and value['percent'] > 70:
359 359 msg = 'Warning: your disk space is running low.'
360 360 state = {'message': msg, 'type': STATE_WARN}
361 361
362 362 return SysInfoRes(value=value, state=state, human_value=human_value)
363 363
364 364
365 365 @register_sysinfo
366 366 def storage_inodes():
367 367 from rhodecode.lib.utils import get_rhodecode_repo_store_path
368 368 path = get_rhodecode_repo_store_path()
369 369
370 370 value = dict(percent=0.0, free=0, used=0, total=0, path=path, text='')
371 371 state = STATE_OK_DEFAULT
372 372
373 373 try:
374 374 i_stat = os.statvfs(path)
375 375 value['free'] = i_stat.f_ffree
376 376 value['used'] = i_stat.f_files-i_stat.f_favail
377 377 value['total'] = i_stat.f_files
378 378 value['percent'] = percentage(value['used'], value['total'])
379 379 except Exception as e:
380 380 log.exception('Failed to fetch disk inodes info')
381 381 state = {'message': str(e), 'type': STATE_ERR}
382 382
383 383 human_value = value.copy()
384 384 human_value['text'] = "{}/{}, {}% used".format(
385 385 value['used'], value['total'], value['percent'])
386 386
387 387 if state['type'] == STATE_OK and value['percent'] > 90:
388 388 msg = 'Critical: your disk free inodes are very low.'
389 389 state = {'message': msg, 'type': STATE_ERR}
390 390
391 391 elif state['type'] == STATE_OK and value['percent'] > 70:
392 392 msg = 'Warning: your disk free inodes are running low.'
393 393 state = {'message': msg, 'type': STATE_WARN}
394 394
395 395 return SysInfoRes(value=value, state=state, human_value=human_value)
396 396
397 397
398 398 @register_sysinfo
399 def storage_archives():
399 def storage_artifacts():
400 400 import rhodecode
401 401 from rhodecode.lib.helpers import format_byte_size_binary
402 402 from rhodecode.lib.archive_cache import get_archival_cache_store
403 403
404 storage_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
404 backend_type = rhodecode.ConfigGet().get_str('archive_cache.backend.type')
405 405
406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=storage_type)
406 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
407 407 state = STATE_OK_DEFAULT
408 408 try:
409 409 d_cache = get_archival_cache_store(config=rhodecode.CONFIG)
410 backend_type = str(d_cache)
410 411
411 412 total_files, total_size, _directory_stats = d_cache.get_statistics()
412 413
413 414 value.update({
414 415 'percent': 100,
415 416 'used': total_size,
416 417 'total': total_size,
417 418 'items': total_files,
418 'path': d_cache.storage_path
419 'path': d_cache.storage_path,
420 'type': backend_type
419 421 })
420 422
421 423 except Exception as e:
422 424 log.exception('failed to fetch archive cache storage')
423 425 state = {'message': str(e), 'type': STATE_ERR}
424 426
425 427 human_value = value.copy()
426 428 human_value['used'] = format_byte_size_binary(value['used'])
427 429 human_value['total'] = format_byte_size_binary(value['total'])
428 human_value['text'] = "{} ({} items)".format(
429 human_value['used'], value['items'])
430 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
431
432 return SysInfoRes(value=value, state=state, human_value=human_value)
433
434
435 @register_sysinfo
436 def storage_archives():
437 import rhodecode
438 from rhodecode.lib.helpers import format_byte_size_binary
439 import rhodecode.apps.file_store.utils as store_utils
440 from rhodecode import CONFIG
441
442 backend_type = rhodecode.ConfigGet().get_str(store_utils.config_keys.backend_type)
443
444 value = dict(percent=0, used=0, total=0, items=0, path='', text='', type=backend_type)
445 state = STATE_OK_DEFAULT
446 try:
447 f_store = store_utils.get_filestore_backend(config=CONFIG)
448 backend_type = str(f_store)
449 total_files, total_size, _directory_stats = f_store.get_statistics()
450
451 value.update({
452 'percent': 100,
453 'used': total_size,
454 'total': total_size,
455 'items': total_files,
456 'path': f_store.storage_path,
457 'type': backend_type
458 })
459
460 except Exception as e:
461 log.exception('failed to fetch archive cache storage')
462 state = {'message': str(e), 'type': STATE_ERR}
463
464 human_value = value.copy()
465 human_value['used'] = format_byte_size_binary(value['used'])
466 human_value['total'] = format_byte_size_binary(value['total'])
467 human_value['text'] = f"{human_value['used']} ({value['items']} items)"
430 468
431 469 return SysInfoRes(value=value, state=state, human_value=human_value)
432 470
433 471
434 472 @register_sysinfo
435 473 def storage_gist():
436 474 from rhodecode.model.gist import GIST_STORE_LOC
437 475 from rhodecode.lib.utils import safe_str, get_rhodecode_repo_store_path
438 476 from rhodecode.lib.helpers import format_byte_size_binary, get_directory_statistics
439 477
440 478 path = safe_str(os.path.join(
441 479 get_rhodecode_repo_store_path(), GIST_STORE_LOC))
442 480
443 481 # gist storage
444 482 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
445 483 state = STATE_OK_DEFAULT
446 484
447 485 try:
448 486 total_files, total_size, _directory_stats = get_directory_statistics(path)
449 487 value.update({
450 488 'percent': 100,
451 489 'used': total_size,
452 490 'total': total_size,
453 491 'items': total_files
454 492 })
455 493 except Exception as e:
456 494 log.exception('failed to fetch gist storage items')
457 495 state = {'message': str(e), 'type': STATE_ERR}
458 496
459 497 human_value = value.copy()
460 498 human_value['used'] = format_byte_size_binary(value['used'])
461 499 human_value['total'] = format_byte_size_binary(value['total'])
462 500 human_value['text'] = "{} ({} items)".format(
463 501 human_value['used'], value['items'])
464 502
465 503 return SysInfoRes(value=value, state=state, human_value=human_value)
466 504
467 505
468 506 @register_sysinfo
469 507 def storage_temp():
470 508 import tempfile
471 509 from rhodecode.lib.helpers import format_byte_size_binary
472 510
473 511 path = tempfile.gettempdir()
474 512 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
475 513 state = STATE_OK_DEFAULT
476 514
477 515 if not psutil:
478 516 return SysInfoRes(value=value, state=state)
479 517
480 518 try:
481 519 value.update(dict(psutil.disk_usage(path)._asdict()))
482 520 except Exception as e:
483 521 log.exception('Failed to fetch temp dir info')
484 522 state = {'message': str(e), 'type': STATE_ERR}
485 523
486 524 human_value = value.copy()
487 525 human_value['used'] = format_byte_size_binary(value['used'])
488 526 human_value['total'] = format_byte_size_binary(value['total'])
489 527 human_value['text'] = "{}/{}, {}% used".format(
490 528 format_byte_size_binary(value['used']),
491 529 format_byte_size_binary(value['total']),
492 530 value['percent'])
493 531
494 532 return SysInfoRes(value=value, state=state, human_value=human_value)
495 533
496 534
497 535 @register_sysinfo
498 536 def search_info():
499 537 import rhodecode
500 538 from rhodecode.lib.index import searcher_from_config
501 539
502 540 backend = rhodecode.CONFIG.get('search.module', '')
503 541 location = rhodecode.CONFIG.get('search.location', '')
504 542
505 543 try:
506 544 searcher = searcher_from_config(rhodecode.CONFIG)
507 545 searcher = searcher.__class__.__name__
508 546 except Exception:
509 547 searcher = None
510 548
511 549 value = dict(
512 550 backend=backend, searcher=searcher, location=location, text='')
513 551 state = STATE_OK_DEFAULT
514 552
515 553 human_value = value.copy()
516 554 human_value['text'] = "backend:`{}`".format(human_value['backend'])
517 555
518 556 return SysInfoRes(value=value, state=state, human_value=human_value)
519 557
520 558
521 559 @register_sysinfo
522 560 def git_info():
523 561 from rhodecode.lib.vcs.backends import git
524 562 state = STATE_OK_DEFAULT
525 563 value = human_value = ''
526 564 try:
527 565 value = git.discover_git_version(raise_on_exc=True)
528 566 human_value = f'version reported from VCSServer: {value}'
529 567 except Exception as e:
530 568 state = {'message': str(e), 'type': STATE_ERR}
531 569
532 570 return SysInfoRes(value=value, state=state, human_value=human_value)
533 571
534 572
535 573 @register_sysinfo
536 574 def hg_info():
537 575 from rhodecode.lib.vcs.backends import hg
538 576 state = STATE_OK_DEFAULT
539 577 value = human_value = ''
540 578 try:
541 579 value = hg.discover_hg_version(raise_on_exc=True)
542 580 human_value = f'version reported from VCSServer: {value}'
543 581 except Exception as e:
544 582 state = {'message': str(e), 'type': STATE_ERR}
545 583 return SysInfoRes(value=value, state=state, human_value=human_value)
546 584
547 585
548 586 @register_sysinfo
549 587 def svn_info():
550 588 from rhodecode.lib.vcs.backends import svn
551 589 state = STATE_OK_DEFAULT
552 590 value = human_value = ''
553 591 try:
554 592 value = svn.discover_svn_version(raise_on_exc=True)
555 593 human_value = f'version reported from VCSServer: {value}'
556 594 except Exception as e:
557 595 state = {'message': str(e), 'type': STATE_ERR}
558 596 return SysInfoRes(value=value, state=state, human_value=human_value)
559 597
560 598
561 599 @register_sysinfo
562 600 def vcs_backends():
563 601 import rhodecode
564 602 value = rhodecode.CONFIG.get('vcs.backends')
565 603 human_value = 'Enabled backends in order: {}'.format(','.join(value))
566 604 return SysInfoRes(value=value, human_value=human_value)
567 605
568 606
569 607 @register_sysinfo
570 608 def vcs_server():
571 609 import rhodecode
572 610 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
573 611
574 612 server_url = rhodecode.CONFIG.get('vcs.server')
575 613 enabled = rhodecode.CONFIG.get('vcs.server.enable')
576 614 protocol = rhodecode.CONFIG.get('vcs.server.protocol') or 'http'
577 615 state = STATE_OK_DEFAULT
578 616 version = None
579 617 workers = 0
580 618
581 619 try:
582 620 data = get_vcsserver_service_data()
583 621 if data and 'version' in data:
584 622 version = data['version']
585 623
586 624 if data and 'config' in data:
587 625 conf = data['config']
588 626 workers = conf.get('workers', 'NOT AVAILABLE')
589 627
590 628 connection = 'connected'
591 629 except Exception as e:
592 630 connection = 'failed'
593 631 state = {'message': str(e), 'type': STATE_ERR}
594 632
595 633 value = dict(
596 634 url=server_url,
597 635 enabled=enabled,
598 636 protocol=protocol,
599 637 connection=connection,
600 638 version=version,
601 639 text='',
602 640 )
603 641
604 642 human_value = value.copy()
605 643 human_value['text'] = \
606 644 '{url}@ver:{ver} via {mode} mode[workers:{workers}], connection:{conn}'.format(
607 645 url=server_url, ver=version, workers=workers, mode=protocol,
608 646 conn=connection)
609 647
610 648 return SysInfoRes(value=value, state=state, human_value=human_value)
611 649
612 650
613 651 @register_sysinfo
614 652 def vcs_server_config():
615 653 from rhodecode.lib.vcs.backends import get_vcsserver_service_data
616 654 state = STATE_OK_DEFAULT
617 655
618 656 value = {}
619 657 try:
620 658 data = get_vcsserver_service_data()
621 659 value = data['app_config']
622 660 except Exception as e:
623 661 state = {'message': str(e), 'type': STATE_ERR}
624 662
625 663 human_value = value.copy()
626 664 human_value['text'] = 'VCS Server config'
627 665
628 666 return SysInfoRes(value=value, state=state, human_value=human_value)
629 667
630 668
631 669 @register_sysinfo
632 670 def rhodecode_app_info():
633 671 import rhodecode
634 672 edition = rhodecode.CONFIG.get('rhodecode.edition')
635 673
636 674 value = dict(
637 675 rhodecode_version=rhodecode.__version__,
638 676 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
639 677 text=''
640 678 )
641 679 human_value = value.copy()
642 680 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
643 681 edition=edition, ver=value['rhodecode_version']
644 682 )
645 683 return SysInfoRes(value=value, human_value=human_value)
646 684
647 685
648 686 @register_sysinfo
649 687 def rhodecode_config():
650 688 import rhodecode
651 689 path = rhodecode.CONFIG.get('__file__')
652 690 rhodecode_ini_safe = rhodecode.CONFIG.copy()
653 691 cert_path = get_cert_path(path)
654 692
655 693 try:
656 694 config = configparser.ConfigParser()
657 695 config.read(path)
658 696 parsed_ini = config
659 697 if parsed_ini.has_section('server:main'):
660 698 parsed_ini = dict(parsed_ini.items('server:main'))
661 699 except Exception:
662 700 log.exception('Failed to read .ini file for display')
663 701 parsed_ini = {}
664 702
665 703 rhodecode_ini_safe['server:main'] = parsed_ini
666 704
667 705 blacklist = [
668 706 f'rhodecode_{LicenseModel.LICENSE_DB_KEY}',
669 707 'routes.map',
670 708 'sqlalchemy.db1.url',
671 709 'channelstream.secret',
672 710 'beaker.session.secret',
673 711 'rhodecode.encrypted_values.secret',
674 712 'rhodecode_auth_github_consumer_key',
675 713 'rhodecode_auth_github_consumer_secret',
676 714 'rhodecode_auth_google_consumer_key',
677 715 'rhodecode_auth_google_consumer_secret',
678 716 'rhodecode_auth_bitbucket_consumer_secret',
679 717 'rhodecode_auth_bitbucket_consumer_key',
680 718 'rhodecode_auth_twitter_consumer_secret',
681 719 'rhodecode_auth_twitter_consumer_key',
682 720
683 721 'rhodecode_auth_twitter_secret',
684 722 'rhodecode_auth_github_secret',
685 723 'rhodecode_auth_google_secret',
686 724 'rhodecode_auth_bitbucket_secret',
687 725
688 726 'appenlight.api_key',
689 727 ('app_conf', 'sqlalchemy.db1.url')
690 728 ]
691 729 for k in blacklist:
692 730 if isinstance(k, tuple):
693 731 section, key = k
694 732 if section in rhodecode_ini_safe:
695 733 rhodecode_ini_safe[section] = '**OBFUSCATED**'
696 734 else:
697 735 rhodecode_ini_safe.pop(k, None)
698 736
699 737 # TODO: maybe put some CONFIG checks here ?
700 738 return SysInfoRes(value={'config': rhodecode_ini_safe,
701 739 'path': path, 'cert_path': cert_path})
702 740
703 741
704 742 @register_sysinfo
705 743 def database_info():
706 744 import rhodecode
707 745 from sqlalchemy.engine import url as engine_url
708 746 from rhodecode.model import meta
709 747 from rhodecode.model.meta import Session
710 748 from rhodecode.model.db import DbMigrateVersion
711 749
712 750 state = STATE_OK_DEFAULT
713 751
714 752 db_migrate = DbMigrateVersion.query().filter(
715 753 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
716 754
717 755 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
718 756
719 757 try:
720 758 engine = meta.get_engine()
721 759 db_server_info = engine.dialect._get_server_version_info(
722 760 Session.connection(bind=engine))
723 761 db_version = '.'.join(map(str, db_server_info))
724 762 except Exception:
725 763 log.exception('failed to fetch db version')
726 764 db_version = 'UNKNOWN'
727 765
728 766 db_info = dict(
729 767 migrate_version=db_migrate.version,
730 768 type=db_url_obj.get_backend_name(),
731 769 version=db_version,
732 770 url=repr(db_url_obj)
733 771 )
734 772 current_version = db_migrate.version
735 773 expected_version = rhodecode.__dbversion__
736 774 if state['type'] == STATE_OK and current_version != expected_version:
737 775 msg = 'Critical: database schema mismatch, ' \
738 776 'expected version {}, got {}. ' \
739 777 'Please run migrations on your database.'.format(
740 778 expected_version, current_version)
741 779 state = {'message': msg, 'type': STATE_ERR}
742 780
743 781 human_value = db_info.copy()
744 782 human_value['url'] = "{} @ migration version: {}".format(
745 783 db_info['url'], db_info['migrate_version'])
746 784 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
747 785 return SysInfoRes(value=db_info, state=state, human_value=human_value)
748 786
749 787
750 788 @register_sysinfo
751 789 def server_info(environ):
752 790 import rhodecode
753 791 from rhodecode.lib.base import get_server_ip_addr, get_server_port
754 792
755 793 value = {
756 794 'server_ip': '{}:{}'.format(
757 795 get_server_ip_addr(environ, log_errors=False),
758 796 get_server_port(environ)
759 797 ),
760 798 'server_id': rhodecode.CONFIG.get('instance_id'),
761 799 }
762 800 return SysInfoRes(value=value)
763 801
764 802
765 803 @register_sysinfo
766 804 def usage_info():
767 805 from rhodecode.model.db import User, Repository, true
768 806 value = {
769 807 'users': User.query().count(),
770 808 'users_active': User.query().filter(User.active == true()).count(),
771 809 'repositories': Repository.query().count(),
772 810 'repository_types': {
773 811 'hg': Repository.query().filter(
774 812 Repository.repo_type == 'hg').count(),
775 813 'git': Repository.query().filter(
776 814 Repository.repo_type == 'git').count(),
777 815 'svn': Repository.query().filter(
778 816 Repository.repo_type == 'svn').count(),
779 817 },
780 818 }
781 819 return SysInfoRes(value=value)
782 820
783 821
784 822 def get_system_info(environ):
785 823 environ = environ or {}
786 824 return {
787 825 'rhodecode_app': SysInfo(rhodecode_app_info)(),
788 826 'rhodecode_config': SysInfo(rhodecode_config)(),
789 827 'rhodecode_usage': SysInfo(usage_info)(),
790 828 'python': SysInfo(python_info)(),
791 829 'py_modules': SysInfo(py_modules)(),
792 830
793 831 'platform': SysInfo(platform_type)(),
794 832 'locale': SysInfo(locale_info)(),
795 833 'server': SysInfo(server_info, environ=environ)(),
796 834 'database': SysInfo(database_info)(),
797 835 'ulimit': SysInfo(ulimit_info)(),
798 836 'storage': SysInfo(storage)(),
799 837 'storage_inodes': SysInfo(storage_inodes)(),
800 838 'storage_archive': SysInfo(storage_archives)(),
839 'storage_artifacts': SysInfo(storage_artifacts)(),
801 840 'storage_gist': SysInfo(storage_gist)(),
802 841 'storage_temp': SysInfo(storage_temp)(),
803 842
804 843 'search': SysInfo(search_info)(),
805 844
806 845 'uptime': SysInfo(uptime)(),
807 846 'load': SysInfo(machine_load)(),
808 847 'cpu': SysInfo(cpu)(),
809 848 'memory': SysInfo(memory)(),
810 849
811 850 'vcs_backends': SysInfo(vcs_backends)(),
812 851 'vcs_server': SysInfo(vcs_server)(),
813 852
814 853 'vcs_server_config': SysInfo(vcs_server_config)(),
815 854
816 855 'git': SysInfo(git_info)(),
817 856 'hg': SysInfo(hg_info)(),
818 857 'svn': SysInfo(svn_info)(),
819 858 }
820 859
821 860
822 861 def load_system_info(key):
823 862 """
824 863 get_sys_info('vcs_server')
825 864 get_sys_info('database')
826 865 """
827 866 return SysInfo(registered_helpers[key])()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now