comparison tincan.py @ 59:60907204a265 draft

Support case-insensitive filesystems properly.
author David Barts <n5jrn@me.com>
date Fri, 31 May 2019 21:20:22 -0700
parents e08e24707da1
children f33cb3e93473
comparison
equal deleted inserted replaced
58:e08e24707da1 59:60907204a265
250 else: 250 else:
251 ret.append(ch) 251 ret.append(ch)
252 first = False 252 first = False
253 return ''.join(ret) 253 return ''.join(ret)
254 254
255 _FOLDS_CASE = sys.platform in ['darwin', 'win32']
256
257 def _casef(string, case="lower"):
258 """
259 If we're on an OS with case-insensitive file names, fold case.
260 Else leave things alone.
261 """
262 return getattr(string, case)() if _FOLDS_CASE else string
263
255 # The TinCan class. Simply a Bottle webapp that contains a forward method, so 264 # The TinCan class. Simply a Bottle webapp that contains a forward method, so
256 # the code-behind can call request.app.forward(). 265 # the code-behind can call request.app.forward().
257 266
258 class TinCan(bottle.Bottle): 267 class TinCan(bottle.Bottle):
259 def forward(self, target): 268 def forward(self, target):
351 self.fname = raw[equals+1:] 360 self.fname = raw[equals+1:]
352 if self.vname == "": 361 if self.vname == "":
353 raise ValueError("empty variable name") 362 raise ValueError("empty variable name")
354 if self.fname == "": 363 if self.fname == "":
355 raise ValueError("empty file name") 364 raise ValueError("empty file name")
356 if not self.fname.endswith(_IEXTEN): 365 if not _casef(self.fname).endswith(_IEXTEN):
357 raise ValueError("file does not end in {0}".format(_IEXTEN)) 366 raise ValueError("file does not end in {0}".format(_IEXTEN))
358 367
359 # Using a cache is likely to help efficiency a lot, since many pages 368 # Using a cache is likely to help efficiency a lot, since many pages
360 # will typically #load the same standard stuff. Except if we're 369 # will typically #load the same standard stuff. Except if we're
361 # multithreading, then we want each page's templates to be private. 370 # multithreading, then we want each page's templates to be private.
442 451
443 def launch(self): 452 def launch(self):
444 self.logger.info("adding static route: %s", self._urlpath) 453 self.logger.info("adding static route: %s", self._urlpath)
445 self._app.route(self._urlpath, 'GET', self) 454 self._app.route(self._urlpath, 'GET', self)
446 for i in _INDICES: 455 for i in _INDICES:
447 if self._urlpath.endswith(i): 456 if _casef(self._urlpath).endswith(i):
448 li = len(i) 457 li = len(i)
449 for j in [ self._urlpath[:1-li], self._urlpath[:-li] ]: 458 for j in [ self._urlpath[:1-li], self._urlpath[:-li] ]:
450 if j: 459 if j:
451 self.logger.info("adding static route: %s", j) 460 self.logger.info("adding static route: %s", j)
452 self._app.route(j, 'GET', self) 461 self._app.route(j, 'GET', self)
580 return 589 return
581 # Get the code-behind #python 590 # Get the code-behind #python
582 if self._header.python is None: 591 if self._header.python is None:
583 self._python_specified = False 592 self._python_specified = False
584 else: 593 else:
585 if not self._header.python.endswith(_PEXTEN): 594 if not _casef(self._header.python).endswith(_PEXTEN):
586 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) 595 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN))
587 self._python = self._header.python 596 self._python = self._header.python
588 self._python_specified = True 597 self._python_specified = True
589 # Obtain a class object by importing and introspecting a module. 598 # Obtain a class object by importing and introspecting a module.
590 self._getclass() 599 self._getclass()
591 # Build body object (#template) and obtain #loads. 600 # Build body object (#template) and obtain #loads.
592 if self._header.template is not None: 601 if self._header.template is not None:
593 if not self._header.template.endswith(_TEXTEN): 602 if not _casef(self._header.template).endswith(_TEXTEN):
594 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) 603 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
595 try: 604 try:
596 rtpath = self._splitpath(self._header.template) 605 rtpath = self._splitpath(self._header.template)
597 tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath)) 606 tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath))
598 tfile = TemplateFile(tpath) 607 tfile = TemplateFile(tpath)
633 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) 642 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
634 # Register this thing with Bottle 643 # Register this thing with Bottle
635 mtxt = ','.join(methods) 644 mtxt = ','.join(methods)
636 self.logger.info("adding route: %s (%s)", self._origin, mtxt) 645 self.logger.info("adding route: %s (%s)", self._origin, mtxt)
637 self._app.route(self._origin, methods, self) 646 self._app.route(self._origin, methods, self)
638 if self._origin.endswith(_INDEX): 647 if _casef(self._origin).endswith(_INDEX):
639 for i in [ self._origin[:1-_LINDEX], self._origin[:-_LINDEX] ]: 648 for i in [ self._origin[:1-_LINDEX], self._origin[:-_LINDEX] ]:
640 if i: 649 if i:
641 self.logger.info("adding route: %s (%s)", i, mtxt) 650 self.logger.info("adding route: %s (%s)", i, mtxt)
642 self._app.route(i, methods, self) 651 self._app.route(i, methods, self)
643 652
749 self._seen.add(forw) 758 self._seen.add(forw)
750 rname = rlist.pop() 759 rname = rlist.pop()
751 except IndexError as e: 760 except IndexError as e:
752 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e 761 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e
753 name, ext = os.path.splitext(rname) 762 name, ext = os.path.splitext(rname)
754 if ext != _TEXTEN: 763 if _casef(ext) != _TEXTEN:
755 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) 764 raise TinCanError("{0}: invalid #forward".format(self._urlpath))
756 self._subdir = rlist 765 self._subdir = rlist
757 self._python = name + _PEXTEN 766 self._python = name + _PEXTEN
758 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) 767 self._fspath = os.path.join(self._fsroot, *self._subdir, rname)
759 self._urlpath = '/' + self.urljoin(*self._subdir, rname) 768 self._urlpath = '/' + self.urljoin(*self._subdir, rname)
818 return False 827 return False
819 828
820 # L a u n c h e r 829 # L a u n c h e r
821 830
822 _WINF = "WEB-INF" 831 _WINF = "WEB-INF"
823 _BANNED = set([_WINF])
824 _EBANNED = set([_IEXTEN, _TEXTEN, _PEXTEN, _PEXTEN+"c"]) 832 _EBANNED = set([_IEXTEN, _TEXTEN, _PEXTEN, _PEXTEN+"c"])
825 ENCODING = "utf-8" 833 ENCODING = "utf-8"
826 _BITBUCKET = logging.getLogger(__name__) 834 _BITBUCKET = logging.getLogger(__name__)
827 _BITBUCKET.addHandler(logging.NullHandler) 835 _BITBUCKET.addHandler(logging.NullHandler)
828 836
884 892
885 def _launch(self, subdir): 893 def _launch(self, subdir):
886 for entry in os.listdir(os.path.join(self.fsroot, *subdir)): 894 for entry in os.listdir(os.path.join(self.fsroot, *subdir)):
887 if entry.startswith("."): 895 if entry.startswith("."):
888 continue # hidden file 896 continue # hidden file
889 if not subdir and entry in _BANNED: 897 if not subdir and _casef(entry, "upper") == _WINF:
890 continue 898 continue
891 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode 899 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode
892 if S_ISREG(etype): 900 if S_ISREG(etype):
893 ename, eext = os.path.splitext(entry) 901 ename, eext = os.path.splitext(entry)
902 eext = _casef(eext)
894 if eext == _TEXTEN: 903 if eext == _TEXTEN:
895 route = _TinCanRoute(self, ename, subdir) 904 route = _TinCanRoute(self, ename, subdir)
896 else: 905 else:
897 if eext in _EBANNED: 906 if eext in _EBANNED:
898 continue 907 continue