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