49

When I extract files from a ZIP file created with the Python zipfile module, all the files are not writable, read only etc.

The file is being created and extracted under Linux and Python 2.5.2.

As best I can tell, I need to set the ZipInfo.external_attr property for each file, but this doesn't seem to be documented anywhere I could find, can anyone enlighten me?

Dharman
  • 30,962
  • 25
  • 85
  • 135
Tom
  • 42,844
  • 35
  • 95
  • 101

8 Answers8

53

This seems to work (thanks Evan, putting it here so the line is in context):

buffer = "path/filename.zip"  # zip filename to write (or file-like object)
name = "folder/data.txt"      # name of file inside zip 
bytes = "blah blah blah"      # contents of file inside zip

zip = zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo(name)
info.external_attr = 0777 << 16L # give full access to included file
zip.writestr(info, bytes)
zip.close()

I'd still like to see something that documents this... An additional resource I found was a note on the Zip file format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT

Tom
  • 42,844
  • 35
  • 95
  • 101
29

This link has more information than anything else I've been able to find on the net. Even the zip source doesn't have anything. Copying the relevant section for posterity. This patch isn't really about documenting this format, which just goes to show how pathetic (read non-existent) the current documentation is.

# external_attr is 4 bytes in size. The high order two
# bytes represent UNIX permission and file type bits,
# while the low order two contain MS-DOS FAT file
# attributes, most notably bit 4 marking directories.
if node.isfile:
    zipinfo.compress_type = ZIP_DEFLATED
    zipinfo.external_attr = 0644 << 16L # permissions -r-wr--r--
    data = node.get_content().read()
    properties = node.get_properties()
    if 'svn:special' in properties and \
           data.startswith('link '):
        data = data[5:]
        zipinfo.external_attr |= 0120000 << 16L # symlink file type
        zipinfo.compress_type = ZIP_STORED
    if 'svn:executable' in properties:
        zipinfo.external_attr |= 0755 << 16L # -rwxr-xr-x
    zipfile.writestr(zipinfo, data)
elif node.isdir and path:
    if not zipinfo.filename.endswith('/'):
        zipinfo.filename += '/'
    zipinfo.compress_type = ZIP_STORED
    zipinfo.external_attr = 040755 << 16L # permissions drwxr-xr-x
    zipinfo.external_attr |= 0x10 # MS-DOS directory flag
    zipfile.writestr(zipinfo, '')

Also, this link has the following. Here the low order byte presumably means the rightmost (lowest) byte of the four bytes. So this one is for MS-DOS and can presumably be left as zero otherwise.

external file attributes: (4 bytes)

      The mapping of the external attributes is
      host-system dependent (see 'version made by').  For
      MS-DOS, the low order byte is the MS-DOS directory
      attribute byte.  If input came from standard input, this
      field is set to zero.

Also, the source file unix/unix.c in the sources for InfoZIP's zip program, downloaded from Debian's archives has the following in comments.

  /* lower-middle external-attribute byte (unused until now):
   *   high bit        => (have GMT mod/acc times) >>> NO LONGER USED! <<<
   *   second-high bit => have Unix UID/GID info
   * NOTE: The high bit was NEVER used in any official Info-ZIP release,
   *       but its future use should be avoided (if possible), since it
   *       was used as "GMT mod/acc times local extra field" flags in Zip beta
   *       versions 2.0j up to 2.0v, for about 1.5 years.
   */

So taking all this together, it looks like only the second highest byte is actually used, at least for Unix.

EDIT: I asked about the Unix aspect of this on Unix.SX, in the question "The zip format's external file attribute". Looks like I got a couple of things wrong. Specifically both of the top two bytes are used for Unix.

Community
  • 1
  • 1
Faheem Mitha
  • 6,096
  • 7
  • 48
  • 83
  • Some of the constants in the example would be more legible if using constants from the stat module (stat.S_IFLNK for example). While looking through this, I found also http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute/14727#14727 – Epu Nov 19 '13 at 20:48
  • 2
    @Epu Technically, there's not a guarantee that S_IFLNK will be equal to 0120000 - as I mentioned, "The Unix values are the same as on traditional unix implementations" and provided an example from one but the exact numeric values are not guaranteed by POSIX (nor is S_IFLNK actually guaranteed to exist as a constant) but 0120000 always means symlink in a zip context due to it being a cross-platform format. – Random832 Nov 19 '13 at 21:33
15

Look at this: Set permissions on a compressed file in python

I'm not entirely sure if that's what you want, but it seems to be.

The key line appears to be:

zi.external_attr = 0777 << 16L

It looks like it sets the permissions to 0777 there.

Community
  • 1
  • 1
Evan Fosmark
  • 98,895
  • 36
  • 105
  • 117
12

The earlier answers did not work for me (on OS X 10.12). I found that as well as the executable flags (octal 755), I also need to set the "regular file" flag (octal 100000). I found this mentioned here: https://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute

A complete example:

zipname = "test.zip"
filename = "test-executable"

zip = zipfile.ZipFile(zipname, 'w', zipfile.ZIP_DEFLATED)

f = open(filename, 'r')
bytes = f.read()
f.close()

info = zipfile.ZipInfo(filename)
info.date_time = time.localtime()
info.external_attr = 0100755 << 16L

zip.writestr(info, bytes, zipfile.ZIP_DEFLATED)

zip.close()

A complete example of my specific usecase, creating a zip of a .app so that everything in the folder Contents/MacOS/ is executable: https://gist.github.com/Draknek/3ce889860cea4f59838386a79cc11a85

Alan Hazelden
  • 121
  • 1
  • 3
5

You can extend the ZipFile class to change the default file permission:

from zipfile import ZipFile, ZipInfo
import time

class PermissiveZipFile(ZipFile):
    def writestr(self, zinfo_or_arcname, data, compress_type=None):
        if not isinstance(zinfo_or_arcname, ZipInfo):
            zinfo = ZipInfo(filename=zinfo_or_arcname,
                            date_time=time.localtime(time.time())[:6])

            zinfo.compress_type = self.compression
            if zinfo.filename[-1] == '/':
                zinfo.external_attr = 0o40775 << 16   # drwxrwxr-x
                zinfo.external_attr |= 0x10           # MS-DOS directory flag
            else:
                zinfo.external_attr = 0o664 << 16     # ?rw-rw-r--
        else:
            zinfo = zinfo_or_arcname

        super(PermissiveZipFile, self).writestr(zinfo, data, compress_type)

This example changes the default file permission to 664 and keeps 775 for directories.

Related code:

Soroush
  • 1,055
  • 2
  • 18
  • 26
2

Also look at what Python's zipfile module does:

def write(self, filename, arcname=None, compress_type=None):
    ...
    st = os.stat(filename)
    ...
    zinfo = ZipInfo(arcname, date_time)
    zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
    ...

```

thakis
  • 5,405
  • 1
  • 33
  • 33
2

To set permissions (Unix attributes) on a file in a ZIP file using Python's zipfile module, pass the attributes as bits 16-31 of the external_attr of ZipInfo.

The Python zipfile module accepts the 16-bit "Mode" field (that stores st_mode field from struct stat, containing user/group/other permissions, setuid/setgid and symlink info, etc) of the ASi extra block for Unix in the external_attr bits above mentioned.

You may also import the Python's "stat" module to get the mode constant definitions.

You may also set 3 in create_system to specify the operating system which created the ZIP archive: 3 = Unix; 0 = Windows.

Here is an example:

#!/usr/bin/python

import stat
import zipfile

def create_zip_with_symlink(output_zip_filename, link_source, link_target):
    zipInfo  = zipfile.ZipInfo(link_source)
    zipInfo.create_system = 3 
    unix_st_mode = stat.S_IFLNK | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH
    zipInfo.external_attr = unix_st_mode << 16 
    zipOut = zipfile.ZipFile(output_zip_filename, 'w', compression=zipfile.ZIP_DEFLATED)
    zipOut.writestr(zipInfo, link_target)
    zipOut.close()

create_zip_with_symlink('cpuinfo.zip', 'cpuinfo.txt', '/proc/cpuinfo')
Maxim Masiutin
  • 3,991
  • 4
  • 55
  • 72
0

When you do it like this, does it work alright?

zf = zipfile.ZipFile("something.zip")
for name in zf.namelist():
    f = open(name, 'wb')
    f.write(self.read(name))
    f.close()

If not, I'd suggest throwing in an os.chmod in the for loop with 0777 permissions like this:

zf = zipfile.ZipFile("something.zip")
for name in zf.namelist():
    f = open(name, 'wb')
    f.write(self.read(name))
    f.close()
    os.chmod(name, 0777)
Evan Fosmark
  • 98,895
  • 36
  • 105
  • 117
  • I'm not using Python to extract the zip, the zip is generated by a webserver and extracted using something on the user's machine. In my case the gnome archive manager program. – Tom Jan 12 '09 at 07:20