Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

zippath.py 8.8KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # -*- test-case-name: twisted.python.test.test_zippath -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. This module contains implementations of L{IFilePath} for zip files.
  6. See the constructor of L{ZipArchive} for use.
  7. """
  8. import errno
  9. import os
  10. import time
  11. from typing import Dict
  12. from zipfile import ZipFile
  13. from zope.interface import implementer
  14. from twisted.python.compat import cmp, comparable
  15. from twisted.python.filepath import (
  16. AbstractFilePath,
  17. FilePath,
  18. IFilePath,
  19. UnlistableError,
  20. _coerceToFilesystemEncoding,
  21. )
  22. ZIP_PATH_SEP = "/" # In zipfiles, "/" is universally used as the
  23. # path separator, regardless of platform.
  24. @comparable
  25. @implementer(IFilePath)
  26. class ZipPath(AbstractFilePath):
  27. """
  28. I represent a file or directory contained within a zip file.
  29. """
  30. def __init__(self, archive, pathInArchive):
  31. """
  32. Don't construct me directly. Use C{ZipArchive.child()}.
  33. @param archive: a L{ZipArchive} instance.
  34. @param pathInArchive: a ZIP_PATH_SEP-separated string.
  35. """
  36. self.archive = archive
  37. self.pathInArchive = pathInArchive
  38. # self.path pretends to be os-specific because that's the way the
  39. # 'zipimport' module does it.
  40. sep = _coerceToFilesystemEncoding(pathInArchive, ZIP_PATH_SEP)
  41. archiveFilename = _coerceToFilesystemEncoding(
  42. pathInArchive, archive.zipfile.filename
  43. )
  44. self.path = os.path.join(archiveFilename, *(self.pathInArchive.split(sep)))
  45. def __cmp__(self, other):
  46. if not isinstance(other, ZipPath):
  47. return NotImplemented
  48. return cmp(
  49. (self.archive, self.pathInArchive), (other.archive, other.pathInArchive)
  50. )
  51. def __repr__(self) -> str:
  52. parts = [
  53. _coerceToFilesystemEncoding(self.sep, os.path.abspath(self.archive.path))
  54. ]
  55. parts.extend(self.pathInArchive.split(self.sep))
  56. ossep = _coerceToFilesystemEncoding(self.sep, os.sep)
  57. return f"ZipPath({ossep.join(parts)!r})"
  58. @property
  59. def sep(self):
  60. """
  61. Return a zip directory separator.
  62. @return: The zip directory separator.
  63. @returntype: The same type as C{self.path}.
  64. """
  65. return _coerceToFilesystemEncoding(self.path, ZIP_PATH_SEP)
  66. def parent(self):
  67. splitup = self.pathInArchive.split(self.sep)
  68. if len(splitup) == 1:
  69. return self.archive
  70. return ZipPath(self.archive, self.sep.join(splitup[:-1]))
  71. def child(self, path):
  72. """
  73. Return a new ZipPath representing a path in C{self.archive} which is
  74. a child of this path.
  75. @note: Requesting the C{".."} (or other special name) child will not
  76. cause L{InsecurePath} to be raised since these names do not have
  77. any special meaning inside a zip archive. Be particularly
  78. careful with the C{path} attribute (if you absolutely must use
  79. it) as this means it may include special names with special
  80. meaning outside of the context of a zip archive.
  81. """
  82. joiner = _coerceToFilesystemEncoding(path, ZIP_PATH_SEP)
  83. pathInArchive = _coerceToFilesystemEncoding(path, self.pathInArchive)
  84. return ZipPath(self.archive, joiner.join([pathInArchive, path]))
  85. def sibling(self, path):
  86. return self.parent().child(path)
  87. def exists(self):
  88. return self.isdir() or self.isfile()
  89. def isdir(self):
  90. return self.pathInArchive in self.archive.childmap
  91. def isfile(self):
  92. return self.pathInArchive in self.archive.zipfile.NameToInfo
  93. def islink(self):
  94. return False
  95. def listdir(self):
  96. if self.exists():
  97. if self.isdir():
  98. return list(self.archive.childmap[self.pathInArchive].keys())
  99. else:
  100. raise UnlistableError(OSError(errno.ENOTDIR, "Leaf zip entry listed"))
  101. else:
  102. raise UnlistableError(
  103. OSError(errno.ENOENT, "Non-existent zip entry listed")
  104. )
  105. def splitext(self):
  106. """
  107. Return a value similar to that returned by C{os.path.splitext}.
  108. """
  109. # This happens to work out because of the fact that we use OS-specific
  110. # path separators in the constructor to construct our fake 'path'
  111. # attribute.
  112. return os.path.splitext(self.path)
  113. def basename(self):
  114. return self.pathInArchive.split(self.sep)[-1]
  115. def dirname(self):
  116. # XXX NOTE: This API isn't a very good idea on filepath, but it's even
  117. # less meaningful here.
  118. return self.parent().path
  119. def open(self, mode="r"):
  120. pathInArchive = _coerceToFilesystemEncoding("", self.pathInArchive)
  121. return self.archive.zipfile.open(pathInArchive, mode=mode)
  122. def changed(self):
  123. pass
  124. def getsize(self):
  125. """
  126. Retrieve this file's size.
  127. @return: file size, in bytes
  128. """
  129. pathInArchive = _coerceToFilesystemEncoding("", self.pathInArchive)
  130. return self.archive.zipfile.NameToInfo[pathInArchive].file_size
  131. def getAccessTime(self):
  132. """
  133. Retrieve this file's last access-time. This is the same as the last access
  134. time for the archive.
  135. @return: a number of seconds since the epoch
  136. """
  137. return self.archive.getAccessTime()
  138. def getModificationTime(self):
  139. """
  140. Retrieve this file's last modification time. This is the time of
  141. modification recorded in the zipfile.
  142. @return: a number of seconds since the epoch.
  143. """
  144. pathInArchive = _coerceToFilesystemEncoding("", self.pathInArchive)
  145. return time.mktime(
  146. self.archive.zipfile.NameToInfo[pathInArchive].date_time + (0, 0, 0)
  147. )
  148. def getStatusChangeTime(self):
  149. """
  150. Retrieve this file's last modification time. This name is provided for
  151. compatibility, and returns the same value as getmtime.
  152. @return: a number of seconds since the epoch.
  153. """
  154. return self.getModificationTime()
  155. class ZipArchive(ZipPath):
  156. """
  157. I am a L{FilePath}-like object which can wrap a zip archive as if it were a
  158. directory.
  159. It works similarly to L{FilePath} in L{bytes} and L{unicode} handling --
  160. instantiating with a L{bytes} will return a "bytes mode" L{ZipArchive},
  161. and instantiating with a L{unicode} will return a "text mode"
  162. L{ZipArchive}. Methods that return new L{ZipArchive} or L{ZipPath}
  163. instances will be in the mode of the argument to the creator method,
  164. converting if required.
  165. """
  166. @property
  167. def archive(self):
  168. return self
  169. def __init__(self, archivePathname):
  170. """
  171. Create a ZipArchive, treating the archive at archivePathname as a zip
  172. file.
  173. @param archivePathname: a L{bytes} or L{unicode}, naming a path in the
  174. filesystem.
  175. """
  176. self.path = archivePathname
  177. self.zipfile = ZipFile(_coerceToFilesystemEncoding("", archivePathname))
  178. self.pathInArchive = _coerceToFilesystemEncoding(archivePathname, "")
  179. # zipfile is already wasting O(N) memory on cached ZipInfo instances,
  180. # so there's no sense in trying to do this lazily or intelligently
  181. self.childmap: Dict[str, Dict[str, int]] = {}
  182. for name in self.zipfile.namelist():
  183. name = _coerceToFilesystemEncoding(self.path, name).split(self.sep)
  184. for x in range(len(name)):
  185. child = name[-x]
  186. parent = self.sep.join(name[:-x])
  187. if parent not in self.childmap:
  188. self.childmap[parent] = {}
  189. self.childmap[parent][child] = 1
  190. parent = _coerceToFilesystemEncoding(archivePathname, "")
  191. def child(self, path):
  192. """
  193. Create a ZipPath pointing at a path within the archive.
  194. @param path: a L{bytes} or L{unicode} with no path separators in it
  195. (either '/' or the system path separator, if it's different).
  196. """
  197. return ZipPath(self, path)
  198. def exists(self):
  199. """
  200. Returns C{True} if the underlying archive exists.
  201. """
  202. return FilePath(self.zipfile.filename).exists()
  203. def getAccessTime(self):
  204. """
  205. Return the archive file's last access time.
  206. """
  207. return FilePath(self.zipfile.filename).getAccessTime()
  208. def getModificationTime(self):
  209. """
  210. Return the archive file's modification time.
  211. """
  212. return FilePath(self.zipfile.filename).getModificationTime()
  213. def getStatusChangeTime(self):
  214. """
  215. Return the archive file's status change time.
  216. """
  217. return FilePath(self.zipfile.filename).getStatusChangeTime()
  218. def __repr__(self) -> str:
  219. return f"ZipArchive({os.path.abspath(self.path)!r})"
  220. __all__ = ["ZipArchive", "ZipPath"]