##// END OF EJS Templates
hgwebdir: refresh configuration periodically...
Bryan O'Sullivan -
r8371:1bd0fdf4 default
parent child Browse files
Show More
@@ -1,314 +1,326 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2, incorporated herein by reference.
8 8
9 import os
9 import os, time
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, util, templater
12 12 from mercurial import error, encoding
13 13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb
16 16 from request import wsgirequest
17 17 import webutil
18 18
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21 21
22 22 class hgwebdir(object):
23 refreshinterval = 20
23 24
24 25 def __init__(self, conf, baseui=None):
26 self.conf = conf
27 self.baseui = baseui
28 self.lastrefresh = 0
29 self.refresh()
25 30
26 if baseui:
27 self.ui = baseui.copy()
31 def refresh(self):
32 if self.lastrefresh + self.refreshinterval > time.time():
33 return
34
35 if self.baseui:
36 self.ui = self.baseui.copy()
28 37 else:
29 38 self.ui = ui.ui()
30 39 self.ui.setconfig('ui', 'report_untrusted', 'off')
31 40 self.ui.setconfig('ui', 'interactive', 'off')
32 41
33 if isinstance(conf, (list, tuple)):
42 if isinstance(self.conf, (list, tuple)):
34 43 self.repos = cleannames(conf)
35 elif isinstance(conf, dict):
36 self.repos = sorted(cleannames(conf.items()))
44 elif isinstance(self.conf, dict):
45 self.repos = sorted(cleannames(self.conf.items()))
37 46 else:
38 self.ui.readconfig(conf, remap={'paths': 'hgweb-paths'}, trust=True)
47 self.ui.readconfig(self.conf, remap={'paths': 'hgweb-paths'}, trust=True)
39 48 self.repos = []
40 49
41 50 self.motd = self.ui.config('web', 'motd')
42 51 self.style = self.ui.config('web', 'style', 'paper')
43 52 self.stripecount = self.ui.config('web', 'stripes', 1)
44 53 if self.stripecount:
45 54 self.stripecount = int(self.stripecount)
46 55 self._baseurl = self.ui.config('web', 'baseurl')
47 56
48 57 if self.repos:
49 58 return
50 59
51 60 for prefix, root in cleannames(self.ui.configitems('hgweb-paths')):
52 61 roothead, roottail = os.path.split(root)
53 62 # "foo = /bar/*" makes every subrepo of /bar/ to be
54 63 # mounted as foo/subrepo
55 64 # and "foo = /bar/**" also recurses into the subdirectories,
56 65 # remember to use it without working dir.
57 66 try:
58 67 recurse = {'*': False, '**': True}[roottail]
59 68 except KeyError:
60 69 self.repos.append((prefix, root))
61 70 continue
62 71 roothead = os.path.normpath(roothead)
63 72 for path in util.walkrepos(roothead, followsym=True,
64 73 recurse=recurse):
65 74 path = os.path.normpath(path)
66 75 name = util.pconvert(path[len(roothead):]).strip('/')
67 76 if prefix:
68 77 name = prefix + '/' + name
69 78 self.repos.append((name, path))
70 79
71 80 for prefix, root in self.ui.configitems('collections'):
72 81 for path in util.walkrepos(root, followsym=True):
73 82 repo = os.path.normpath(path)
74 83 name = repo
75 84 if name.startswith(prefix):
76 85 name = name[len(prefix):]
77 86 self.repos.append((name.lstrip(os.sep), repo))
78 87
79 88 self.repos.sort()
89 self.lastrefresh = time.time()
80 90
81 91 def run(self):
82 92 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
83 93 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
84 94 import mercurial.hgweb.wsgicgi as wsgicgi
85 95 wsgicgi.launch(self)
86 96
87 97 def __call__(self, env, respond):
88 98 req = wsgirequest(env, respond)
89 99 return self.run_wsgi(req)
90 100
91 101 def read_allowed(self, ui, req):
92 102 """Check allow_read and deny_read config options of a repo's ui object
93 103 to determine user permissions. By default, with neither option set (or
94 104 both empty), allow all users to read the repo. There are two ways a
95 105 user can be denied read access: (1) deny_read is not empty, and the
96 106 user is unauthenticated or deny_read contains user (or *), and (2)
97 107 allow_read is not empty and the user is not in allow_read. Return True
98 108 if user is allowed to read the repo, else return False."""
99 109
100 110 user = req.env.get('REMOTE_USER')
101 111
102 112 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
103 113 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
104 114 return False
105 115
106 116 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
107 117 # by default, allow reading if no allow_read option has been set
108 118 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
109 119 return True
110 120
111 121 return False
112 122
113 123 def run_wsgi(self, req):
114
115 124 try:
116 125 try:
126 self.refresh()
117 127
118 128 virtual = req.env.get("PATH_INFO", "").strip('/')
119 129 tmpl = self.templater(req)
120 130 ctype = tmpl('mimetype', encoding=encoding.encoding)
121 131 ctype = templater.stringify(ctype)
122 132
123 133 # a static file
124 134 if virtual.startswith('static/') or 'static' in req.form:
125 135 if virtual.startswith('static/'):
126 136 fname = virtual[7:]
127 137 else:
128 138 fname = req.form['static'][0]
129 139 static = templater.templatepath('static')
130 140 return (staticfile(static, fname, req),)
131 141
132 142 # top-level index
133 143 elif not virtual:
134 144 req.respond(HTTP_OK, ctype)
135 145 return self.makeindex(req, tmpl)
136 146
137 147 # nested indexes and hgwebs
138 148
139 149 repos = dict(self.repos)
140 150 while virtual:
141 151 real = repos.get(virtual)
142 152 if real:
143 153 req.env['REPO_NAME'] = virtual
144 154 try:
145 155 repo = hg.repository(self.ui, real)
146 156 return hgweb(repo).run_wsgi(req)
147 157 except IOError, inst:
148 158 msg = inst.strerror
149 159 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
150 160 except error.RepoError, inst:
151 161 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
152 162
153 163 # browse subdirectories
154 164 subdir = virtual + '/'
155 165 if [r for r in repos if r.startswith(subdir)]:
156 166 req.respond(HTTP_OK, ctype)
157 167 return self.makeindex(req, tmpl, subdir)
158 168
159 169 up = virtual.rfind('/')
160 170 if up < 0:
161 171 break
162 172 virtual = virtual[:up]
163 173
164 174 # prefixes not found
165 175 req.respond(HTTP_NOT_FOUND, ctype)
166 176 return tmpl("notfound", repo=virtual)
167 177
168 178 except ErrorResponse, err:
169 179 req.respond(err, ctype)
170 180 return tmpl('error', error=err.message or '')
171 181 finally:
172 182 tmpl = None
173 183
174 184 def makeindex(self, req, tmpl, subdir=""):
175 185
176 186 def archivelist(ui, nodeid, url):
177 187 allowed = ui.configlist("web", "allow_archive", untrusted=True)
178 188 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
179 189 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
180 190 untrusted=True):
181 191 yield {"type" : i[0], "extension": i[1],
182 192 "node": nodeid, "url": url}
183 193
184 194 sortdefault = 'name', False
185 195 def entries(sortcolumn="", descending=False, subdir="", **map):
186 196 rows = []
187 197 parity = paritygen(self.stripecount)
188 198 for name, path in self.repos:
189 199 if not name.startswith(subdir):
190 200 continue
191 201 name = name[len(subdir):]
192 202
193 203 u = self.ui.copy()
194 204 try:
195 205 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
196 206 except Exception, e:
197 207 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
198 208 continue
199 209 def get(section, name, default=None):
200 210 return u.config(section, name, default, untrusted=True)
201 211
202 212 if u.configbool("web", "hidden", untrusted=True):
203 213 continue
204 214
205 215 if not self.read_allowed(u, req):
206 216 continue
207 217
208 218 parts = [name]
209 219 if 'PATH_INFO' in req.env:
210 220 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
211 221 if req.env['SCRIPT_NAME']:
212 222 parts.insert(0, req.env['SCRIPT_NAME'])
213 223 url = ('/'.join(parts).replace("//", "/")) + '/'
214 224
215 225 # update time with local timezone
216 226 try:
217 227 d = (get_mtime(path), util.makedate()[1])
218 228 except OSError:
219 229 continue
220 230
221 231 contact = get_contact(get)
222 232 description = get("web", "description", "")
223 233 name = get("web", "name", name)
224 234 row = dict(contact=contact or "unknown",
225 235 contact_sort=contact.upper() or "unknown",
226 236 name=name,
227 237 name_sort=name,
228 238 url=url,
229 239 description=description or "unknown",
230 240 description_sort=description.upper() or "unknown",
231 241 lastchange=d,
232 242 lastchange_sort=d[1]-d[0],
233 243 archives=archivelist(u, "tip", url))
234 244 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
235 245 # fast path for unsorted output
236 246 row['parity'] = parity.next()
237 247 yield row
238 248 else:
239 249 rows.append((row["%s_sort" % sortcolumn], row))
240 250 if rows:
241 251 rows.sort()
242 252 if descending:
243 253 rows.reverse()
244 254 for key, row in rows:
245 255 row['parity'] = parity.next()
246 256 yield row
247 257
258 self.refresh()
248 259 sortable = ["name", "description", "contact", "lastchange"]
249 260 sortcolumn, descending = sortdefault
250 261 if 'sort' in req.form:
251 262 sortcolumn = req.form['sort'][0]
252 263 descending = sortcolumn.startswith('-')
253 264 if descending:
254 265 sortcolumn = sortcolumn[1:]
255 266 if sortcolumn not in sortable:
256 267 sortcolumn = ""
257 268
258 269 sort = [("sort_%s" % column,
259 270 "%s%s" % ((not descending and column == sortcolumn)
260 271 and "-" or "", column))
261 272 for column in sortable]
262 273
274 self.refresh()
263 275 if self._baseurl is not None:
264 276 req.env['SCRIPT_NAME'] = self._baseurl
265 277
266 278 return tmpl("index", entries=entries, subdir=subdir,
267 279 sortcolumn=sortcolumn, descending=descending,
268 280 **dict(sort))
269 281
270 282 def templater(self, req):
271 283
272 284 def header(**map):
273 285 yield tmpl('header', encoding=encoding.encoding, **map)
274 286
275 287 def footer(**map):
276 288 yield tmpl("footer", **map)
277 289
278 290 def motd(**map):
279 291 if self.motd is not None:
280 292 yield self.motd
281 293 else:
282 294 yield config('web', 'motd', '')
283 295
284 296 def config(section, name, default=None, untrusted=True):
285 297 return self.ui.config(section, name, default, untrusted)
286 298
287 299 if self._baseurl is not None:
288 300 req.env['SCRIPT_NAME'] = self._baseurl
289 301
290 302 url = req.env.get('SCRIPT_NAME', '')
291 303 if not url.endswith('/'):
292 304 url += '/'
293 305
294 306 vars = {}
295 307 style = self.style
296 308 if 'style' in req.form:
297 309 vars['style'] = style = req.form['style'][0]
298 310 start = url[-1] == '?' and '&' or '?'
299 311 sessionvars = webutil.sessionvars(vars, start)
300 312
301 313 staticurl = config('web', 'staticurl') or url + 'static/'
302 314 if not staticurl.endswith('/'):
303 315 staticurl += '/'
304 316
305 317 style = 'style' in req.form and req.form['style'][0] or self.style
306 318 mapfile = templater.stylemap(style)
307 319 tmpl = templater.templater(mapfile,
308 320 defaults={"header": header,
309 321 "footer": footer,
310 322 "motd": motd,
311 323 "url": url,
312 324 "staticurl": staticurl,
313 325 "sessionvars": sessionvars})
314 326 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now