| "Read and write ZIP files" | 
 | # Written by James C. Ahlstrom jim@interet.com | 
 | # All rights transferred to CNRI pursuant to the Python contribution agreement | 
 |  | 
 | import struct, os, time | 
 | import binascii, py_compile | 
 |  | 
 | try: | 
 |     import zlib	# We may need its compression method | 
 | except: | 
 |     zlib = None | 
 |  | 
 | class _BadZipfile(Exception): | 
 |     pass | 
 | error = _BadZipfile	# The exception raised by this module | 
 |  | 
 | # constants for Zip file compression methods | 
 | ZIP_STORED = 0 | 
 | ZIP_DEFLATED = 8 | 
 | # Other ZIP compression methods not supported | 
 |  | 
 | # Here are some struct module formats for reading headers | 
 | structEndArchive = "<4s4H2lH"     # 9 items, end of archive, 22 bytes | 
 | stringEndArchive = "PK\005\006"   # magic number for end of archive record | 
 | structCentralDir = "<4s4B4H3l5H2l"# 19 items, central directory, 46 bytes | 
 | stringCentralDir = "PK\001\002"   # magic number for central directory | 
 | structFileHeader = "<4s2B4H3l2H"  # 12 items, file header record, 30 bytes | 
 | stringFileHeader = "PK\003\004"   # magic number for file header | 
 |  | 
 | def is_zipfile(filename): | 
 |     """Quickly see if file is a ZIP file by checking the magic number. | 
 |  | 
 | Will not accept a ZIP archive with an ending comment.""" | 
 |     try: | 
 |         fpin = open(filename, "rb") | 
 |         fpin.seek(-22, 2)		# Seek to end-of-file record | 
 |         endrec = fpin.read() | 
 |         fpin.close() | 
 |         if endrec[0:4] == "PK\005\006" and endrec[-2:] == "\000\000": | 
 |             return 1	# file has correct magic number | 
 |     except: | 
 |         pass | 
 |  | 
 | class ZipInfo: | 
 |     "Class with attributes describing each file in the ZIP archive" | 
 |     def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)): | 
 |         self.filename = filename	# Name of the file in the archive | 
 |         self.date_time = date_time	# year, month, day, hour, min, sec | 
 |         # Standard values: | 
 |         self.compress_type = ZIP_STORED	# Type of compression for the file | 
 |         self.comment = ""		# Comment for each file | 
 |         self.extra = ""			# ZIP extra data | 
 |         self.create_system = 0		# System which created ZIP archive | 
 |         self.create_version = 20	# Version which created ZIP archive | 
 |         self.extract_version = 20	# Version needed to extract archive | 
 |         self.reserved = 0		# Must be zero | 
 |         self.flag_bits = 0		# ZIP flag bits | 
 |         self.volume = 0			# Volume number of file header | 
 |         self.internal_attr = 0		# Internal attributes | 
 |         self.external_attr = 0		# External file attributes | 
 |         # Other attributes are set by class ZipFile: | 
 |         # header_offset		Byte offset to the file header | 
 |         # file_offset		Byte offset to the start of the file data | 
 |         # CRC			CRC-32 of the uncompressed file | 
 |         # compress_size		Size of the compressed file | 
 |         # file_size		Size of the uncompressed file | 
 |  | 
 |     def FileHeader(self): | 
 |         'Return the per-file header as a string' | 
 |         dt = self.date_time | 
 |         dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] | 
 |         dostime = dt[3] << 11 | dt[4] << 5 | dt[5] / 2 | 
 |         if self.flag_bits & 0x08: | 
 |           # Set these to zero because we write them after the file data | 
 |           CRC = compress_size = file_size = 0 | 
 |         else: | 
 |           CRC = self.CRC | 
 |           compress_size = self.compress_size | 
 |           file_size = self.file_size | 
 |         header = struct.pack(structFileHeader, stringFileHeader, | 
 |                  self.extract_version, self.reserved, self.flag_bits, | 
 |                  self.compress_type, dostime, dosdate, CRC, | 
 |                  compress_size, file_size, | 
 |                  len(self.filename), len(self.extra)) | 
 |         return header + self.filename + self.extra | 
 |  | 
 |  | 
 | class ZipFile: | 
 |     "Class with methods to open, read, write, close, list zip files" | 
 |     def __init__(self, filename, mode="r", compression=ZIP_STORED): | 
 |         'Open the ZIP file with mode read "r", write "w" or append "a".' | 
 |         if compression == ZIP_STORED: | 
 |             pass | 
 |         elif compression == ZIP_DEFLATED: | 
 |             if not zlib: | 
 |                 raise RuntimeError,\ | 
 |                 "Compression requires the (missing) zlib module" | 
 |         else: | 
 |             raise RuntimeError, "That compression method is not supported" | 
 |         self.debug = 0	# Level of printing: 0 through 3 | 
 |         self.NameToInfo = {}	# Find file info given name | 
 |         self.filelist = []	# List of ZipInfo instances for archive | 
 |         self.compression = compression	# Method of compression | 
 |         self.filename = filename | 
 |         self.mode = key = mode[0] | 
 |         if key == 'r': | 
 |             self.fp = open(filename, "rb") | 
 |             self._GetContents() | 
 |         elif key == 'w': | 
 |             self.fp = open(filename, "wb") | 
 |         elif key == 'a': | 
 |             fp = self.fp = open(filename, "r+b") | 
 |             fp.seek(-22, 2)		# Seek to end-of-file record | 
 |             endrec = fp.read() | 
 |             if endrec[0:4] == stringEndArchive and \ | 
 |                        endrec[-2:] == "\000\000": | 
 |                 self._GetContents()	# file is a zip file | 
 |                 # seek to start of directory and overwrite | 
 |                 fp.seek(self.start_dir, 0) | 
 |             else:		# file is not a zip file, just append | 
 |                 fp.seek(0, 2) | 
 |         else: | 
 |             raise RuntimeError, 'Mode must be "r", "w" or "a"' | 
 |  | 
 |     def _GetContents(self): | 
 |         "Read in the table of contents for the zip file" | 
 |         fp = self.fp | 
 |         fp.seek(-22, 2)		# Start of end-of-archive record | 
 |         filesize = fp.tell() + 22	# Get file size | 
 |         endrec = fp.read(22)	# Archive must not end with a comment! | 
 |         if endrec[0:4] != stringEndArchive or endrec[-2:] != "\000\000": | 
 |             raise BadZipfile, "File is not a zip file, or ends with a comment" | 
 |         endrec = struct.unpack(structEndArchive, endrec) | 
 |         if self.debug > 1: | 
 |             print endrec | 
 |         size_cd = endrec[5]		# bytes in central directory | 
 |         offset_cd = endrec[6]	# offset of central directory | 
 |         x = filesize - 22 - size_cd | 
 |         # "concat" is zero, unless zip was concatenated to another file | 
 |         concat = x - offset_cd | 
 |         if self.debug > 2: | 
 |             print "given, inferred, offset", offset_cd, x, concat | 
 |         # self.start_dir:  Position of start of central directory | 
 |         self.start_dir = offset_cd + concat | 
 |         fp.seek(self.start_dir, 0) | 
 |         total = 0 | 
 |         while total < size_cd: | 
 |             centdir = fp.read(46) | 
 |             total = total + 46 | 
 |             if centdir[0:4] != stringCentralDir: | 
 |                 raise BadZipfile, "Bad magic number for central directory" | 
 |             centdir = struct.unpack(structCentralDir, centdir) | 
 |             if self.debug > 2: | 
 |                 print centdir | 
 |             filename = fp.read(centdir[12]) | 
 |             # Create ZipInfo instance to store file information | 
 |             x = ZipInfo(filename) | 
 |             x.extra = fp.read(centdir[13]) | 
 |             x.comment = fp.read(centdir[14]) | 
 |             total = total + centdir[12] + centdir[13] + centdir[14] | 
 |             x.header_offset = centdir[18] + concat | 
 |             x.file_offset = x.header_offset + 30 + centdir[12] + centdir[13] | 
 |             (x.create_version, x.create_system, x.extract_version, x.reserved, | 
 |                 x.flag_bits, x.compress_type, t, d, | 
 |                 x.CRC, x.compress_size, x.file_size) = centdir[1:12] | 
 |             x.volume, x.internal_attr, x.external_attr = centdir[15:18] | 
 |             # Convert date/time code to (year, month, day, hour, min, sec) | 
 |             x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F, | 
 |                                      t>>11, (t>>5)&0x3F, t&0x1F * 2 ) | 
 |             self.filelist.append(x) | 
 |             self.NameToInfo[x.filename] = x | 
 |             if self.debug > 2: | 
 |                 print "total", total | 
 |         for data in self.filelist: | 
 |             fp.seek(data.header_offset, 0) | 
 |             fheader = fp.read(30) | 
 |             if fheader[0:4] != stringFileHeader: | 
 |                 raise BadZipfile, "Bad magic number for file header" | 
 |             fheader = struct.unpack(structFileHeader, fheader) | 
 |             fname = fp.read(fheader[10]) | 
 |             if fname != data.filename: | 
 |                 raise RuntimeError, \ | 
 |  'File name in Central Directory "%s" and File Header "%s" differ.' % ( | 
 |                              data.filename, fname) | 
 |  | 
 |     def namelist(self): | 
 |         "Return a list of file names in the archive" | 
 |         l = [] | 
 |         for data in self.filelist: | 
 |             l.append(data.filename) | 
 |         return l | 
 |  | 
 |     def infolist(self): | 
 |         "Return a list of class ZipInfo instances for files in the archive" | 
 |         return self.filelist | 
 |  | 
 |     def printdir(self): | 
 |         "Print a table of contents for the zip file" | 
 |         print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size") | 
 |         for zinfo in self.filelist: | 
 |             date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time | 
 |             print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size) | 
 |  | 
 |     def testzip(self): | 
 |         "Read all the files and check the CRC" | 
 |         for zinfo in self.filelist: | 
 |             try: | 
 |                 self.read(zinfo.filename)	# Check CRC-32 | 
 |             except: | 
 |                 return zinfo.filename | 
 |  | 
 |     def getinfo(self, name): | 
 |         'Return the instance of ZipInfo given "name"' | 
 |         return self.NameToInfo[name] | 
 |  | 
 |     def read(self, name): | 
 |         "Return file bytes (as a string) for name" | 
 |         if self.mode not in ("r", "a"): | 
 |             raise RuntimeError, 'read() requires mode "r" or "a"' | 
 |         if not self.fp: | 
 |             raise RuntimeError, \ | 
 |             "Attempt to read ZIP archive that was already closed" | 
 |         zinfo = self.getinfo(name) | 
 |         filepos = self.fp.tell() | 
 |         self.fp.seek(zinfo.file_offset, 0) | 
 |         bytes = self.fp.read(zinfo.compress_size) | 
 |         self.fp.seek(filepos, 0) | 
 |         if zinfo.compress_type == ZIP_STORED: | 
 |             pass | 
 |         elif zinfo.compress_type == ZIP_DEFLATED: | 
 |             if not zlib: | 
 |                 raise RuntimeError, \ | 
 |                 "De-compression requires the (missing) zlib module" | 
 |             # zlib compress/decompress code by Jeremy Hylton of CNRI | 
 |             dc = zlib.decompressobj(-15) | 
 |             bytes = dc.decompress(bytes) | 
 |             # need to feed in unused pad byte so that zlib won't choke | 
 |             ex = dc.decompress('Z') + dc.flush() | 
 |             if ex: | 
 |                 bytes = bytes + ex | 
 |         else: | 
 |             raise BadZipfile, \ | 
 |             "Unsupported compression method %d for file %s" % \ | 
 |             (zinfo.compress_type, name) | 
 |         crc = binascii.crc32(bytes) | 
 |         if crc != zinfo.CRC: | 
 |             raise BadZipfile, "Bad CRC-32 for file %s" % name | 
 |         return bytes | 
 |  | 
 |     def _writecheck(self, zinfo): | 
 |         'Check for errors before writing a file to the archive' | 
 |         if self.NameToInfo.has_key(zinfo.filename): | 
 |             if self.debug:	# Warning for duplicate names | 
 |                 print "Duplicate name:", zinfo.filename | 
 |         if self.mode not in ("w", "a"): | 
 |             raise RuntimeError, 'write() requires mode "w" or "a"' | 
 |         if not self.fp: | 
 |             raise RuntimeError, \ | 
 |             "Attempt to write ZIP archive that was already closed" | 
 |         if zinfo.compress_type == ZIP_DEFLATED and not zlib: | 
 |             raise RuntimeError, \ | 
 |             "Compression requires the (missing) zlib module" | 
 |         if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED): | 
 |             raise RuntimeError, \ | 
 |             "That compression method is not supported" | 
 |  | 
 |     def write(self, filename, arcname=None, compress_type=None): | 
 |         'Put the bytes from filename into the archive under the name arcname.' | 
 |         st = os.stat(filename) | 
 |         mtime = time.localtime(st[8]) | 
 |         date_time = mtime[0:6] | 
 |         # Create ZipInfo instance to store file information | 
 |         if arcname is None: | 
 |           zinfo = ZipInfo(filename, date_time) | 
 |         else: | 
 |           zinfo = ZipInfo(arcname, date_time) | 
 |         zinfo.external_attr = st[0] << 16	# Unix attributes | 
 |         if compress_type is None: | 
 |           zinfo.compress_type = self.compression | 
 |         else: | 
 |           zinfo.compress_type = compress_type | 
 |         self._writecheck(zinfo) | 
 |         fp = open(filename, "rb") | 
 |         zinfo.flag_bits = 0x08 | 
 |         zinfo.header_offset = self.fp.tell()	# Start of header bytes | 
 |         self.fp.write(zinfo.FileHeader()) | 
 |         zinfo.file_offset = self.fp.tell()	# Start of file bytes | 
 |         CRC = 0 | 
 |         compress_size = 0 | 
 |         file_size = 0 | 
 |         if zinfo.compress_type == ZIP_DEFLATED: | 
 |             cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, | 
 |                  zlib.DEFLATED, -15) | 
 |         else: | 
 |             cmpr = None | 
 |         while 1: | 
 |             buf = fp.read(1024 * 8) | 
 |             if not buf: | 
 |                 break | 
 |             file_size = file_size + len(buf) | 
 |             CRC = binascii.crc32(buf, CRC) | 
 |             if cmpr: | 
 |                 buf = cmpr.compress(buf) | 
 |                 compress_size = compress_size + len(buf) | 
 |             self.fp.write(buf) | 
 |         fp.close() | 
 |         if cmpr: | 
 |             buf = cmpr.flush() | 
 |             compress_size = compress_size + len(buf) | 
 |             self.fp.write(buf) | 
 |             zinfo.compress_size = compress_size | 
 |         else: | 
 |             zinfo.compress_size = file_size | 
 |         zinfo.CRC = CRC | 
 |         zinfo.file_size = file_size | 
 |         # Write CRC and file sizes after the file data | 
 |         self.fp.write(struct.pack("<lll", zinfo.CRC, zinfo.compress_size, | 
 |               zinfo.file_size)) | 
 |         self.filelist.append(zinfo) | 
 |         self.NameToInfo[zinfo.filename] = zinfo | 
 |  | 
 |     def writestr(self, zinfo, bytes): | 
 |         'Write a file into the archive.  The contents is the string "bytes"' | 
 |         self._writecheck(zinfo) | 
 |         zinfo.file_size = len(bytes)		# Uncompressed size | 
 |         zinfo.CRC = binascii.crc32(bytes)	# CRC-32 checksum | 
 |         if zinfo.compress_type == ZIP_DEFLATED: | 
 |             co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, | 
 |                  zlib.DEFLATED, -15) | 
 |             bytes = co.compress(bytes) + co.flush() | 
 |             zinfo.compress_size = len(bytes)	# Compressed size | 
 |         else: | 
 |             zinfo.compress_size = zinfo.file_size | 
 |         zinfo.header_offset = self.fp.tell()	# Start of header bytes | 
 |         self.fp.write(zinfo.FileHeader()) | 
 |         zinfo.file_offset = self.fp.tell()	# Start of file bytes | 
 |         self.fp.write(bytes) | 
 |         if zinfo.flag_bits & 0x08: | 
 |           # Write CRC and file sizes after the file data | 
 |           self.fp.write(struct.pack("<lll", zinfo.CRC, zinfo.compress_size, | 
 |                 zinfo.file_size)) | 
 |         self.filelist.append(zinfo) | 
 |         self.NameToInfo[zinfo.filename] = zinfo | 
 |  | 
 |     def __del__(self): | 
 |         'Call the "close()" method in case the user forgot' | 
 |         if self.fp: | 
 |             self.fp.close() | 
 |             self.fp = None | 
 |  | 
 |     def close(self): | 
 |         'Close the file, and for mode "w" and "a" write the ending records' | 
 |         if self.mode in ("w", "a"):		# write ending records | 
 |             count = 0 | 
 |             pos1 = self.fp.tell() | 
 |             for zinfo in self.filelist:		# write central directory | 
 |                 count = count + 1 | 
 |                 dt = zinfo.date_time | 
 |                 dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2] | 
 |                 dostime = dt[3] << 11 | dt[4] << 5 | dt[5] / 2 | 
 |                 centdir = struct.pack(structCentralDir, | 
 |                   stringCentralDir, zinfo.create_version, | 
 |                   zinfo.create_system, zinfo.extract_version, zinfo.reserved, | 
 |                   zinfo.flag_bits, zinfo.compress_type, dostime, dosdate, | 
 |                   zinfo.CRC, zinfo.compress_size, zinfo.file_size, | 
 |                   len(zinfo.filename), len(zinfo.extra), len(zinfo.comment), | 
 |                   0, zinfo.internal_attr, zinfo.external_attr, | 
 |                   zinfo.header_offset) | 
 |                 self.fp.write(centdir) | 
 |                 self.fp.write(zinfo.filename) | 
 |                 self.fp.write(zinfo.extra) | 
 |                 self.fp.write(zinfo.comment) | 
 |             pos2 = self.fp.tell() | 
 |             # Write end-of-zip-archive record | 
 |             endrec = struct.pack(structEndArchive, stringEndArchive, | 
 |                      0, 0, count, count, pos2 - pos1, pos1, 0) | 
 |             self.fp.write(endrec) | 
 |         self.fp.close() | 
 |         self.fp = None | 
 |  | 
 |  | 
 | class PyZipFile(ZipFile): | 
 |     "Class to create ZIP archives with Python library files and packages" | 
 |     def writepy(self, pathname, basename = ""): | 
 |         """Add all files from "pathname" to the ZIP archive. | 
 |  | 
 | If pathname is a package directory, search the directory and all | 
 | package subdirectories recursively for all *.py and enter the modules into | 
 | the archive.  If pathname is a plain directory, listdir *.py and enter all | 
 | modules.  Else, pathname must be a Python *.py file and the module will be | 
 | put into the archive.  Added modules are always module.pyo or module.pyc. | 
 | This method will compile the module.py into module.pyc if necessary.""" | 
 |         dir, name = os.path.split(pathname) | 
 |         if os.path.isdir(pathname): | 
 |             initname = os.path.join(pathname, "__init__.py") | 
 |             if os.path.isfile(initname): | 
 |                 # This is a package directory, add it | 
 |                 if basename: | 
 |                     basename = "%s/%s" % (basename, name) | 
 |                 else: | 
 |                     basename = name | 
 |                 if self.debug: | 
 |                     print "Adding package in", pathname, "as", basename | 
 |                 fname, arcname = self._get_codename(initname[0:-3], basename) | 
 |                 if self.debug: | 
 |                     print "Adding", arcname | 
 |                 self.write(fname, arcname) | 
 |                 dirlist = os.listdir(pathname) | 
 |                 dirlist.remove("__init__.py") | 
 |                 # Add all *.py files and package subdirectories | 
 |                 for filename in dirlist: | 
 |                     path = os.path.join(pathname, filename) | 
 |                     root, ext = os.path.splitext(filename) | 
 |                     if os.path.isdir(path): | 
 |                         if os.path.isfile(os.path.join(path, "__init__.py")): | 
 |                             # This is a package directory, add it | 
 |                             self.writepy(path, basename)  # Recursive call | 
 |                     elif ext == ".py": | 
 |                         fname, arcname = self._get_codename(path[0:-3], | 
 |                                          basename) | 
 |                         if self.debug: | 
 |                             print "Adding", arcname | 
 |                         self.write(fname, arcname) | 
 |             else: | 
 |                 # This is NOT a package directory, add its files at top level | 
 |                 if self.debug: | 
 |                     print "Adding files from directory", pathname | 
 |                 for filename in os.listdir(pathname): | 
 |                     path = os.path.join(pathname, filename) | 
 |                     root, ext = os.path.splitext(filename) | 
 |                     if ext == ".py": | 
 |                         fname, arcname = self._get_codename(path[0:-3], | 
 |                                          basename) | 
 |                         if self.debug: | 
 |                             print "Adding", arcname | 
 |                         self.write(fname, arcname) | 
 |         else: | 
 |             if pathname[-3:] != ".py": | 
 |                 raise RuntimeError, \ | 
 |                 'Files added with writepy() must end with ".py"' | 
 |             fname, arcname = self._get_codename(pathname[0:-3], basename) | 
 |             if self.debug: | 
 |                 print "Adding file", arcname | 
 |             self.write(fname, arcname) | 
 |  | 
 |     def _get_codename(self, pathname, basename): | 
 |         """Return (filename, archivename) for the path. | 
 |  | 
 | Given a module name path, return the correct file path and archive name, | 
 | compiling if necessary.  For example, given /python/lib/string, | 
 | return (/python/lib/string.pyc, string)""" | 
 |         file_py  = pathname + ".py" | 
 |         file_pyc = pathname + ".pyc" | 
 |         file_pyo = pathname + ".pyo" | 
 |         if os.path.isfile(file_pyo) and \ | 
 |                             os.stat(file_pyo)[8] >= os.stat(file_py)[8]: | 
 |             fname = file_pyo	# Use .pyo file | 
 |         elif not os.path.isfile(file_pyc) or \ | 
 |                             os.stat(file_pyc)[8] < os.stat(file_py)[8]: | 
 |             if self.debug: | 
 |                 print "Compiling", file_py | 
 |             py_compile.compile(file_py, file_pyc) | 
 |             fname = file_pyc | 
 |         else: | 
 |             fname = file_pyc | 
 |         archivename = os.path.split(fname)[1] | 
 |         if basename: | 
 |             archivename = "%s/%s" % (basename, archivename) | 
 |         return (fname, archivename) | 
 |  |