/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.StringTokenizer; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.apache.commons.io.IOUtils; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.modules.bc.meta.MetaInfo; import org.olat.core.commons.modules.bc.meta.tagged.MetaTagged; import org.olat.core.id.Identity; import org.olat.core.id.Roles; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.core.util.vfs.LocalFolderImpl; import org.olat.core.util.vfs.LocalImpl; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSLockManager; import org.olat.core.util.vfs.version.Versionable; /** * Initial Date: 04.12.2002 * * @author Mike Stock * * Comment: * */ public class ZipUtil { private static final String DIR_NAME__MACOSX = "__MACOSX/"; private static final OLog log = Tracing.createLoggerFor(ZipUtil.class); /** * Constructor for ZipUtil. */ public ZipUtil() { super(); } /** * Unzip a file to a directory * @param zipFile The zip file to unzip * @param targetDir The directory to unzip the file to * @return True if successfull, false otherwise */ public static boolean unzip(File zipFile, File targetDir) { try { long s = System.currentTimeMillis(); xxunzip (new FileInputStream(zipFile), targetDir.getAbsolutePath()); log.info("unzip file="+zipFile.getName()+" to="+targetDir.getAbsolutePath() +" t="+Long.toString(System.currentTimeMillis()-s)); return true; } catch (IOException e) { log.error("I/O failure while unzipping "+zipFile.getAbsolutePath()+" to "+targetDir.getAbsolutePath()); return false; } } /** * Unzip a VFSLeaf (zip zip archive file) to a directory * @param zipLeaf zip archive file to unzip * @param targetDir The directory to unzip the file to * @return True if successfull, false otherwise */ public static boolean unzip(VFSLeaf zipLeaf, VFSContainer targetDir) { if (targetDir instanceof LocalFolderImpl) { String outdir = ((LocalFolderImpl) targetDir).getBasefile().getAbsolutePath(); try { long s = System.currentTimeMillis(); xxunzip (zipLeaf.getInputStream(), outdir); log.info("unzip file="+zipLeaf.getName()+" to="+outdir +" t="+Long.toString(System.currentTimeMillis()-s)); return true; } catch (IOException e) { log.error("I/O failure while unzipping "+zipLeaf.getName()+" to "+outdir); return false; } } return unzip(zipLeaf, targetDir, null, false); } /** * Unzip a file in the target dir with the restricted version * @param zipFile * @param targetDir * @return */ public static boolean unzipStrict(File zipFile, VFSContainer targetDir) { if (targetDir instanceof LocalFolderImpl) { String outdir = ((LocalFolderImpl) targetDir).getBasefile().getAbsolutePath(); InputStream in = null; try { long s = System.currentTimeMillis(); in = new FileInputStream(zipFile); xxunzip (in, outdir); log.info("unzip file="+zipFile.getName()+" to="+outdir +" t="+Long.toString(System.currentTimeMillis()-s)); return true; } catch (IOException e) { log.error("I/O failure while unzipping "+zipFile.getName()+" to "+outdir); return false; } finally { IOUtils.closeQuietly(in); } } return false; } /** * Unzip a file to a directory using the versioning system of VFS * @param zipLeaf The file to unzip * @param targetDir The directory to unzip the file to * @param the identity of who unzip the file * @param versioning enabled or not * @return True if successfull, false otherwise */ public static boolean unzip(VFSLeaf zipLeaf, VFSContainer targetDir, Identity identity, boolean versioning) { InputStream in = zipLeaf.getInputStream(); boolean unzipped = unzip(in, targetDir, identity, versioning); FileUtils.closeSafely(in); return unzipped; } /** * Unzip an inputstream to a directory using the versioning system of VFS * @param zipLeaf The file to unzip * @param targetDir The directory to unzip the file to * @param the identity of who unzip the file * @param versioning enabled or not * @return True if successfull, false otherwise */ private static boolean unzip(InputStream in, VFSContainer targetDir, Identity identity, boolean versioning) { ZipInputStream oZip = new ZipInputStream(in); try { // unzip files ZipEntry oEntr = oZip.getNextEntry(); while (oEntr != null) { if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) { if (oEntr.isDirectory()) { // skip MacOSX specific metadata directory // create directories getAllSubdirs(targetDir, oEntr.getName(), identity, true); } else { // create file VFSContainer createIn = targetDir; String name = oEntr.getName(); // check if entry has directories which did not show up as // directories above int dirSepIndex = name.lastIndexOf('/'); if (dirSepIndex == -1) { // try it windows style, backslash is also valid format dirSepIndex = name.lastIndexOf('\\'); } if (dirSepIndex > 0) { // create subdirs createIn = getAllSubdirs(targetDir, name.substring(0, dirSepIndex), identity, true); if (createIn == null) { if (log.isDebug()) log.debug("Error creating directory structure for zip entry: " + oEntr.getName()); return false; } name = name.substring(dirSepIndex + 1); } if(versioning) { VFSLeaf newEntry = (VFSLeaf)createIn.resolve(name); if(newEntry == null) { newEntry = createIn.createChildLeaf(name); OutputStream out = newEntry.getOutputStream(false); if (!FileUtils.copy(oZip, out)) return false; FileUtils.closeSafely(out); } else if (newEntry instanceof Versionable) { Versionable versionable = (Versionable)newEntry; if(versionable.getVersions().isVersioned()) { versionable.getVersions().addVersion(identity, "", oZip); } } if(newEntry instanceof MetaTagged) { MetaInfo info = ((MetaTagged)newEntry).getMetaInfo(); if(info != null) { info.setAuthor(identity); info.write(); } } } else { VFSLeaf newEntry = createIn.createChildLeaf(name); if (newEntry != null) { OutputStream out = newEntry.getOutputStream(false); if (!FileUtils.copy(oZip, out)) return false; FileUtils.closeSafely(out); } if(newEntry instanceof MetaTagged) { MetaInfo info = ((MetaTagged)newEntry).getMetaInfo(); if(info != null && identity != null) { info.setAuthor(identity); info.write(); } } } } } oZip.closeEntry(); oEntr = oZip.getNextEntry(); } } catch (IOException e) { return false; } finally { FileUtils.closeSafely(oZip); } return true; } // unzip /** * Unzip a file to a directory using the versioning system of VFS and a ZIP * library which handle encoding errors. It may results in special characters * wrongly translated on the file system. * @param zipLeaf The file to unzip * @param targetDir The directory to unzip the file to * @param the identity of who unzip the file * @param versioning enabled or not * @return True if successfull, false otherwise */ public static boolean unzipNonStrict(VFSLeaf zipLeaf, VFSContainer targetDir, Identity identity, boolean versioning) { InputStream in = zipLeaf.getInputStream(); boolean unzipped = unzipNonStrict(in, targetDir, identity, versioning); FileUtils.closeSafely(in); return unzipped; } /** * Unzip with jazzlib * @param in * @param targetDir * @param identity * @param versioning * @return */ private static boolean unzipNonStrict(InputStream in, VFSContainer targetDir, Identity identity, boolean versioning) { net.sf.jazzlib.ZipInputStream oZip = new net.sf.jazzlib.ZipInputStream(in); try { // unzip files net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry(); while (oEntr != null) { if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) { if (oEntr.isDirectory()) { // skip MacOSX specific metadata directory // create directories getAllSubdirs(targetDir, oEntr.getName(), identity, true); } else { // create file VFSContainer createIn = targetDir; String name = oEntr.getName(); // check if entry has directories which did not show up as // directories above int dirSepIndex = name.lastIndexOf('/'); if (dirSepIndex == -1) { // try it windows style, backslash is also valid format dirSepIndex = name.lastIndexOf('\\'); } if (dirSepIndex > 0) { // create subdirs createIn = getAllSubdirs(targetDir, name.substring(0, dirSepIndex), identity, true); if (createIn == null) { if (log.isDebug()) log.debug("Error creating directory structure for zip entry: " + oEntr.getName()); return false; } name = name.substring(dirSepIndex + 1); } if(versioning) { VFSLeaf newEntry = (VFSLeaf)createIn.resolve(name); if(newEntry == null) { newEntry = createIn.createChildLeaf(name); OutputStream out = newEntry.getOutputStream(false); if (!FileUtils.copy(oZip, out)) return false; FileUtils.closeSafely(out); } else if (newEntry instanceof Versionable) { Versionable versionable = (Versionable)newEntry; if(versionable.getVersions().isVersioned()) { versionable.getVersions().addVersion(identity, "", oZip); } } if(newEntry instanceof MetaTagged) { MetaInfo info = ((MetaTagged)newEntry).getMetaInfo(); if(info != null) { info.setAuthor(identity); info.write(); } } } else { VFSLeaf newEntry = createIn.createChildLeaf(name); if (newEntry != null) { OutputStream out = newEntry.getOutputStream(false); if (!FileUtils.copy(oZip, out)) return false; FileUtils.closeSafely(out); } if(newEntry instanceof MetaTagged) { MetaInfo info = ((MetaTagged)newEntry).getMetaInfo(); if(info != null && identity != null) { info.setAuthor(identity); info.write(); } } } } } oZip.closeEntry(); oEntr = oZip.getNextEntry(); } } catch (IOException e) { return false; } finally { FileUtils.closeSafely(oZip); } return true; } // unzip /** * Check if a file in the zip is already in the path * @param zipLeaf * @param targetDir * @param identity * @param isAdmin * @return the list of files which already exist */ public static List<String> checkLockedFileBeforeUnzip(VFSLeaf zipLeaf, VFSContainer targetDir, Identity identity, Roles isAdmin) { List<String> lockedFiles = new ArrayList<String>(); InputStream in = zipLeaf.getInputStream(); ZipInputStream oZip = new ZipInputStream(in); VFSLockManager vfsLockManager = CoreSpringFactory.getImpl(VFSLockManager.class); try { // unzip files ZipEntry oEntr = oZip.getNextEntry(); while (oEntr != null) { if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) { if (oEntr.isDirectory()) { // skip MacOSX specific metadata directory // directories aren't locked oZip.closeEntry(); oEntr = oZip.getNextEntry(); continue; } else { // search file VFSContainer createIn = targetDir; String name = oEntr.getName(); // check if entry has directories which did not show up as // directories above int dirSepIndex = name.lastIndexOf('/'); if (dirSepIndex == -1) { // try it windows style, backslash is also valid format dirSepIndex = name.lastIndexOf('\\'); } if (dirSepIndex > 0) { // get subdirs createIn = getAllSubdirs(targetDir, name.substring(0, dirSepIndex), identity, false); if (createIn == null) { //sub directories don't exist, and aren't locked oZip.closeEntry(); oEntr = oZip.getNextEntry(); continue; } name = name.substring(dirSepIndex + 1); } VFSLeaf newEntry = (VFSLeaf)createIn.resolve(name); if(vfsLockManager.isLockedForMe(newEntry, identity, isAdmin)) { lockedFiles.add(name); } } } oZip.closeEntry(); oEntr = oZip.getNextEntry(); } } catch (IOException e) { return null; } finally { FileUtils.closeSafely(oZip); FileUtils.closeSafely(in); } return lockedFiles; } /** * * @param zipLeaf * @param targetDir * @param identity * @param roles * @return */ public static List<String> checkLockedFileBeforeUnzipNonStrict(VFSLeaf zipLeaf, VFSContainer targetDir, Identity identity, Roles roles) { List<String> lockedFiles = new ArrayList<String>(); InputStream in = zipLeaf.getInputStream(); net.sf.jazzlib.ZipInputStream oZip = new net.sf.jazzlib.ZipInputStream(in); VFSLockManager vfsLockManager = CoreSpringFactory.getImpl(VFSLockManager.class); try { // unzip files net.sf.jazzlib.ZipEntry oEntr = oZip.getNextEntry(); while (oEntr != null) { if (oEntr.getName() != null && !oEntr.getName().startsWith(DIR_NAME__MACOSX)) { if (oEntr.isDirectory()) { // skip MacOSX specific metadata directory // directories aren't locked oZip.closeEntry(); oEntr = oZip.getNextEntry(); continue; } else { // search file VFSContainer createIn = targetDir; String name = oEntr.getName(); // check if entry has directories which did not show up as // directories above int dirSepIndex = name.lastIndexOf('/'); if (dirSepIndex == -1) { // try it windows style, backslash is also valid format dirSepIndex = name.lastIndexOf('\\'); } if (dirSepIndex > 0) { // get subdirs createIn = getAllSubdirs(targetDir, name.substring(0, dirSepIndex), identity, false); if (createIn == null) { //sub directories don't exist, and aren't locked oZip.closeEntry(); oEntr = oZip.getNextEntry(); continue; } name = name.substring(dirSepIndex + 1); } VFSLeaf newEntry = (VFSLeaf)createIn.resolve(name); if(vfsLockManager.isLockedForMe(newEntry, identity, roles)) { lockedFiles.add(name); } } } oZip.closeEntry(); oEntr = oZip.getNextEntry(); } } catch (IOException e) { return null; } finally { FileUtils.closeSafely(oZip); FileUtils.closeSafely(in); } return lockedFiles; } /** * Get the whole subpath. * @param create the missing directories * @param base * @param subDirPath * @return Returns the last container of this subpath. */ public static VFSContainer getAllSubdirs(VFSContainer base, String subDirPath, Identity identity, boolean create) { StringTokenizer st; if (subDirPath.indexOf("/") != -1) { st = new StringTokenizer(subDirPath, "/", false); } else { // try it windows style, backslash is also valid format st = new StringTokenizer(subDirPath, "\\", false); } VFSContainer currentPath = base; while (st.hasMoreTokens()) { String nextSubpath = st.nextToken(); VFSItem vfsSubpath = currentPath.resolve(nextSubpath); if (vfsSubpath == null && !create) { return null; } if (vfsSubpath == null || (vfsSubpath instanceof VFSLeaf)) { vfsSubpath = currentPath.createChildContainer(nextSubpath); if (vfsSubpath == null) return null; if (identity != null && vfsSubpath instanceof MetaTagged) { MetaInfo info = ((MetaTagged)vfsSubpath).getMetaInfo(); if(info != null) { info.setAuthor(identity); info.write(); } } } currentPath = (VFSContainer)vfsSubpath; } return currentPath; } /** * Add the set of files residing in root to the ZIP file named target. * Files in subfolders will be compressed too. * if target already exists, this will abort and return false. * * @param files Filenames to add to ZIP, relative to root * @param root Base path. * @param target Target ZIP file. * @param compress to compress ot just store * @return true if successfull, false otherwise. */ public static boolean zip(Set<String> files, File root, File target, boolean compress) { // Create a buffer for reading the files if (target.exists()) return false; List<VFSItem> vfsFiles = new ArrayList<VFSItem>(); LocalFolderImpl vfsRoot = new LocalFolderImpl(root); for (Iterator<String> iter = files.iterator(); iter.hasNext();) { String fileName = iter.next(); VFSItem item = vfsRoot.resolve(fileName); if (item == null) return false; vfsFiles.add(item); } return zip(vfsFiles, new LocalFileImpl(target), compress); } // zip /** * Add the set of files residing in root to the ZIP file named target. * Files in subfolders will be compressed too. * * @param files Filenames to add to ZIP, relative to root * @param root Base path. * @param target Target ZIP file. * @param compress to compress ot just store * @return true if successfull, false otherwise. */ public static boolean zip(Set<String> files, File root, File target) { return zip(files, root, target, true); } // zip public static boolean zip(List<VFSItem> vfsFiles, VFSLeaf target, boolean compress) { boolean success = true; String zname = target.getName(); if (target instanceof LocalImpl) { zname = ((LocalImpl)target).getBasefile().getAbsolutePath(); } OutputStream out = target.getOutputStream(false); if (out == null) { throw new OLATRuntimeException(ZipUtil.class, "Error getting output stream for file: " + zname, null); } long s = System.currentTimeMillis(); ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(out, FileUtils.BSIZE)); if (vfsFiles.size() == 0) { try { zipOut.close(); } catch (IOException e) { // } return true; } zipOut.setLevel(compress?9:0); for (Iterator<VFSItem> iter = vfsFiles.iterator(); success && iter.hasNext();) { success = addToZip(iter.next(), "", zipOut); } try { zipOut.flush(); zipOut.close(); log.info("zipped ("+(compress?"compress":"store")+") "+zname+" t="+Long.toString(System.currentTimeMillis()-s)); } catch (IOException e) { throw new OLATRuntimeException(ZipUtil.class, "I/O error closing file: " + zname, null); } return success; } public static boolean addToZip(VFSItem vfsItem, String currentPath, ZipOutputStream out) { boolean success = true; InputStream in = null; byte[] buffer = new byte[FileUtils.BSIZE]; try { // The separator / is the separator defined by the ZIP standard String itemName = currentPath.length() == 0 ? vfsItem.getName() : currentPath + "/" + vfsItem.getName(); if (vfsItem instanceof VFSContainer) { out.putNextEntry(new ZipEntry(itemName + "/")); out.closeEntry(); List<VFSItem> items = ((VFSContainer)vfsItem).getItems(); for (Iterator<VFSItem> iter = items.iterator(); iter.hasNext();) { if (!addToZip(iter.next(), itemName, out)) { success = false; break; } } } else { out.putNextEntry(new ZipEntry(itemName)); in = ((VFSLeaf)vfsItem).getInputStream(); int c; while ((c = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, c); } out.closeEntry(); } } catch (IOException ioe) { String name = vfsItem.getName(); if (vfsItem instanceof LocalImpl) { name = ((LocalImpl)vfsItem).getBasefile().getAbsolutePath(); } log.error("I/O error while adding "+name+" to zip:"+ioe); return false; } finally { FileUtils.closeSafely(in); } return success; } /** * Zip all files under a certain root directory. (choose to compress or not param compress) * * @param rootFile * @param targetZipFile * @param compress to compress or just store (if already compressed) * @return true = success, false = exception/error */ public static boolean zipAll(File rootFile, File targetZipFile, boolean compress) { Set<String> fileSet = new HashSet<String>(); String[] files = rootFile.list(); for (int i = 0; i < files.length; i++) { fileSet.add(files[i]); } return zip(fileSet, rootFile, targetZipFile, compress); } /** * Add the content of a directory to a zip stream. * * @param path * @param dirName * @param zout */ public static void addDirectoryToZip(final Path path, final String baseDirName, final ZipOutputStream zout) { try { Files.walkFileTree(path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if(!attrs.isDirectory()) { Path relativeFile = path.relativize(file); String names = baseDirName + "/" + relativeFile.toString(); zout.putNextEntry(new ZipEntry(names)); try(InputStream in=Files.newInputStream(file)) { FileUtils.copy(in, zout); } catch (Exception e) { log.error("", e); } zout.closeEntry(); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { log.error("", e); } } /** * Add a file to a zip stream. * @param path * @param file * @param exportStream */ public static void addFileToZip(String path, File file, ZipOutputStream exportStream) { try(InputStream source = new FileInputStream(file)) { exportStream.putNextEntry(new ZipEntry(path)); FileUtils.copy(source, exportStream); exportStream.closeEntry(); } catch(IOException e) { log.error("", e); } } public static void addFileToZip(String path, Path file, ZipOutputStream exportStream) { try(InputStream source = Files.newInputStream(file)) { exportStream.putNextEntry(new ZipEntry(path)); FileUtils.copy(source, exportStream); exportStream.closeEntry(); } catch(IOException e) { log.error("", e); } } /** * Zip all files under a certain root directory. (with compression) * * @param rootFile * @param targetZipFile * @return true = success, false = exception/error */ public static boolean zipAll(File rootFile, File targetZipFile) { return zipAll(rootFile, targetZipFile, true); } /** * Unzip files from VFSLeaf into VFSContainer and do NOTHING ELSE!!! * See OLAT-6213 * * @param src, VFSLeaf input data * @param target, outout VFSContainer */ public static boolean xxunzip (VFSLeaf src, VFSContainer dst) { if (dst instanceof LocalImpl) { try { xxunzip (src.getInputStream(), ((LocalImpl)dst).getBasefile().getAbsolutePath()); return true; } catch (IOException e) { String s = ((LocalImpl)src).getBasefile().getAbsolutePath(); String d = ((LocalImpl)dst).getBasefile().getAbsolutePath(); log.error("I/O error unzipping "+s+" to "+d); return false; } } return false; } /** * Unzip files from stream into target dir and do NOTHING ELSE!!! * See OLAT-6213 * * @param is, stream from zip archive * @param outdir, path to output directory, relative to cwd or absolute */ private static void xxunzip (InputStream is, String outdir) throws IOException { byte[] buffer = new byte[FileUtils.BSIZE]; ZipInputStream zis = new ZipInputStream (new BufferedInputStream(is)); ZipEntry entry; try { while ((entry = zis.getNextEntry()) != null) { File of = new File(outdir, entry.getName()); if (entry.isDirectory()) { of.mkdirs(); continue; } else { File xx = new File (of.getParent()); if (!xx.exists()) { Stack<String> todo = new Stack<String>(); do { todo.push (xx.getAbsolutePath()); xx = new File (xx.getParent()); } while (!xx.exists()); while(todo.size()>0) { xx = new File (todo.pop()); if (!xx.exists()) { xx.mkdirs(); } } } } BufferedOutputStream bos = new BufferedOutputStream ( new FileOutputStream(of), buffer.length ); FileUtils.cpio(new BufferedInputStream(zis), bos, "unzip:"+entry.getName()); bos.flush(); bos.close(); } } catch (IllegalArgumentException e) { //problem with chars in entry name likely } zis.close(); } }