001package co.codewizards.cloudstore.core.util;
002
003import static co.codewizards.cloudstore.core.util.IOUtil.*;
004
005import java.io.BufferedInputStream;
006import java.io.BufferedOutputStream;
007import java.io.File;
008import java.io.FileInputStream;
009import java.io.FileOutputStream;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.OutputStream;
013import java.net.URL;
014import java.util.Properties;
015import java.util.zip.ZipEntry;
016import java.util.zip.ZipInputStream;
017import java.util.zip.ZipOutputStream;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022import co.codewizards.cloudstore.core.progress.ProgressMonitor;
023import co.codewizards.cloudstore.core.progress.SubProgressMonitor;
024
025public final class ZipUtil {
026        private static final Logger logger = LoggerFactory.getLogger(ZipUtil.class);
027
028        private ZipUtil() { }
029
030        /**
031         * Recursively zips all entries of the given zipInputFolder to
032         * a zipFile defined by zipOutputFile.
033         *
034         * @param zipOutputFile The file to write to (will be deleted if existent).
035         * @param zipInputFolder The inputFolder to zip.
036         * @throws IOException in case of an I/O error.
037         */
038        public static void zipFolder(File zipOutputFile, File zipInputFolder)
039        throws IOException
040        {
041                zipFolder(zipOutputFile, zipInputFolder, (ProgressMonitor) null);
042        }
043
044        /**
045         * Recursively zips all entries of the given zipInputFolder to
046         * a zipFile defined by zipOutputFile.
047         *
048         * @param zipOutputFile The file to write to (will be deleted if existent).
049         * @param zipInputFolder The inputFolder to zip.
050         * @param monitor an optional monitor for progress feedback (can be <code>null</code>).
051         * @throws IOException in case of an I/O error.
052         */
053        public static void zipFolder(File zipOutputFile, File zipInputFolder, ProgressMonitor monitor)
054        throws IOException
055        {
056                zipFilesRecursively(zipOutputFile, zipInputFolder.listFiles(), zipInputFolder.getAbsoluteFile(), monitor);
057        }
058
059        /**
060         * Recursively zips all given files to a zipFile defined by zipOutputFile.
061         *
062         * @param zipOutputFile The file to write to (will be deleted if existent).
063         * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
064         *              if <code>entryRoot</code> is <code>null</code>.
065         * @param entryRoot The root folder of all entries. Entries in subfolders will be
066         *              added relative to this. If <code>entryRoot==null</code>, all given files will be
067         *              added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
068         *              both be <code>null</code> at the same time.
069         * @throws IOException in case of an I/O error.
070         */
071        public static void zipFilesRecursively(File zipOutputFile, File[] files, File entryRoot)
072        throws IOException
073        {
074                zipFilesRecursively(zipOutputFile, files, entryRoot, (ProgressMonitor) null);
075        }
076
077        /**
078         * Recursively zips all given files to a zipFile defined by zipOutputFile.
079         *
080         * @param zipOutputFile The file to write to (will be deleted if existent).
081         * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
082         *              if <code>entryRoot</code> is <code>null</code>.
083         * @param entryRoot The root folder of all entries. Entries in subfolders will be
084         *              added relative to this. If <code>entryRoot==null</code>, all given files will be
085         *              added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
086         *              both be <code>null</code> at the same time.
087         * @param monitor an optional monitor for progress feedback (can be <code>null</code>).
088         * @throws IOException in case of an I/O error.
089         */
090        public static void zipFilesRecursively(File zipOutputFile, File[] files, File entryRoot, ProgressMonitor monitor)
091        throws IOException
092        {
093                FileOutputStream fout = new FileOutputStream(zipOutputFile);
094                ZipOutputStream out = new ZipOutputStream(fout);
095                try {
096                        zipFilesRecursively(out, zipOutputFile, files, entryRoot, monitor);
097                } finally {
098                        out.close();
099                }
100        }
101
102        /**
103         * Recursively writes all found files as entries into the given ZipOutputStream.
104         *
105         * @param out The ZipOutputStream to write to.
106         * @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether
107         *              your current output file is located within the zipped directory tree. You must not locate
108         *              your zip-output file within the source directory, if you leave this <code>null</code>.
109         * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
110         *              if <code>entryRoot</code> is <code>null</code>.
111         * @param entryRoot The root folder of all entries. Entries in subfolders will be
112         *              added relative to this. If <code>entryRoot==null</code>, all given files will be
113         *              added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
114         *              both be <code>null</code> at the same time.
115         * @throws IOException in case of an I/O error.
116         */
117        public static void zipFilesRecursively(ZipOutputStream out, File zipOutputFile, File[] files, File entryRoot)
118        throws IOException
119        {
120                zipFilesRecursively(out, zipOutputFile, files, entryRoot, (ProgressMonitor) null);
121        }
122
123        /**
124         * Recursively writes all found files as entries into the given ZipOutputStream.
125         *
126         * @param out The ZipOutputStream to write to.
127         * @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether
128         *              your current output file is located within the zipped directory tree. You must not locate
129         *              your zip-output file within the source directory, if you leave this <code>null</code>.
130         * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
131         *              if <code>entryRoot</code> is <code>null</code>.
132         * @param entryRoot The root folder of all entries. Entries in subfolders will be
133         *              added relative to this. If <code>entryRoot==null</code>, all given files will be
134         *              added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
135         *              both be <code>null</code> at the same time.
136         * @param monitor an optional monitor for progress feedback (can be <code>null</code>).
137         * @throws IOException in case of an I/O error.
138         */
139        public static void zipFilesRecursively(ZipOutputStream out, File zipOutputFile, File[] files, File entryRoot, ProgressMonitor monitor)
140        throws IOException
141        {
142                if (entryRoot == null && files == null)
143                        throw new IllegalArgumentException("entryRoot and files must not both be null!");
144
145                if (entryRoot != null && !entryRoot.isDirectory())
146                        throw new IllegalArgumentException("entryRoot is not a directory: "+entryRoot.getAbsolutePath());
147
148                if ( files == null ) {
149                        files = new File[] { entryRoot };
150                }
151
152                if (monitor != null) {
153                        int dirCount = 0;
154                        int fileCount = 0;
155                        for (File file : files) {
156                                if (file.isDirectory())
157                                        ++dirCount;
158                                else
159                                        ++fileCount;
160                        }
161
162                        monitor.beginTask("Zipping files", dirCount * 10 + fileCount);
163                }
164                try {
165                        byte[] buf = new byte[1024 * 5];
166                        for (File file : files) {
167                                if (zipOutputFile != null && file.equals(zipOutputFile)) {
168                                        if (monitor != null)
169                                                monitor.worked(1);
170
171                                        continue;
172                                }
173
174                                String relativePath = entryRoot == null ? file.getName() : getRelativePath(entryRoot, file.getAbsoluteFile());
175                                // The method ZipEntry.isDirectory checks for 'name.endsWith("/");' and thus seems not to take
176                                // File.separator into account. Furthermore, I browsed the web for source codes (both implementation and
177                                // usage of ZipFile/ZipEntry and it seems to always use '/' - even in Windows.
178                                // Thus, I assume that all backslashes should be converted to slashes here. Marco.
179                                relativePath = relativePath.replace('\\', '/');
180                                if ( file.isDirectory() ) {
181                                        // store directory (necessary, in case the directory is empty - otherwise it's lost)
182                                        relativePath += '/';
183                                        ZipEntry entry = new ZipEntry(relativePath);
184                                        entry.setTime(file.lastModified());
185                                        entry.setSize(0);
186                                        entry.setCompressedSize(0);
187                                        entry.setCrc(0);
188                                        entry.setMethod(ZipEntry.STORED);
189                                        out.putNextEntry(entry);
190                                        out.closeEntry();
191
192                                        // recurse
193                                        File[] dirFiles = file.listFiles();
194                                        if (dirFiles == null) {
195                                                logger.error("zipFilesRecursively: file.listFiles() returned null, even though file is a directory! file=\"{}\"", file.getAbsolutePath());
196                                                if (monitor != null)
197                                                        monitor.worked(10);
198                                        }
199                                        else {
200                                                zipFilesRecursively(
201                                                                out,
202                                                                zipOutputFile,
203                                                                dirFiles,
204                                                                entryRoot,
205                                                                monitor == null ? null : new SubProgressMonitor(monitor, 10)
206                                                );
207                                        }
208                                }
209                                else {
210                                        // Create a new zipEntry
211                                        BufferedInputStream in = new BufferedInputStream( new FileInputStream(file) );
212                                        ZipEntry entry = new ZipEntry(relativePath);
213                                        entry.setTime(file.lastModified());
214                                        out.putNextEntry(entry);
215
216                                        int len;
217                                        while ((len = in.read(buf)) > 0) {
218                                                out.write(buf, 0, len);
219                                        }
220
221                                        out.closeEntry();
222                                        in.close();
223
224                                        if (monitor != null)
225                                                monitor.worked(1);
226                                }
227                        } // end of for ( int i = 0; i < files.length; i++ )
228                } finally {
229                        if (monitor != null)
230                                monitor.done();
231                }
232        }
233
234        private static final String PROPERTY_KEY_ZIP_TIMESTAMP = "zip.timestamp";
235        private static final String PROPERTY_KEY_ZIP_FILESIZE = "zip.size";
236
237        /**
238         * Calls {@link #unzipArchiveIfModified(URL, File)} converting the File-parameter zipArchive to an url.
239         *
240         * @see #unzipArchiveIfModified(URL, File).
241         */
242        public static synchronized void unzipArchiveIfModified(File zipArchive, File unzipRootFolder)
243        throws IOException
244        {
245                unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder);
246        }
247
248        /**
249         * Unzip the given archive into the given folder, if the archive was modified
250         * after being unzipped the last time by this method.
251         * <p>
252         * The current implementation
253         * of this method creates a file named ".archive.properties" inside the
254         * <code>unzipRootFolder</code> and stores the <code>zipArchive</code>'s file size and
255         * last-modified-timestamp to decide whether a future call to this method needs
256         * to unzip the data again.
257         * </p>
258         * <p>
259         * Note, that this method deletes the <code>unzipRootFolder</code> prior to unzipping
260         * in order to guarantee that content which was removed from the <code>zipArchive</code> is not existing
261         * in the <code>unzipRootFolder</code> anymore, too.
262         * </p>
263         * TODO instead of being synchronized, this method should use lower (= operating-system) locking mechanisms. Marco.
264         *
265         * @param zipArchive The zip file to unzip.
266         * @param unzipRootFolder The folder to unzip to.
267         * @throws IOException in case of an I/O error.
268         */
269        public static synchronized void unzipArchiveIfModified(URL zipArchive, File unzipRootFolder)
270        throws IOException
271        {
272                File metaFile = new File(unzipRootFolder, ".archive.properties");
273                long timestamp = Long.MIN_VALUE;
274                long fileSize = Long.MIN_VALUE;
275
276                Properties properties = new Properties();
277                if (metaFile.exists()) {
278                        InputStream in = new FileInputStream(metaFile);
279                        try {
280                                properties.load(in);
281                        } finally {
282                                in.close();
283                        }
284
285                        String timestampS = properties.getProperty(PROPERTY_KEY_ZIP_TIMESTAMP);
286                        if (timestampS != null) {
287                                try {
288                                        timestamp = Long.parseLong(timestampS, 36);
289                                } catch (NumberFormatException x) {
290                                        // ignore
291                                }
292                        }
293
294                        String fileSizeS = properties.getProperty(PROPERTY_KEY_ZIP_FILESIZE);
295                        if (fileSizeS != null) {
296                                try {
297                                        fileSize = Long.parseLong(fileSizeS, 36);
298                                } catch (NumberFormatException x) {
299                                        // ignore
300                                }
301                        }
302                }
303
304                boolean doUnzip = true;
305                long zipLength = -1;
306                long zipLastModified = System.currentTimeMillis();
307
308                if ("file".equals(zipArchive.getProtocol())) {
309                        File fileToCheck = new File(Util.urlToUri(zipArchive));
310                        zipLastModified = fileToCheck.lastModified();
311                        zipLength = fileToCheck.length();
312                        doUnzip = !unzipRootFolder.exists() || zipLastModified != timestamp || zipLength != fileSize;
313                }
314
315                if (doUnzip) {
316                        deleteDirectoryRecursively(unzipRootFolder);
317                        unzipArchive(zipArchive, unzipRootFolder);
318                        properties.setProperty(PROPERTY_KEY_ZIP_FILESIZE, Long.toString(zipLength, 36));
319                        properties.setProperty(PROPERTY_KEY_ZIP_TIMESTAMP, Long.toString(zipLastModified, 36));
320                        OutputStream out = new FileOutputStream(metaFile);
321                        try {
322                                properties.store(out, null);
323                        } finally {
324                                out.close();
325                        }
326                }
327        }
328
329        /**
330         * Unzip the given archive into the given folder.
331         *
332         * @param zipArchive The zip file to unzip.
333         * @param unzipRootFolder The folder to unzip to.
334         * @throws IOException in case of an I/O error.
335         */
336        public static void unzipArchive(URL zipArchive, File unzipRootFolder)
337        throws IOException
338        {
339                ZipInputStream in = new ZipInputStream(zipArchive.openStream());
340                try {
341                        ZipEntry entry = null;
342                        while ((entry = in.getNextEntry()) != null) {
343                                if(entry.isDirectory()) {
344                                        // create the directory
345                                        File dir = new File(unzipRootFolder, entry.getName());
346                                        if (!dir.exists() && !dir.mkdirs())
347                                                throw new IllegalStateException("Could not create directory entry, possibly permission issues.");
348                                }
349                                else {
350                                        File file = new File(unzipRootFolder, entry.getName());
351
352                                        File dir = file.getParentFile();
353                                        if (dir.exists( )) {
354                                                assert (dir.isDirectory( ));
355                                        }
356                                        else {
357                                                dir.mkdirs( );
358                                        }
359
360                                        BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(file) );
361
362                                        int len;
363                                        byte[] buf = new byte[1024 * 5];
364                                        while( (len = in.read(buf)) > 0 )
365                                        {
366                                                out.write(buf, 0, len);
367                                        }
368                                        out.close();
369                                }
370                        }
371                } finally {
372                        if (in != null)
373                                in.close();
374                }
375        }
376
377        /**
378         * Calls {@link #unzipArchive(URL, File)} converting the File-parameter zipArchive to an url.
379         *
380         * @see #unzipArchive(URL, File).
381         */
382        public static void unzipArchive(File zipArchive, File unzipRootFolder)
383        throws IOException
384        {
385                unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder);
386        }
387}