cachedpath.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #
  2. # SPDX-License-Identifier: GPL-2.0-only
  3. #
  4. # Based on standard python library functions but avoid
  5. # repeated stat calls. Its assumed the files will not change from under us
  6. # so we can cache stat calls.
  7. #
  8. import os
  9. import errno
  10. import stat as statmod
  11. class CachedPath(object):
  12. def __init__(self):
  13. self.statcache = {}
  14. self.lstatcache = {}
  15. self.normpathcache = {}
  16. return
  17. def updatecache(self, x):
  18. x = self.normpath(x)
  19. if x in self.statcache:
  20. del self.statcache[x]
  21. if x in self.lstatcache:
  22. del self.lstatcache[x]
  23. def normpath(self, path):
  24. if path in self.normpathcache:
  25. return self.normpathcache[path]
  26. newpath = os.path.normpath(path)
  27. self.normpathcache[path] = newpath
  28. return newpath
  29. def _callstat(self, path):
  30. if path in self.statcache:
  31. return self.statcache[path]
  32. try:
  33. st = os.stat(path)
  34. self.statcache[path] = st
  35. return st
  36. except os.error:
  37. self.statcache[path] = False
  38. return False
  39. # We might as well call lstat and then only
  40. # call stat as well in the symbolic link case
  41. # since this turns out to be much more optimal
  42. # in real world usage of this cache
  43. def callstat(self, path):
  44. path = self.normpath(path)
  45. self.calllstat(path)
  46. return self.statcache[path]
  47. def calllstat(self, path):
  48. path = self.normpath(path)
  49. if path in self.lstatcache:
  50. return self.lstatcache[path]
  51. #bb.error("LStatpath:" + path)
  52. try:
  53. lst = os.lstat(path)
  54. self.lstatcache[path] = lst
  55. if not statmod.S_ISLNK(lst.st_mode):
  56. self.statcache[path] = lst
  57. else:
  58. self._callstat(path)
  59. return lst
  60. except (os.error, AttributeError):
  61. self.lstatcache[path] = False
  62. self.statcache[path] = False
  63. return False
  64. # This follows symbolic links, so both islink() and isdir() can be true
  65. # for the same path ono systems that support symlinks
  66. def isfile(self, path):
  67. """Test whether a path is a regular file"""
  68. st = self.callstat(path)
  69. if not st:
  70. return False
  71. return statmod.S_ISREG(st.st_mode)
  72. # Is a path a directory?
  73. # This follows symbolic links, so both islink() and isdir()
  74. # can be true for the same path on systems that support symlinks
  75. def isdir(self, s):
  76. """Return true if the pathname refers to an existing directory."""
  77. st = self.callstat(s)
  78. if not st:
  79. return False
  80. return statmod.S_ISDIR(st.st_mode)
  81. def islink(self, path):
  82. """Test whether a path is a symbolic link"""
  83. st = self.calllstat(path)
  84. if not st:
  85. return False
  86. return statmod.S_ISLNK(st.st_mode)
  87. # Does a path exist?
  88. # This is false for dangling symbolic links on systems that support them.
  89. def exists(self, path):
  90. """Test whether a path exists. Returns False for broken symbolic links"""
  91. if self.callstat(path):
  92. return True
  93. return False
  94. def lexists(self, path):
  95. """Test whether a path exists. Returns True for broken symbolic links"""
  96. if self.calllstat(path):
  97. return True
  98. return False
  99. def stat(self, path):
  100. return self.callstat(path)
  101. def lstat(self, path):
  102. return self.calllstat(path)
  103. def walk(self, top, topdown=True, onerror=None, followlinks=False):
  104. # Matches os.walk, not os.path.walk()
  105. # We may not have read permission for top, in which case we can't
  106. # get a list of the files the directory contains. os.path.walk
  107. # always suppressed the exception then, rather than blow up for a
  108. # minor reason when (say) a thousand readable directories are still
  109. # left to visit. That logic is copied here.
  110. try:
  111. names = os.listdir(top)
  112. except os.error as err:
  113. if onerror is not None:
  114. onerror(err)
  115. return
  116. dirs, nondirs = [], []
  117. for name in names:
  118. if self.isdir(os.path.join(top, name)):
  119. dirs.append(name)
  120. else:
  121. nondirs.append(name)
  122. if topdown:
  123. yield top, dirs, nondirs
  124. for name in dirs:
  125. new_path = os.path.join(top, name)
  126. if followlinks or not self.islink(new_path):
  127. for x in self.walk(new_path, topdown, onerror, followlinks):
  128. yield x
  129. if not topdown:
  130. yield top, dirs, nondirs
  131. ## realpath() related functions
  132. def __is_path_below(self, file, root):
  133. return (file + os.path.sep).startswith(root)
  134. def __realpath_rel(self, start, rel_path, root, loop_cnt, assume_dir):
  135. """Calculates real path of symlink 'start' + 'rel_path' below
  136. 'root'; no part of 'start' below 'root' must contain symlinks. """
  137. have_dir = True
  138. for d in rel_path.split(os.path.sep):
  139. if not have_dir and not assume_dir:
  140. raise OSError(errno.ENOENT, "no such directory %s" % start)
  141. if d == os.path.pardir: # '..'
  142. if len(start) >= len(root):
  143. # do not follow '..' before root
  144. start = os.path.dirname(start)
  145. else:
  146. # emit warning?
  147. pass
  148. else:
  149. (start, have_dir) = self.__realpath(os.path.join(start, d),
  150. root, loop_cnt, assume_dir)
  151. assert(self.__is_path_below(start, root))
  152. return start
  153. def __realpath(self, file, root, loop_cnt, assume_dir):
  154. while self.islink(file) and len(file) >= len(root):
  155. if loop_cnt == 0:
  156. raise OSError(errno.ELOOP, file)
  157. loop_cnt -= 1
  158. target = os.path.normpath(os.readlink(file))
  159. if not os.path.isabs(target):
  160. tdir = os.path.dirname(file)
  161. assert(self.__is_path_below(tdir, root))
  162. else:
  163. tdir = root
  164. file = self.__realpath_rel(tdir, target, root, loop_cnt, assume_dir)
  165. try:
  166. is_dir = self.isdir(file)
  167. except:
  168. is_dir = False
  169. return (file, is_dir)
  170. def realpath(self, file, root, use_physdir = True, loop_cnt = 100, assume_dir = False):
  171. """ Returns the canonical path of 'file' with assuming a
  172. toplevel 'root' directory. When 'use_physdir' is set, all
  173. preceding path components of 'file' will be resolved first;
  174. this flag should be set unless it is guaranteed that there is
  175. no symlink in the path. When 'assume_dir' is not set, missing
  176. path components will raise an ENOENT error"""
  177. root = os.path.normpath(root)
  178. file = os.path.normpath(file)
  179. if not root.endswith(os.path.sep):
  180. # letting root end with '/' makes some things easier
  181. root = root + os.path.sep
  182. if not self.__is_path_below(file, root):
  183. raise OSError(errno.EINVAL, "file '%s' is not below root" % file)
  184. try:
  185. if use_physdir:
  186. file = self.__realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir)
  187. else:
  188. file = self.__realpath(file, root, loop_cnt, assume_dir)[0]
  189. except OSError as e:
  190. if e.errno == errno.ELOOP:
  191. # make ELOOP more readable; without catching it, there will
  192. # be printed a backtrace with 100s of OSError exceptions
  193. # else
  194. raise OSError(errno.ELOOP,
  195. "too much recursions while resolving '%s'; loop in '%s'" %
  196. (file, e.strerror))
  197. raise
  198. return file