##// END OF EJS Templates
gists: define and use explicit Mercurial backend to speed up creation and fetching of backend repo
marcink -
r3536:5ee2c115 default
parent child Browse files
Show More

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

@@ -1,413 +1,412 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2019 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.utils2 import time_to_datetime
36 from rhodecode.lib.utils2 import time_to_datetime
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 from rhodecode.model.gist import GistModel
39 from rhodecode.model.gist import GistModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.db import Gist, User, or_
41 from rhodecode.model.db import Gist, User, or_
42 from rhodecode.model import validation_schema
42 from rhodecode.model import validation_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class GistView(BaseAppView):
49 class GistView(BaseAppView):
50
50
51 def load_default_context(self):
51 def load_default_context(self):
52 _ = self.request.translate
52 _ = self.request.translate
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 c.user = c.auth_user.get_instance()
54 c.user = c.auth_user.get_instance()
55
55
56 c.lifetime_values = [
56 c.lifetime_values = [
57 (-1, _('forever')),
57 (-1, _('forever')),
58 (5, _('5 minutes')),
58 (5, _('5 minutes')),
59 (60, _('1 hour')),
59 (60, _('1 hour')),
60 (60 * 24, _('1 day')),
60 (60 * 24, _('1 day')),
61 (60 * 24 * 30, _('1 month')),
61 (60 * 24 * 30, _('1 month')),
62 ]
62 ]
63
63
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 c.acl_options = [
65 c.acl_options = [
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 ]
68 ]
69
69
70
71 return c
70 return c
72
71
73 @LoginRequired()
72 @LoginRequired()
74 @view_config(
73 @view_config(
75 route_name='gists_show', request_method='GET',
74 route_name='gists_show', request_method='GET',
76 renderer='rhodecode:templates/admin/gists/index.mako')
75 renderer='rhodecode:templates/admin/gists/index.mako')
77 def gist_show_all(self):
76 def gist_show_all(self):
78 c = self.load_default_context()
77 c = self.load_default_context()
79
78
80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
79 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
81 c.show_private = self.request.GET.get('private') and not_default_user
80 c.show_private = self.request.GET.get('private') and not_default_user
82 c.show_public = self.request.GET.get('public') and not_default_user
81 c.show_public = self.request.GET.get('public') and not_default_user
83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
82 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
84
83
85 gists = _gists = Gist().query()\
84 gists = _gists = Gist().query()\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
85 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 .order_by(Gist.created_on.desc())
86 .order_by(Gist.created_on.desc())
88
87
89 c.active = 'public'
88 c.active = 'public'
90 # MY private
89 # MY private
91 if c.show_private and not c.show_public:
90 if c.show_private and not c.show_public:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
91 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
92 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 c.active = 'my_private'
93 c.active = 'my_private'
95 # MY public
94 # MY public
96 elif c.show_public and not c.show_private:
95 elif c.show_public and not c.show_private:
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
96 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
97 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 c.active = 'my_public'
98 c.active = 'my_public'
100 # MY public+private
99 # MY public+private
101 elif c.show_private and c.show_public:
100 elif c.show_private and c.show_public:
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
101 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 Gist.gist_type == Gist.GIST_PRIVATE))\
102 Gist.gist_type == Gist.GIST_PRIVATE))\
104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
103 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
105 c.active = 'my_all'
104 c.active = 'my_all'
106 # Show all by super-admin
105 # Show all by super-admin
107 elif c.show_all:
106 elif c.show_all:
108 c.active = 'all'
107 c.active = 'all'
109 gists = _gists
108 gists = _gists
110
109
111 # default show ALL public gists
110 # default show ALL public gists
112 if not c.show_public and not c.show_private and not c.show_all:
111 if not c.show_public and not c.show_private and not c.show_all:
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
112 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 c.active = 'public'
113 c.active = 'public'
115
114
116 _render = self.request.get_partial_renderer(
115 _render = self.request.get_partial_renderer(
117 'rhodecode:templates/data_table/_dt_elements.mako')
116 'rhodecode:templates/data_table/_dt_elements.mako')
118
117
119 data = []
118 data = []
120
119
121 for gist in gists:
120 for gist in gists:
122 data.append({
121 data.append({
123 'created_on': _render('gist_created', gist.created_on),
122 'created_on': _render('gist_created', gist.created_on),
124 'created_on_raw': gist.created_on,
123 'created_on_raw': gist.created_on,
125 'type': _render('gist_type', gist.gist_type),
124 'type': _render('gist_type', gist.gist_type),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
125 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
126 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 'author_raw': h.escape(gist.owner.full_contact),
127 'author_raw': h.escape(gist.owner.full_contact),
129 'expires': _render('gist_expires', gist.gist_expires),
128 'expires': _render('gist_expires', gist.gist_expires),
130 'description': _render('gist_description', gist.gist_description)
129 'description': _render('gist_description', gist.gist_description)
131 })
130 })
132 c.data = json.dumps(data)
131 c.data = json.dumps(data)
133
132
134 return self._get_template_context(c)
133 return self._get_template_context(c)
135
134
136 @LoginRequired()
135 @LoginRequired()
137 @NotAnonymous()
136 @NotAnonymous()
138 @view_config(
137 @view_config(
139 route_name='gists_new', request_method='GET',
138 route_name='gists_new', request_method='GET',
140 renderer='rhodecode:templates/admin/gists/new.mako')
139 renderer='rhodecode:templates/admin/gists/new.mako')
141 def gist_new(self):
140 def gist_new(self):
142 c = self.load_default_context()
141 c = self.load_default_context()
143 return self._get_template_context(c)
142 return self._get_template_context(c)
144
143
145 @LoginRequired()
144 @LoginRequired()
146 @NotAnonymous()
145 @NotAnonymous()
147 @CSRFRequired()
146 @CSRFRequired()
148 @view_config(
147 @view_config(
149 route_name='gists_create', request_method='POST',
148 route_name='gists_create', request_method='POST',
150 renderer='rhodecode:templates/admin/gists/new.mako')
149 renderer='rhodecode:templates/admin/gists/new.mako')
151 def gist_create(self):
150 def gist_create(self):
152 _ = self.request.translate
151 _ = self.request.translate
153 c = self.load_default_context()
152 c = self.load_default_context()
154
153
155 data = dict(self.request.POST)
154 data = dict(self.request.POST)
156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
155 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
157 data['nodes'] = [{
156 data['nodes'] = [{
158 'filename': data['filename'],
157 'filename': data['filename'],
159 'content': data.get('content'),
158 'content': data.get('content'),
160 'mimetype': data.get('mimetype') # None is autodetect
159 'mimetype': data.get('mimetype') # None is autodetect
161 }]
160 }]
162
161
163 data['gist_type'] = (
162 data['gist_type'] = (
164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
163 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
165 data['gist_acl_level'] = (
164 data['gist_acl_level'] = (
166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
165 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
167
166
168 schema = gist_schema.GistSchema().bind(
167 schema = gist_schema.GistSchema().bind(
169 lifetime_options=[x[0] for x in c.lifetime_values])
168 lifetime_options=[x[0] for x in c.lifetime_values])
170
169
171 try:
170 try:
172
171
173 schema_data = schema.deserialize(data)
172 schema_data = schema.deserialize(data)
174 # convert to safer format with just KEYs so we sure no duplicates
173 # convert to safer format with just KEYs so we sure no duplicates
175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
174 schema_data['nodes'] = gist_schema.sequence_to_nodes(
176 schema_data['nodes'])
175 schema_data['nodes'])
177
176
178 gist = GistModel().create(
177 gist = GistModel().create(
179 gist_id=schema_data['gistid'], # custom access id not real ID
178 gist_id=schema_data['gistid'], # custom access id not real ID
180 description=schema_data['description'],
179 description=schema_data['description'],
181 owner=self._rhodecode_user.user_id,
180 owner=self._rhodecode_user.user_id,
182 gist_mapping=schema_data['nodes'],
181 gist_mapping=schema_data['nodes'],
183 gist_type=schema_data['gist_type'],
182 gist_type=schema_data['gist_type'],
184 lifetime=schema_data['lifetime'],
183 lifetime=schema_data['lifetime'],
185 gist_acl_level=schema_data['gist_acl_level']
184 gist_acl_level=schema_data['gist_acl_level']
186 )
185 )
187 Session().commit()
186 Session().commit()
188 new_gist_id = gist.gist_access_id
187 new_gist_id = gist.gist_access_id
189 except validation_schema.Invalid as errors:
188 except validation_schema.Invalid as errors:
190 defaults = data
189 defaults = data
191 errors = errors.asdict()
190 errors = errors.asdict()
192
191
193 if 'nodes.0.content' in errors:
192 if 'nodes.0.content' in errors:
194 errors['content'] = errors['nodes.0.content']
193 errors['content'] = errors['nodes.0.content']
195 del errors['nodes.0.content']
194 del errors['nodes.0.content']
196 if 'nodes.0.filename' in errors:
195 if 'nodes.0.filename' in errors:
197 errors['filename'] = errors['nodes.0.filename']
196 errors['filename'] = errors['nodes.0.filename']
198 del errors['nodes.0.filename']
197 del errors['nodes.0.filename']
199
198
200 data = render('rhodecode:templates/admin/gists/new.mako',
199 data = render('rhodecode:templates/admin/gists/new.mako',
201 self._get_template_context(c), self.request)
200 self._get_template_context(c), self.request)
202 html = formencode.htmlfill.render(
201 html = formencode.htmlfill.render(
203 data,
202 data,
204 defaults=defaults,
203 defaults=defaults,
205 errors=errors,
204 errors=errors,
206 prefix_error=False,
205 prefix_error=False,
207 encoding="UTF-8",
206 encoding="UTF-8",
208 force_defaults=False
207 force_defaults=False
209 )
208 )
210 return Response(html)
209 return Response(html)
211
210
212 except Exception:
211 except Exception:
213 log.exception("Exception while trying to create a gist")
212 log.exception("Exception while trying to create a gist")
214 h.flash(_('Error occurred during gist creation'), category='error')
213 h.flash(_('Error occurred during gist creation'), category='error')
215 raise HTTPFound(h.route_url('gists_new'))
214 raise HTTPFound(h.route_url('gists_new'))
216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
215 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
217
216
218 @LoginRequired()
217 @LoginRequired()
219 @NotAnonymous()
218 @NotAnonymous()
220 @CSRFRequired()
219 @CSRFRequired()
221 @view_config(
220 @view_config(
222 route_name='gist_delete', request_method='POST')
221 route_name='gist_delete', request_method='POST')
223 def gist_delete(self):
222 def gist_delete(self):
224 _ = self.request.translate
223 _ = self.request.translate
225 gist_id = self.request.matchdict['gist_id']
224 gist_id = self.request.matchdict['gist_id']
226
225
227 c = self.load_default_context()
226 c = self.load_default_context()
228 c.gist = Gist.get_or_404(gist_id)
227 c.gist = Gist.get_or_404(gist_id)
229
228
230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
229 owner = c.gist.gist_owner == self._rhodecode_user.user_id
231 if not (h.HasPermissionAny('hg.admin')() or owner):
230 if not (h.HasPermissionAny('hg.admin')() or owner):
232 log.warning('Deletion of Gist was forbidden '
231 log.warning('Deletion of Gist was forbidden '
233 'by unauthorized user: `%s`', self._rhodecode_user)
232 'by unauthorized user: `%s`', self._rhodecode_user)
234 raise HTTPNotFound()
233 raise HTTPNotFound()
235
234
236 GistModel().delete(c.gist)
235 GistModel().delete(c.gist)
237 Session().commit()
236 Session().commit()
238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
237 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
239
238
240 raise HTTPFound(h.route_url('gists_show'))
239 raise HTTPFound(h.route_url('gists_show'))
241
240
242 def _get_gist(self, gist_id):
241 def _get_gist(self, gist_id):
243
242
244 gist = Gist.get_or_404(gist_id)
243 gist = Gist.get_or_404(gist_id)
245
244
246 # Check if this gist is expired
245 # Check if this gist is expired
247 if gist.gist_expires != -1:
246 if gist.gist_expires != -1:
248 if time.time() > gist.gist_expires:
247 if time.time() > gist.gist_expires:
249 log.error(
248 log.error(
250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
249 'Gist expired at %s', time_to_datetime(gist.gist_expires))
251 raise HTTPNotFound()
250 raise HTTPNotFound()
252
251
253 # check if this gist requires a login
252 # check if this gist requires a login
254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
253 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
254 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
256 log.error("Anonymous user %s tried to access protected gist `%s`",
255 log.error("Anonymous user %s tried to access protected gist `%s`",
257 self._rhodecode_user, gist_id)
256 self._rhodecode_user, gist_id)
258 raise HTTPNotFound()
257 raise HTTPNotFound()
259 return gist
258 return gist
260
259
261 @LoginRequired()
260 @LoginRequired()
262 @view_config(
261 @view_config(
263 route_name='gist_show', request_method='GET',
262 route_name='gist_show', request_method='GET',
264 renderer='rhodecode:templates/admin/gists/show.mako')
263 renderer='rhodecode:templates/admin/gists/show.mako')
265 @view_config(
264 @view_config(
266 route_name='gist_show_rev', request_method='GET',
265 route_name='gist_show_rev', request_method='GET',
267 renderer='rhodecode:templates/admin/gists/show.mako')
266 renderer='rhodecode:templates/admin/gists/show.mako')
268 @view_config(
267 @view_config(
269 route_name='gist_show_formatted', request_method='GET',
268 route_name='gist_show_formatted', request_method='GET',
270 renderer=None)
269 renderer=None)
271 @view_config(
270 @view_config(
272 route_name='gist_show_formatted_path', request_method='GET',
271 route_name='gist_show_formatted_path', request_method='GET',
273 renderer=None)
272 renderer=None)
274 def gist_show(self):
273 def gist_show(self):
275 gist_id = self.request.matchdict['gist_id']
274 gist_id = self.request.matchdict['gist_id']
276
275
277 # TODO(marcink): expose those via matching dict
276 # TODO(marcink): expose those via matching dict
278 revision = self.request.matchdict.get('revision', 'tip')
277 revision = self.request.matchdict.get('revision', 'tip')
279 f_path = self.request.matchdict.get('f_path', None)
278 f_path = self.request.matchdict.get('f_path', None)
280 return_format = self.request.matchdict.get('format')
279 return_format = self.request.matchdict.get('format')
281
280
282 c = self.load_default_context()
281 c = self.load_default_context()
283 c.gist = self._get_gist(gist_id)
282 c.gist = self._get_gist(gist_id)
284 c.render = not self.request.GET.get('no-render', False)
283 c.render = not self.request.GET.get('no-render', False)
285
284
286 try:
285 try:
287 c.file_last_commit, c.files = GistModel().get_gist_files(
286 c.file_last_commit, c.files = GistModel().get_gist_files(
288 gist_id, revision=revision)
287 gist_id, revision=revision)
289 except VCSError:
288 except VCSError:
290 log.exception("Exception in gist show")
289 log.exception("Exception in gist show")
291 raise HTTPNotFound()
290 raise HTTPNotFound()
292
291
293 if return_format == 'raw':
292 if return_format == 'raw':
294 content = '\n\n'.join([f.content for f in c.files
293 content = '\n\n'.join([f.content for f in c.files
295 if (f_path is None or f.path == f_path)])
294 if (f_path is None or f.path == f_path)])
296 response = Response(content)
295 response = Response(content)
297 response.content_type = 'text/plain'
296 response.content_type = 'text/plain'
298 return response
297 return response
299
298
300 return self._get_template_context(c)
299 return self._get_template_context(c)
301
300
302 @LoginRequired()
301 @LoginRequired()
303 @NotAnonymous()
302 @NotAnonymous()
304 @view_config(
303 @view_config(
305 route_name='gist_edit', request_method='GET',
304 route_name='gist_edit', request_method='GET',
306 renderer='rhodecode:templates/admin/gists/edit.mako')
305 renderer='rhodecode:templates/admin/gists/edit.mako')
307 def gist_edit(self):
306 def gist_edit(self):
308 _ = self.request.translate
307 _ = self.request.translate
309 gist_id = self.request.matchdict['gist_id']
308 gist_id = self.request.matchdict['gist_id']
310 c = self.load_default_context()
309 c = self.load_default_context()
311 c.gist = self._get_gist(gist_id)
310 c.gist = self._get_gist(gist_id)
312
311
313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
312 owner = c.gist.gist_owner == self._rhodecode_user.user_id
314 if not (h.HasPermissionAny('hg.admin')() or owner):
313 if not (h.HasPermissionAny('hg.admin')() or owner):
315 raise HTTPNotFound()
314 raise HTTPNotFound()
316
315
317 try:
316 try:
318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
317 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
319 except VCSError:
318 except VCSError:
320 log.exception("Exception in gist edit")
319 log.exception("Exception in gist edit")
321 raise HTTPNotFound()
320 raise HTTPNotFound()
322
321
323 if c.gist.gist_expires == -1:
322 if c.gist.gist_expires == -1:
324 expiry = _('never')
323 expiry = _('never')
325 else:
324 else:
326 # this cannot use timeago, since it's used in select2 as a value
325 # this cannot use timeago, since it's used in select2 as a value
327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
326 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
328
327
329 c.lifetime_values.append(
328 c.lifetime_values.append(
330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
329 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
331 )
330 )
332
331
333 return self._get_template_context(c)
332 return self._get_template_context(c)
334
333
335 @LoginRequired()
334 @LoginRequired()
336 @NotAnonymous()
335 @NotAnonymous()
337 @CSRFRequired()
336 @CSRFRequired()
338 @view_config(
337 @view_config(
339 route_name='gist_update', request_method='POST',
338 route_name='gist_update', request_method='POST',
340 renderer='rhodecode:templates/admin/gists/edit.mako')
339 renderer='rhodecode:templates/admin/gists/edit.mako')
341 def gist_update(self):
340 def gist_update(self):
342 _ = self.request.translate
341 _ = self.request.translate
343 gist_id = self.request.matchdict['gist_id']
342 gist_id = self.request.matchdict['gist_id']
344 c = self.load_default_context()
343 c = self.load_default_context()
345 c.gist = self._get_gist(gist_id)
344 c.gist = self._get_gist(gist_id)
346
345
347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
346 owner = c.gist.gist_owner == self._rhodecode_user.user_id
348 if not (h.HasPermissionAny('hg.admin')() or owner):
347 if not (h.HasPermissionAny('hg.admin')() or owner):
349 raise HTTPNotFound()
348 raise HTTPNotFound()
350
349
351 data = peppercorn.parse(self.request.POST.items())
350 data = peppercorn.parse(self.request.POST.items())
352
351
353 schema = gist_schema.GistSchema()
352 schema = gist_schema.GistSchema()
354 schema = schema.bind(
353 schema = schema.bind(
355 # '0' is special value to leave lifetime untouched
354 # '0' is special value to leave lifetime untouched
356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
355 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
357 )
356 )
358
357
359 try:
358 try:
360 schema_data = schema.deserialize(data)
359 schema_data = schema.deserialize(data)
361 # convert to safer format with just KEYs so we sure no duplicates
360 # convert to safer format with just KEYs so we sure no duplicates
362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
361 schema_data['nodes'] = gist_schema.sequence_to_nodes(
363 schema_data['nodes'])
362 schema_data['nodes'])
364
363
365 GistModel().update(
364 GistModel().update(
366 gist=c.gist,
365 gist=c.gist,
367 description=schema_data['description'],
366 description=schema_data['description'],
368 owner=c.gist.owner,
367 owner=c.gist.owner,
369 gist_mapping=schema_data['nodes'],
368 gist_mapping=schema_data['nodes'],
370 lifetime=schema_data['lifetime'],
369 lifetime=schema_data['lifetime'],
371 gist_acl_level=schema_data['gist_acl_level']
370 gist_acl_level=schema_data['gist_acl_level']
372 )
371 )
373
372
374 Session().commit()
373 Session().commit()
375 h.flash(_('Successfully updated gist content'), category='success')
374 h.flash(_('Successfully updated gist content'), category='success')
376 except NodeNotChangedError:
375 except NodeNotChangedError:
377 # raised if nothing was changed in repo itself. We anyway then
376 # raised if nothing was changed in repo itself. We anyway then
378 # store only DB stuff for gist
377 # store only DB stuff for gist
379 Session().commit()
378 Session().commit()
380 h.flash(_('Successfully updated gist data'), category='success')
379 h.flash(_('Successfully updated gist data'), category='success')
381 except validation_schema.Invalid as errors:
380 except validation_schema.Invalid as errors:
382 errors = h.escape(errors.asdict())
381 errors = h.escape(errors.asdict())
383 h.flash(_('Error occurred during update of gist {}: {}').format(
382 h.flash(_('Error occurred during update of gist {}: {}').format(
384 gist_id, errors), category='error')
383 gist_id, errors), category='error')
385 except Exception:
384 except Exception:
386 log.exception("Exception in gist edit")
385 log.exception("Exception in gist edit")
387 h.flash(_('Error occurred during update of gist %s') % gist_id,
386 h.flash(_('Error occurred during update of gist %s') % gist_id,
388 category='error')
387 category='error')
389
388
390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
389 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
391
390
392 @LoginRequired()
391 @LoginRequired()
393 @NotAnonymous()
392 @NotAnonymous()
394 @view_config(
393 @view_config(
395 route_name='gist_edit_check_revision', request_method='GET',
394 route_name='gist_edit_check_revision', request_method='GET',
396 renderer='json_ext')
395 renderer='json_ext')
397 def gist_edit_check_revision(self):
396 def gist_edit_check_revision(self):
398 _ = self.request.translate
397 _ = self.request.translate
399 gist_id = self.request.matchdict['gist_id']
398 gist_id = self.request.matchdict['gist_id']
400 c = self.load_default_context()
399 c = self.load_default_context()
401 c.gist = self._get_gist(gist_id)
400 c.gist = self._get_gist(gist_id)
402
401
403 last_rev = c.gist.scm_instance().get_commit()
402 last_rev = c.gist.scm_instance().get_commit()
404 success = True
403 success = True
405 revision = self.request.GET.get('revision')
404 revision = self.request.GET.get('revision')
406
405
407 if revision != last_rev.raw_id:
406 if revision != last_rev.raw_id:
408 log.error('Last revision %s is different then submitted %s',
407 log.error('Last revision %s is different then submitted %s',
409 revision, last_rev)
408 revision, last_rev)
410 # our gist has newer version than we
409 # our gist has newer version than we
411 success = False
410 success = False
412
411
413 return {'success': success}
412 return {'success': success}
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,255 +1,256 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2019 RhodeCode GmbH
3 # Copyright (C) 2013-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 gist model for RhodeCode
22 gist model for RhodeCode
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import shutil
29 import shutil
30
30
31 from pyramid.threadlocal import get_current_request
31 from pyramid.threadlocal import get_current_request
32
32
33 from rhodecode.lib.utils2 import (
33 from rhodecode.lib.utils2 import (
34 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
34 safe_unicode, unique_id, safe_int, time_to_datetime, AttributeDict)
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs import VCSError
36 from rhodecode.lib.vcs import VCSError
37 from rhodecode.model import BaseModel
37 from rhodecode.model import BaseModel
38 from rhodecode.model.db import Gist
38 from rhodecode.model.db import Gist
39 from rhodecode.model.repo import RepoModel
39 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.scm import ScmModel
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 GIST_STORE_LOC = '.rc_gist_store'
44 GIST_STORE_LOC = '.rc_gist_store'
45 GIST_METADATA_FILE = '.rc_gist_metadata'
45 GIST_METADATA_FILE = '.rc_gist_metadata'
46
46
47
47
48 class GistModel(BaseModel):
48 class GistModel(BaseModel):
49 cls = Gist
49 cls = Gist
50 vcs_backend = 'hg'
50
51
51 def _get_gist(self, gist):
52 def _get_gist(self, gist):
52 """
53 """
53 Helper method to get gist by ID, or gist_access_id as a fallback
54 Helper method to get gist by ID, or gist_access_id as a fallback
54
55
55 :param gist: GistID, gist_access_id, or Gist instance
56 :param gist: GistID, gist_access_id, or Gist instance
56 """
57 """
57 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
58 return self._get_instance(Gist, gist, callback=Gist.get_by_access_id)
58
59
59 def __delete_gist(self, gist):
60 def __delete_gist(self, gist):
60 """
61 """
61 removes gist from filesystem
62 removes gist from filesystem
62
63
63 :param gist: gist object
64 :param gist: gist object
64 """
65 """
65 root_path = RepoModel().repos_path
66 root_path = RepoModel().repos_path
66 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
67 rm_path = os.path.join(root_path, GIST_STORE_LOC, gist.gist_access_id)
67 log.info("Removing %s", rm_path)
68 log.info("Removing %s", rm_path)
68 shutil.rmtree(rm_path)
69 shutil.rmtree(rm_path)
69
70
70 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
71 def _store_metadata(self, repo, gist_id, gist_access_id, user_id, username,
71 gist_type, gist_expires, gist_acl_level):
72 gist_type, gist_expires, gist_acl_level):
72 """
73 """
73 store metadata inside the gist repo, this can be later used for imports
74 store metadata inside the gist repo, this can be later used for imports
74 or gist identification. Currently we use this inside RhodeCode tools
75 or gist identification. Currently we use this inside RhodeCode tools
75 to do cleanup of gists that are in storage but not in database.
76 to do cleanup of gists that are in storage but not in database.
76 """
77 """
77 metadata = {
78 metadata = {
78 'metadata_version': '2',
79 'metadata_version': '2',
79 'gist_db_id': gist_id,
80 'gist_db_id': gist_id,
80 'gist_access_id': gist_access_id,
81 'gist_access_id': gist_access_id,
81 'gist_owner_id': user_id,
82 'gist_owner_id': user_id,
82 'gist_owner_username': username,
83 'gist_owner_username': username,
83 'gist_type': gist_type,
84 'gist_type': gist_type,
84 'gist_expires': gist_expires,
85 'gist_expires': gist_expires,
85 'gist_updated': time.time(),
86 'gist_updated': time.time(),
86 'gist_acl_level': gist_acl_level,
87 'gist_acl_level': gist_acl_level,
87 }
88 }
88 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
89 metadata_file = os.path.join(repo.path, '.hg', GIST_METADATA_FILE)
89 with open(metadata_file, 'wb') as f:
90 with open(metadata_file, 'wb') as f:
90 f.write(json.dumps(metadata))
91 f.write(json.dumps(metadata))
91
92
92 def get_gist(self, gist):
93 def get_gist(self, gist):
93 return self._get_gist(gist)
94 return self._get_gist(gist)
94
95
95 def get_gist_files(self, gist_access_id, revision=None):
96 def get_gist_files(self, gist_access_id, revision=None):
96 """
97 """
97 Get files for given gist
98 Get files for given gist
98
99
99 :param gist_access_id:
100 :param gist_access_id:
100 """
101 """
101 repo = Gist.get_by_access_id(gist_access_id)
102 repo = Gist.get_by_access_id(gist_access_id)
102 vcs_repo = repo.scm_instance()
103 vcs_repo = repo.scm_instance()
103 if not vcs_repo:
104 if not vcs_repo:
104 raise VCSError('Failed to load gist repository for {}'.format(repo))
105 raise VCSError('Failed to load gist repository for {}'.format(repo))
105
106
106 commit = vcs_repo.get_commit(commit_id=revision)
107 commit = vcs_repo.get_commit(commit_id=revision)
107 return commit, [n for n in commit.get_node('/')]
108 return commit, [n for n in commit.get_node('/')]
108
109
109 def create(self, description, owner, gist_mapping,
110 def create(self, description, owner, gist_mapping,
110 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
111 gist_type=Gist.GIST_PUBLIC, lifetime=-1, gist_id=None,
111 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
112 gist_acl_level=Gist.ACL_LEVEL_PRIVATE):
112 """
113 """
113 Create a gist
114 Create a gist
114
115
115 :param description: description of the gist
116 :param description: description of the gist
116 :param owner: user who created this gist
117 :param owner: user who created this gist
117 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
118 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
118 :param gist_type: type of gist private/public
119 :param gist_type: type of gist private/public
119 :param lifetime: in minutes, -1 == forever
120 :param lifetime: in minutes, -1 == forever
120 :param gist_acl_level: acl level for this gist
121 :param gist_acl_level: acl level for this gist
121 """
122 """
122 owner = self._get_user(owner)
123 owner = self._get_user(owner)
123 gist_id = safe_unicode(gist_id or unique_id(20))
124 gist_id = safe_unicode(gist_id or unique_id(20))
124 lifetime = safe_int(lifetime, -1)
125 lifetime = safe_int(lifetime, -1)
125 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
126 gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
126 expiration = (time_to_datetime(gist_expires)
127 expiration = (time_to_datetime(gist_expires)
127 if gist_expires != -1 else 'forever')
128 if gist_expires != -1 else 'forever')
128 log.debug('set GIST expiration date to: %s', expiration)
129 log.debug('set GIST expiration date to: %s', expiration)
129 # create the Database version
130 # create the Database version
130 gist = Gist()
131 gist = Gist()
131 gist.gist_description = description
132 gist.gist_description = description
132 gist.gist_access_id = gist_id
133 gist.gist_access_id = gist_id
133 gist.gist_owner = owner.user_id
134 gist.gist_owner = owner.user_id
134 gist.gist_expires = gist_expires
135 gist.gist_expires = gist_expires
135 gist.gist_type = safe_unicode(gist_type)
136 gist.gist_type = safe_unicode(gist_type)
136 gist.acl_level = gist_acl_level
137 gist.acl_level = gist_acl_level
137 self.sa.add(gist)
138 self.sa.add(gist)
138 self.sa.flush()
139 self.sa.flush()
139 if gist_type == Gist.GIST_PUBLIC:
140 if gist_type == Gist.GIST_PUBLIC:
140 # use DB ID for easy to use GIST ID
141 # use DB ID for easy to use GIST ID
141 gist_id = safe_unicode(gist.gist_id)
142 gist_id = safe_unicode(gist.gist_id)
142 gist.gist_access_id = gist_id
143 gist.gist_access_id = gist_id
143 self.sa.add(gist)
144 self.sa.add(gist)
144
145
145 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
146 gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
146 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
147 log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
147 repo = RepoModel()._create_filesystem_repo(
148 repo = RepoModel()._create_filesystem_repo(
148 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
149 repo_name=gist_id, repo_type=self.vcs_backend, repo_group=GIST_STORE_LOC,
149 use_global_config=True)
150 use_global_config=True)
150
151
151 # now create single multifile commit
152 # now create single multifile commit
152 message = 'added file'
153 message = 'added file'
153 message += 's: ' if len(gist_mapping) > 1 else ': '
154 message += 's: ' if len(gist_mapping) > 1 else ': '
154 message += ', '.join([x for x in gist_mapping])
155 message += ', '.join([x for x in gist_mapping])
155
156
156 # fake RhodeCode Repository object
157 # fake RhodeCode Repository object
157 fake_repo = AttributeDict({
158 fake_repo = AttributeDict({
158 'repo_name': gist_repo_path,
159 'repo_name': gist_repo_path,
159 'scm_instance': lambda *args, **kwargs: repo,
160 'scm_instance': lambda *args, **kwargs: repo,
160 })
161 })
161
162
162 ScmModel().create_nodes(
163 ScmModel().create_nodes(
163 user=owner.user_id, repo=fake_repo,
164 user=owner.user_id, repo=fake_repo,
164 message=message,
165 message=message,
165 nodes=gist_mapping,
166 nodes=gist_mapping,
166 trigger_push_hook=False
167 trigger_push_hook=False
167 )
168 )
168
169
169 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
170 self._store_metadata(repo, gist.gist_id, gist.gist_access_id,
170 owner.user_id, owner.username, gist.gist_type,
171 owner.user_id, owner.username, gist.gist_type,
171 gist.gist_expires, gist_acl_level)
172 gist.gist_expires, gist_acl_level)
172 return gist
173 return gist
173
174
174 def delete(self, gist, fs_remove=True):
175 def delete(self, gist, fs_remove=True):
175 gist = self._get_gist(gist)
176 gist = self._get_gist(gist)
176 try:
177 try:
177 self.sa.delete(gist)
178 self.sa.delete(gist)
178 if fs_remove:
179 if fs_remove:
179 self.__delete_gist(gist)
180 self.__delete_gist(gist)
180 else:
181 else:
181 log.debug('skipping removal from filesystem')
182 log.debug('skipping removal from filesystem')
182 except Exception:
183 except Exception:
183 log.error(traceback.format_exc())
184 log.error(traceback.format_exc())
184 raise
185 raise
185
186
186 def update(self, gist, description, owner, gist_mapping, lifetime,
187 def update(self, gist, description, owner, gist_mapping, lifetime,
187 gist_acl_level):
188 gist_acl_level):
188 gist = self._get_gist(gist)
189 gist = self._get_gist(gist)
189 gist_repo = gist.scm_instance()
190 gist_repo = gist.scm_instance()
190
191
191 if lifetime == 0: # preserve old value
192 if lifetime == 0: # preserve old value
192 gist_expires = gist.gist_expires
193 gist_expires = gist.gist_expires
193 else:
194 else:
194 gist_expires = (
195 gist_expires = (
195 time.time() + (lifetime * 60) if lifetime != -1 else -1)
196 time.time() + (lifetime * 60) if lifetime != -1 else -1)
196
197
197 # calculate operation type based on given data
198 # calculate operation type based on given data
198 gist_mapping_op = {}
199 gist_mapping_op = {}
199 for k, v in gist_mapping.items():
200 for k, v in gist_mapping.items():
200 # add, mod, del
201 # add, mod, del
201 if not v['filename_org'] and v['filename']:
202 if not v['filename_org'] and v['filename']:
202 op = 'add'
203 op = 'add'
203 elif v['filename_org'] and not v['filename']:
204 elif v['filename_org'] and not v['filename']:
204 op = 'del'
205 op = 'del'
205 else:
206 else:
206 op = 'mod'
207 op = 'mod'
207
208
208 v['op'] = op
209 v['op'] = op
209 gist_mapping_op[k] = v
210 gist_mapping_op[k] = v
210
211
211 gist.gist_description = description
212 gist.gist_description = description
212 gist.gist_expires = gist_expires
213 gist.gist_expires = gist_expires
213 gist.owner = owner
214 gist.owner = owner
214 gist.acl_level = gist_acl_level
215 gist.acl_level = gist_acl_level
215 self.sa.add(gist)
216 self.sa.add(gist)
216 self.sa.flush()
217 self.sa.flush()
217
218
218 message = 'updated file'
219 message = 'updated file'
219 message += 's: ' if len(gist_mapping) > 1 else ': '
220 message += 's: ' if len(gist_mapping) > 1 else ': '
220 message += ', '.join([x for x in gist_mapping])
221 message += ', '.join([x for x in gist_mapping])
221
222
222 # fake RhodeCode Repository object
223 # fake RhodeCode Repository object
223 fake_repo = AttributeDict({
224 fake_repo = AttributeDict({
224 'repo_name': gist_repo.path,
225 'repo_name': gist_repo.path,
225 'scm_instance': lambda *args, **kwargs: gist_repo,
226 'scm_instance': lambda *args, **kwargs: gist_repo,
226 })
227 })
227
228
228 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
229 self._store_metadata(gist_repo, gist.gist_id, gist.gist_access_id,
229 owner.user_id, owner.username, gist.gist_type,
230 owner.user_id, owner.username, gist.gist_type,
230 gist.gist_expires, gist_acl_level)
231 gist.gist_expires, gist_acl_level)
231
232
232 # this can throw NodeNotChangedError, if changes we're trying to commit
233 # this can throw NodeNotChangedError, if changes we're trying to commit
233 # are not actually changes...
234 # are not actually changes...
234 ScmModel().update_nodes(
235 ScmModel().update_nodes(
235 user=owner.user_id,
236 user=owner.user_id,
236 repo=fake_repo,
237 repo=fake_repo,
237 message=message,
238 message=message,
238 nodes=gist_mapping_op,
239 nodes=gist_mapping_op,
239 trigger_push_hook=False
240 trigger_push_hook=False
240 )
241 )
241
242
242 return gist
243 return gist
243
244
244 def get_url(self, gist, request=None):
245 def get_url(self, gist, request=None):
245 import rhodecode
246 import rhodecode
246
247
247 if not request:
248 if not request:
248 request = get_current_request()
249 request = get_current_request()
249
250
250 alias_url = rhodecode.CONFIG.get('gist_alias_url')
251 alias_url = rhodecode.CONFIG.get('gist_alias_url')
251 if alias_url:
252 if alias_url:
252 return alias_url.replace('{gistid}', gist.gist_access_id)
253 return alias_url.replace('{gistid}', gist.gist_access_id)
253
254
254 return request.route_url('gist_show', gist_id=gist.gist_access_id)
255 return request.route_url('gist_show', gist_id=gist.gist_access_id)
255
256
General Comments 0
You need to be logged in to leave comments. Login now