MozZipFile.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. import os
  5. import time
  6. import zipfile
  7. from mozbuild.util import lock_file
  8. class ZipFile(zipfile.ZipFile):
  9. """ Class with methods to open, read, write, close, list zip files.
  10. Subclassing zipfile.ZipFile to allow for overwriting of existing
  11. entries, though only for writestr, not for write.
  12. """
  13. def __init__(self, file, mode="r", compression=zipfile.ZIP_STORED,
  14. lock = False):
  15. if lock:
  16. assert isinstance(file, basestring)
  17. self.lockfile = lock_file(file + '.lck')
  18. else:
  19. self.lockfile = None
  20. if mode == 'a' and lock:
  21. # appending to a file which doesn't exist fails, but we can't check
  22. # existence util we hold the lock
  23. if (not os.path.isfile(file)) or os.path.getsize(file) == 0:
  24. mode = 'w'
  25. zipfile.ZipFile.__init__(self, file, mode, compression)
  26. self._remove = []
  27. self.end = self.fp.tell()
  28. self.debug = 0
  29. def writestr(self, zinfo_or_arcname, bytes):
  30. """Write contents into the archive.
  31. The contents is the argument 'bytes', 'zinfo_or_arcname' is either
  32. a ZipInfo instance or the name of the file in the archive.
  33. This method is overloaded to allow overwriting existing entries.
  34. """
  35. if not isinstance(zinfo_or_arcname, zipfile.ZipInfo):
  36. zinfo = zipfile.ZipInfo(filename=zinfo_or_arcname,
  37. date_time=time.localtime(time.time()))
  38. zinfo.compress_type = self.compression
  39. # Add some standard UNIX file access permissions (-rw-r--r--).
  40. zinfo.external_attr = (0x81a4 & 0xFFFF) << 16L
  41. else:
  42. zinfo = zinfo_or_arcname
  43. # Now to the point why we overwrote this in the first place,
  44. # remember the entry numbers if we already had this entry.
  45. # Optimizations:
  46. # If the entry to overwrite is the last one, just reuse that.
  47. # If we store uncompressed and the new content has the same size
  48. # as the old, reuse the existing entry.
  49. doSeek = False # store if we need to seek to the eof after overwriting
  50. if self.NameToInfo.has_key(zinfo.filename):
  51. # Find the last ZipInfo with our name.
  52. # Last, because that's catching multiple overwrites
  53. i = len(self.filelist)
  54. while i > 0:
  55. i -= 1
  56. if self.filelist[i].filename == zinfo.filename:
  57. break
  58. zi = self.filelist[i]
  59. if ((zinfo.compress_type == zipfile.ZIP_STORED
  60. and zi.compress_size == len(bytes))
  61. or (i + 1) == len(self.filelist)):
  62. # make sure we're allowed to write, otherwise done by writestr below
  63. self._writecheck(zi)
  64. # overwrite existing entry
  65. self.fp.seek(zi.header_offset)
  66. if (i + 1) == len(self.filelist):
  67. # this is the last item in the file, just truncate
  68. self.fp.truncate()
  69. else:
  70. # we need to move to the end of the file afterwards again
  71. doSeek = True
  72. # unhook the current zipinfo, the writestr of our superclass
  73. # will add a new one
  74. self.filelist.pop(i)
  75. self.NameToInfo.pop(zinfo.filename)
  76. else:
  77. # Couldn't optimize, sadly, just remember the old entry for removal
  78. self._remove.append(self.filelist.pop(i))
  79. zipfile.ZipFile.writestr(self, zinfo, bytes)
  80. self.filelist.sort(lambda l, r: cmp(l.header_offset, r.header_offset))
  81. if doSeek:
  82. self.fp.seek(self.end)
  83. self.end = self.fp.tell()
  84. def close(self):
  85. """Close the file, and for mode "w" and "a" write the ending
  86. records.
  87. Overwritten to compact overwritten entries.
  88. """
  89. if not self._remove:
  90. # we don't have anything special to do, let's just call base
  91. r = zipfile.ZipFile.close(self)
  92. self.lockfile = None
  93. return r
  94. if self.fp.mode != 'r+b':
  95. # adjust file mode if we originally just wrote, now we rewrite
  96. self.fp.close()
  97. self.fp = open(self.filename, 'r+b')
  98. all = map(lambda zi: (zi, True), self.filelist) + \
  99. map(lambda zi: (zi, False), self._remove)
  100. all.sort(lambda l, r: cmp(l[0].header_offset, r[0].header_offset))
  101. # empty _remove for multiple closes
  102. self._remove = []
  103. lengths = [all[i+1][0].header_offset - all[i][0].header_offset
  104. for i in xrange(len(all)-1)]
  105. lengths.append(self.end - all[-1][0].header_offset)
  106. to_pos = 0
  107. for (zi, keep), length in zip(all, lengths):
  108. if not keep:
  109. continue
  110. oldoff = zi.header_offset
  111. # python <= 2.4 has file_offset
  112. if hasattr(zi, 'file_offset'):
  113. zi.file_offset = zi.file_offset + to_pos - oldoff
  114. zi.header_offset = to_pos
  115. self.fp.seek(oldoff)
  116. content = self.fp.read(length)
  117. self.fp.seek(to_pos)
  118. self.fp.write(content)
  119. to_pos += length
  120. self.fp.truncate()
  121. zipfile.ZipFile.close(self)
  122. self.lockfile = None