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