/** * Copyright (c) 2011 Cloudsmith Inc. and other contributors, as listed below. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Cloudsmith * */ package org.cloudsmith.geppetto.forge.util; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarConstants; import org.cloudsmith.geppetto.common.os.FileUtils; import org.cloudsmith.geppetto.common.os.OsUtil; import org.cloudsmith.geppetto.common.os.StreamUtil; public class TarUtils { /** * An interface that can be used when individual files are to be extracted from an archive, possibly * without storing them to disk. */ public interface FileCatcher { /** * Implementors should return <tt>true</tt> or <tt>false</tt> to indicate if this * file is of interest or not. * * @param fileName * The name of the file as it occurs in the archive. * @return a flag indicating if the file is accepted */ boolean accept(String fileName); /** * This method will be called for files that accepted * * @param fileName * The name of the accepted file * @param fileData * A stream from which the file data can be read. * @return <tt>false</tt> to indicate that processing should continue or <tt>true</tt> to indicate that further processing is of no interest * (i.e. the read terminates here). */ boolean catchData(String fileName, InputStream fileData); } private static final int MAX_FILES_PER_COMMAND = 20; private static void append(File file, FileFilter filter, int baseNameLen, String addedTopFolder, TarArchiveOutputStream tarOut) throws IOException { String name = file.getAbsolutePath(); if(name.length() <= baseNameLen) name = ""; else name = name.substring(baseNameLen); if(File.separatorChar == '\\') name = name.replace('\\', '/'); if(addedTopFolder != null) name = addedTopFolder + '/' + name; if(FileUtils.isSymlink(file)) { String linkTarget = FileUtils.readSymbolicLink(file); if(linkTarget != null) { TarArchiveEntry entry = new TarArchiveEntry(name, TarConstants.LF_SYMLINK); entry.setName(name); entry.setLinkName(linkTarget); tarOut.putArchiveEntry(entry); } return; } ArchiveEntry entry = tarOut.createArchiveEntry(file, name); tarOut.putArchiveEntry(entry); File[] children = file.listFiles(filter); if(children != null) { tarOut.closeArchiveEntry(); // This is a directory. Append its children for(File child : children) append(child, filter, baseNameLen, addedTopFolder, tarOut); return; } // Append the content of the file InputStream input = new FileInputStream(file); try { StreamUtil.copy(input, tarOut); tarOut.closeArchiveEntry(); } finally { StreamUtil.close(input); } } private static void chmod(Map<File, Map<Integer, List<String>>> chmodMap) throws IOException { for(Map.Entry<File, Map<Integer, List<String>>> entry : chmodMap.entrySet()) for(Map.Entry<Integer, List<String>> dirEntry : entry.getValue().entrySet()) for(List<String> files : splitList(dirEntry.getValue(), MAX_FILES_PER_COMMAND)) OsUtil.chmod(entry.getKey(), dirEntry.getKey().intValue(), files.toArray(new String[files.size()])); } private static <T> List<String> getFileList(Map<File, Map<T, List<String>>> map, File dir, T key) { Map<T, List<String>> dirMap = map.get(dir); if(dirMap == null) map.put(dir, dirMap = new HashMap<T, List<String>>()); List<String> files = dirMap.get(key); if(files == null) dirMap.put(key, files = new ArrayList<String>()); return files; } public static void pack(File sourceFolder, OutputStream output, boolean includeTopFolder) throws IOException { pack(sourceFolder, output, null, includeTopFolder, null); } public static void pack(File sourceFolder, OutputStream output, FileFilter filter, boolean includeTopFolder, String addedTopFolder) throws IOException { TarArchiveOutputStream tarOut = new TarArchiveOutputStream(output); tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); String absName = sourceFolder.getAbsolutePath(); int baseNameLen = absName.length() + 1; if(includeTopFolder) baseNameLen -= (sourceFolder.getName().length() + 1); try { append(sourceFolder, filter, baseNameLen, addedTopFolder, tarOut); } finally { StreamUtil.close(tarOut); } } private static void registerChmodFile(Map<File, Map<Integer, List<String>>> chmodMap, File dir, Integer mode, String file) { getFileList(chmodMap, dir, mode).add(file); } private static List<List<String>> splitList(List<String> files, int limit) { List<List<String>> result = new ArrayList<List<String>>(); int top = files.size(); int start = 0; while(start < top) { int max = Math.min(limit, top - start); result.add(files.subList(start, start + max)); start += max; } return result; } /** * Unpack the content read from <i>source</i> into <i>targetFolder</i>. If the * <i>skipTopFolder</i> is set, then don't assume that the archive contains one * single folder and unpack the content of that folder, not including the folder * itself. * * @param source * The input source. Must be in <i>TAR</i> format. * @param targetFolder * The destination folder for the unpack. Not used when a <tt>fileCatcher</tt> is provided * @param skipTopFolder * Set to <code>true</code> to unpack beneath the top folder * of the archive. The archive must consist of one single folder and nothing else * in order for this to work. * @param fileCatcher * Used when specific files should be picked from the archive without writing them to disk. Can be <tt>null</tt>. * @throws IOException */ public static void unpack(InputStream source, File targetFolder, boolean skipTopFolder, FileCatcher fileCatcher) throws IOException { String topFolderName = null; Map<File, Map<Integer, List<String>>> chmodMap = new HashMap<File, Map<Integer, List<String>>>(); TarArchiveInputStream in = new TarArchiveInputStream(source); try { TarArchiveEntry te = in.getNextTarEntry(); if(te == null) { throw new IOException("No entry in the tar file"); } do { if(te.isGlobalPaxHeader()) continue; String name = te.getName(); if(skipTopFolder) { int firstSlash = name.indexOf('/'); if(firstSlash < 0) throw new IOException("Archive doesn't contain one single folder"); String tfName = name.substring(0, firstSlash); if(topFolderName == null) topFolderName = tfName; else if(!tfName.equals(topFolderName)) throw new IOException("Archive doesn't contain one single folder"); name = name.substring(firstSlash + 1); } if(name.length() == 0) continue; String linkName = te.getLinkName(); if(linkName != null) { if(linkName.trim().equals("")) linkName = null; } if(fileCatcher != null) { if(linkName == null && !te.isDirectory() && fileCatcher.accept(name)) { if(fileCatcher.catchData(name, in)) // We're done here return; } continue; } File outFile = new File(targetFolder, name); if(linkName != null) { if(!OsUtil.link(targetFolder, name, te.getLinkName())) throw new IOException("Archive contains links but they are not supported on this platform"); } else { if(te.isDirectory()) { outFile.mkdirs(); } else { outFile.getParentFile().mkdirs(); OutputStream target = new FileOutputStream(outFile); StreamUtil.copy(in, target); target.close(); outFile.setLastModified(te.getModTime().getTime()); } registerChmodFile(chmodMap, targetFolder, Integer.valueOf(te.getMode()), name); } } while((te = in.getNextTarEntry()) != null); } finally { StreamUtil.close(in); } chmod(chmodMap); } }