1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ from zipfile import ZipFile , ZIP_STORED , ZipInfo
5
+
6
+
7
+ class UpdateableZipFile (ZipFile ):
8
+ """
9
+ Add delete (via remove_file) and update (via writestr and write methods)
10
+ To enable update features use UpdateableZipFile with the 'with statement',
11
+ Upon __exit__ (if updates were applied) a new zip file will override the exiting one with the updates
12
+ """
13
+
14
+ class DeleteMarker (object ):
15
+ pass
16
+
17
+ def __init__ (self , file , mode = "r" , compression = ZIP_STORED , allowZip64 = False ):
18
+ # Init base
19
+ super (UpdateableZipFile , self ).__init__ (file , mode = mode ,
20
+ compression = compression ,
21
+ allowZip64 = allowZip64 )
22
+ # track file to override in zip
23
+ self ._replace = {}
24
+ # Whether the with statement was called
25
+ self ._allow_updates = False
26
+
27
+ def writestr (self , zinfo_or_arcname , bytes , compress_type = None ):
28
+ if isinstance (zinfo_or_arcname , ZipInfo ):
29
+ name = zinfo_or_arcname .filename
30
+ else :
31
+ name = zinfo_or_arcname
32
+ # If the file exits, and needs to be overridden,
33
+ # mark the entry, and create a temp-file for it
34
+ # we allow this only if the with statement is used
35
+ if self ._allow_updates and name in self .namelist ():
36
+ temp_file = self ._replace [name ] = self ._replace .get (name ,
37
+ tempfile .TemporaryFile ())
38
+ temp_file .write (bytes )
39
+ # Otherwise just act normally
40
+ else :
41
+ super (UpdateableZipFile , self ).writestr (zinfo_or_arcname ,
42
+ bytes , compress_type = compress_type , compresslevel = None )
43
+
44
+ def write (self , filename , arcname = None , compress_type = None ):
45
+ arcname = arcname or filename
46
+ # If the file exits, and needs to be overridden,
47
+ # mark the entry, and create a temp-file for it
48
+ # we allow this only if the with statement is used
49
+ if self ._allow_updates and arcname in self .namelist ():
50
+ temp_file = self ._replace [arcname ] = self ._replace .get (arcname ,
51
+ tempfile .TemporaryFile ())
52
+ with open (filename , "rb" ) as source :
53
+ shutil .copyfileobj (source , temp_file )
54
+ # Otherwise just act normally
55
+ else :
56
+ super (UpdateableZipFile , self ).write (filename ,
57
+ arcname = arcname , compress_type = compress_type )
58
+
59
+ def __enter__ (self ):
60
+ # Allow updates
61
+ self ._allow_updates = True
62
+ return self
63
+
64
+ def __exit__ (self , exc_type , exc_val , exc_tb ):
65
+ # call base to close zip file, organically
66
+ try :
67
+ super (UpdateableZipFile , self ).__exit__ (exc_type , exc_val , exc_tb )
68
+ if len (self ._replace ) > 0 :
69
+ self ._rebuild_zip ()
70
+ finally :
71
+ # In case rebuild zip failed,
72
+ # be sure to still release all the temp files
73
+ self ._close_all_temp_files ()
74
+ self ._allow_updates = False
75
+
76
+ def _close_all_temp_files (self ):
77
+ for temp_file in self ._replace .values ():
78
+ if hasattr (temp_file , 'close' ):
79
+ temp_file .close ()
80
+
81
+ def remove_file (self , path ):
82
+ self ._replace [path ] = self .DeleteMarker ()
83
+
84
+ def _rebuild_zip (self ):
85
+ tempdir = tempfile .mkdtemp ()
86
+ try :
87
+ temp_zip_path = os .path .join (tempdir , 'new.zip' )
88
+ with ZipFile (self .filename , 'r' ) as zip_read :
89
+ # Create new zip with assigned properties
90
+ with ZipFile (temp_zip_path , 'w' , compression = self .compression ,
91
+ allowZip64 = self ._allowZip64 ) as zip_write :
92
+ for item in zip_read .infolist ():
93
+ # Check if the file should be replaced / or deleted
94
+ replacement = self ._replace .get (item .filename , None )
95
+ # If marked for deletion, do not copy file to new zipfile
96
+ if isinstance (replacement , self .DeleteMarker ):
97
+ del self ._replace [item .filename ]
98
+ continue
99
+ # If marked for replacement, copy temp_file, instead of old file
100
+ elif replacement is not None :
101
+ del self ._replace [item .filename ]
102
+ # Write replacement to archive,
103
+ # and then close it (deleting the temp file)
104
+ replacement .seek (0 )
105
+ data = replacement .read ()
106
+ replacement .close ()
107
+ else :
108
+ data = zip_read .read (item .filename )
109
+ zip_write .writestr (item , data , compresslevel = None )
110
+ # Override the archive with the updated one
111
+ shutil .move (temp_zip_path , self .filename )
112
+ finally :
113
+ shutil .rmtree (tempdir )
0 commit comments