package co.codewizards.cloudstore.core.util;
import static co.codewizards.cloudstore.core.io.StreamUtil.*;
import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
import static co.codewizards.cloudstore.core.util.IOUtil.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.progress.ProgressMonitor;
import co.codewizards.cloudstore.core.progress.SubProgressMonitor;
public final class ZipUtil {
private static final Logger logger = LoggerFactory.getLogger(ZipUtil.class);
private ZipUtil() { }
/**
* Recursively zips all entries of the given zipInputFolder to
* a zipFile defined by zipOutputFile.
*
* @param zipOutputFile The file to write to (will be deleted if existent).
* @param zipInputFolder The inputFolder to zip.
* @throws IOException in case of an I/O error.
*/
public static void zipFolder(final File zipOutputFile, final File zipInputFolder)
throws IOException
{
zipFolder(zipOutputFile, zipInputFolder, (ProgressMonitor) null);
}
/**
* Recursively zips all entries of the given zipInputFolder to
* a zipFile defined by zipOutputFile.
*
* @param zipOutputFile The file to write to (will be deleted if existent).
* @param zipInputFolder The inputFolder to zip.
* @param monitor an optional monitor for progress feedback (can be <code>null</code>).
* @throws IOException in case of an I/O error.
*/
public static void zipFolder(final File zipOutputFile, final File zipInputFolder, final ProgressMonitor monitor)
throws IOException
{
zipFilesRecursively(zipOutputFile, zipInputFolder.listFiles(), zipInputFolder.getAbsoluteFile(), monitor);
}
/**
* Recursively zips all given files to a zipFile defined by zipOutputFile.
*
* @param zipOutputFile The file to write to (will be deleted if existent).
* @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
* if <code>entryRoot</code> is <code>null</code>.
* @param entryRoot The root folder of all entries. Entries in subfolders will be
* added relative to this. If <code>entryRoot==null</code>, all given files will be
* added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
* both be <code>null</code> at the same time.
* @throws IOException in case of an I/O error.
*/
public static void zipFilesRecursively(final File zipOutputFile, final File[] files, final File entryRoot)
throws IOException
{
zipFilesRecursively(zipOutputFile, files, entryRoot, (ProgressMonitor) null);
}
/**
* Recursively zips all given files to a zipFile defined by zipOutputFile.
*
* @param zipOutputFile The file to write to (will be deleted if existent).
* @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
* if <code>entryRoot</code> is <code>null</code>.
* @param entryRoot The root folder of all entries. Entries in subfolders will be
* added relative to this. If <code>entryRoot==null</code>, all given files will be
* added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
* both be <code>null</code> at the same time.
* @param monitor an optional monitor for progress feedback (can be <code>null</code>).
* @throws IOException in case of an I/O error.
*/
public static void zipFilesRecursively(final File zipOutputFile, final File[] files, final File entryRoot, final ProgressMonitor monitor)
throws IOException
{
final OutputStream fout = castStream(zipOutputFile.createOutputStream());
final ZipOutputStream out = new ZipOutputStream(fout);
try {
zipFilesRecursively(out, zipOutputFile, files, entryRoot, monitor);
} finally {
out.close();
}
}
/**
* Recursively writes all found files as entries into the given ZipOutputStream.
*
* @param out The ZipOutputStream to write to.
* @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether
* your current output file is located within the zipped directory tree. You must not locate
* your zip-output file within the source directory, if you leave this <code>null</code>.
* @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
* if <code>entryRoot</code> is <code>null</code>.
* @param entryRoot The root folder of all entries. Entries in subfolders will be
* added relative to this. If <code>entryRoot==null</code>, all given files will be
* added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
* both be <code>null</code> at the same time.
* @throws IOException in case of an I/O error.
*/
public static void zipFilesRecursively(final ZipOutputStream out, final File zipOutputFile, final File[] files, final File entryRoot)
throws IOException
{
zipFilesRecursively(out, zipOutputFile, files, entryRoot, (ProgressMonitor) null);
}
/**
* Recursively writes all found files as entries into the given ZipOutputStream.
*
* @param out The ZipOutputStream to write to.
* @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether
* your current output file is located within the zipped directory tree. You must not locate
* your zip-output file within the source directory, if you leave this <code>null</code>.
* @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>,
* if <code>entryRoot</code> is <code>null</code>.
* @param entryRoot The root folder of all entries. Entries in subfolders will be
* added relative to this. If <code>entryRoot==null</code>, all given files will be
* added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not
* both be <code>null</code> at the same time.
* @param monitor an optional monitor for progress feedback (can be <code>null</code>).
* @throws IOException in case of an I/O error.
*/
public static void zipFilesRecursively(final ZipOutputStream out, final File zipOutputFile, File[] files, final File entryRoot, final ProgressMonitor monitor)
throws IOException
{
if (entryRoot == null && files == null)
throw new IllegalArgumentException("entryRoot and files must not both be null!");
if (entryRoot != null && !entryRoot.isDirectory())
throw new IllegalArgumentException("entryRoot is not a directory: "+entryRoot.getAbsolutePath());
if ( files == null ) {
files = new File[] { entryRoot };
}
if (monitor != null) {
int dirCount = 0;
int fileCount = 0;
for (final File file : files) {
if (file.isDirectory())
++dirCount;
else
++fileCount;
}
monitor.beginTask("Zipping files", dirCount * 10 + fileCount);
}
try {
final byte[] buf = new byte[1024 * 5];
for (final File file : files) {
if (zipOutputFile != null && file.equals(zipOutputFile)) {
if (monitor != null)
monitor.worked(1);
continue;
}
String relativePath = entryRoot == null ? file.getName() : getRelativePath(entryRoot, file.getAbsoluteFile());
// The method ZipEntry.isDirectory checks for 'name.endsWith("/");' and thus seems not to take
// File.separator into account. Furthermore, I browsed the web for source codes (both implementation and
// usage of ZipFile/ZipEntry and it seems to always use '/' - even in Windows.
// Thus, I assume that all backslashes should be converted to slashes here. Marco.
relativePath = relativePath.replace('\\', '/');
if ( file.isDirectory() ) {
// store directory (necessary, in case the directory is empty - otherwise it's lost)
relativePath += '/';
final ZipEntry entry = new ZipEntry(relativePath);
entry.setTime(file.lastModified());
entry.setSize(0);
entry.setCompressedSize(0);
entry.setCrc(0);
entry.setMethod(ZipEntry.STORED);
out.putNextEntry(entry);
out.closeEntry();
// recurse
final File[] dirFiles = file.listFiles();
if (dirFiles == null) {
logger.error("zipFilesRecursively: file.listFiles() returned null, even though file is a directory! file=\"{}\"", file.getAbsolutePath());
if (monitor != null)
monitor.worked(10);
}
else {
zipFilesRecursively(
out,
zipOutputFile,
dirFiles,
entryRoot,
monitor == null ? null : new SubProgressMonitor(monitor, 10)
);
}
}
else {
// Create a new zipEntry
final BufferedInputStream in = new BufferedInputStream(castStream(file.createInputStream()));
final ZipEntry entry = new ZipEntry(relativePath);
entry.setTime(file.lastModified());
out.putNextEntry(entry);
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.closeEntry();
in.close();
if (monitor != null)
monitor.worked(1);
}
} // end of for ( int i = 0; i < files.length; i++ )
} finally {
if (monitor != null)
monitor.done();
}
}
private static final String PROPERTY_KEY_ZIP_TIMESTAMP = "zip.timestamp";
private static final String PROPERTY_KEY_ZIP_FILESIZE = "zip.size";
/**
* Calls {@link #unzipArchiveIfModified(URL, File)} converting the File-parameter zipArchive to an url.
*
* @see #unzipArchiveIfModified(URL, File).
*/
public static synchronized void unzipArchiveIfModified(final File zipArchive, final File unzipRootFolder)
throws IOException
{
unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder);
}
/**
* Unzip the given archive into the given folder, if the archive was modified
* after being unzipped the last time by this method.
* <p>
* The current implementation
* of this method creates a file named ".archive.properties" inside the
* <code>unzipRootFolder</code> and stores the <code>zipArchive</code>'s file size and
* last-modified-timestamp to decide whether a future call to this method needs
* to unzip the data again.
* </p>
* <p>
* Note, that this method deletes the <code>unzipRootFolder</code> prior to unzipping
* in order to guarantee that content which was removed from the <code>zipArchive</code> is not existing
* in the <code>unzipRootFolder</code> anymore, too.
* </p>
* TODO instead of being synchronized, this method should use lower (= operating-system) locking mechanisms. Marco.
*
* @param zipArchive The zip file to unzip.
* @param unzipRootFolder The folder to unzip to.
* @throws IOException in case of an I/O error.
*/
public static synchronized void unzipArchiveIfModified(final URL zipArchive, final File unzipRootFolder)
throws IOException
{
final File metaFile = createFile(unzipRootFolder, ".archive.properties");
long timestamp = Long.MIN_VALUE;
long fileSize = Long.MIN_VALUE;
final Properties properties = new Properties();
if (metaFile.exists()) {
final InputStream in = castStream(metaFile.createInputStream());
try {
properties.load(in);
} finally {
in.close();
}
final String timestampS = properties.getProperty(PROPERTY_KEY_ZIP_TIMESTAMP);
if (timestampS != null) {
try {
timestamp = Long.parseLong(timestampS, 36);
} catch (final NumberFormatException x) {
// ignore
}
}
final String fileSizeS = properties.getProperty(PROPERTY_KEY_ZIP_FILESIZE);
if (fileSizeS != null) {
try {
fileSize = Long.parseLong(fileSizeS, 36);
} catch (final NumberFormatException x) {
// ignore
}
}
}
boolean doUnzip = true;
long zipLength = -1;
long zipLastModified = System.currentTimeMillis();
if ("file".equals(zipArchive.getProtocol())) {
final File fileToCheck = createFile(UrlUtil.urlToUri(zipArchive));
zipLastModified = fileToCheck.lastModified();
zipLength = fileToCheck.length();
doUnzip = !unzipRootFolder.exists() || zipLastModified != timestamp || zipLength != fileSize;
}
if (doUnzip) {
deleteDirectoryRecursively(unzipRootFolder);
unzipArchive(zipArchive, unzipRootFolder);
properties.setProperty(PROPERTY_KEY_ZIP_FILESIZE, Long.toString(zipLength, 36));
properties.setProperty(PROPERTY_KEY_ZIP_TIMESTAMP, Long.toString(zipLastModified, 36));
try (final OutputStream out = castStream(metaFile.createOutputStream())) {
properties.store(out, null);
}
}
}
/**
* Unzip the given archive into the given folder.
*
* @param zipArchive The zip file to unzip.
* @param unzipRootFolder The folder to unzip to.
* @throws IOException in case of an I/O error.
*/
public static void unzipArchive(final URL zipArchive, final File unzipRootFolder)
throws IOException
{
final ZipInputStream in = new ZipInputStream(zipArchive.openStream());
try {
ZipEntry entry = null;
while ((entry = in.getNextEntry()) != null) {
if(entry.isDirectory()) {
// create the directory
final File dir = createFile(unzipRootFolder, entry.getName());
if (!dir.exists() && !dir.mkdirs())
throw new IllegalStateException("Could not create directory entry, possibly permission issues.");
}
else {
final File file = createFile(unzipRootFolder, entry.getName());
final File dir = file.getParentFile();
if (dir.exists( )) {
assert (dir.isDirectory( ));
}
else {
dir.mkdirs( );
}
try (final BufferedOutputStream out = new BufferedOutputStream(castStream(file.createOutputStream()))) {
int len;
final byte[] buf = new byte[1024 * 5];
while( (len = in.read(buf)) > 0 )
{
out.write(buf, 0, len);
}
}
}
}
} finally {
if (in != null)
in.close();
}
}
/**
* Calls {@link #unzipArchive(URL, File)} converting the File-parameter zipArchive to an url.
*
* @see #unzipArchive(URL, File).
*/
public static void unzipArchive(final File zipArchive, final File unzipRootFolder)
throws IOException
{
unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder);
}
}