/* ** Authored by Timothy Gerard Endres ** <mailto:time@gjt.org> <http://www.trustice.com> ** ** This work has been placed into the public domain. ** You may use this work in any way and for any purpose you wish. ** ** THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, ** NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR ** OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY ** CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR ** REDISTRIBUTION OF THIS SOFTWARE. ** */ package org.jboss.shrinkwrap.impl.base.io.tar; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import javax.activation.FileTypeMap; import javax.activation.MimeType; import javax.activation.MimeTypeParseException; /** * The TarArchive class implements the concept of a tar archive. A tar archive is a series of entries, each of which * represents a file system object. Each entry in the archive consists of a header record. Directory entries consist * only of the header record, and are followed by entries for the directory's contents. File entries consist of a header * record followed by the number of records needed to contain the file's contents. All entries are written on record * boundaries. Records are 512 bytes long. * * TarArchives are instantiated in either read or write mode, based upon whether they are instantiated with an * InputStream or an OutputStream. Once instantiated TarArchives read/write mode can not be changed. * * There is currently no support for random access to tar archives. However, it seems that subclassing TarArchive, and * using the TarBuffer.getCurrentRecordNum() and TarBuffer.getCurrentBlockNum() methods, this would be rather trvial. * * @version $Revision: 1.15 $ * @author Timothy Gerard Endres, <time@gjt.org> * @see TarBuffer * @see TarHeader * @see TarEntry */ public class TarArchive extends Object { protected boolean verbose; protected boolean debug; protected boolean keepOldFiles; protected boolean asciiTranslate; protected int userId; protected String userName; protected int groupId; protected String groupName; protected String rootPath; protected String tempPath; protected String pathPrefix; protected int recordSize; protected byte[] recordBuf; protected TarInputStream tarIn; protected TarOutputStreamImpl tarOut; protected TarTransFileTyper transTyper; protected TarProgressDisplay progressDisplay; /** * The InputStream based constructors create a TarArchive for the purposes of e'x'tracting or lis't'ing a tar * archive. Thus, use these constructors when you wish to extract files from or list the contents of an existing tar * archive. */ public TarArchive(InputStream inStream) { this(inStream, TarBuffer.DEFAULT_BLKSIZE); } public TarArchive(InputStream inStream, int blockSize) { this(inStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); } public TarArchive(InputStream inStream, int blockSize, int recordSize) { this.tarIn = new TarInputStream(inStream, blockSize, recordSize); this.initialize(recordSize); } /** * The OutputStream based constructors create a TarArchive for the purposes of 'c'reating a tar archive. Thus, use * these constructors when you wish to create a new tar archive and write files into it. */ public TarArchive(OutputStream outStream) { this(outStream, TarBuffer.DEFAULT_BLKSIZE); } public TarArchive(OutputStream outStream, int blockSize) { this(outStream, blockSize, TarBuffer.DEFAULT_RCDSIZE); } public TarArchive(OutputStream outStream, int blockSize, int recordSize) { this.tarOut = new TarOutputStreamImpl(outStream, blockSize, recordSize); this.initialize(recordSize); } /** * Common constructor initialization code. */ private void initialize(int recordSize) { this.rootPath = null; this.pathPrefix = null; this.tempPath = System.getProperty("user.dir"); this.userId = 0; this.userName = ""; this.groupId = 0; this.groupName = ""; this.debug = false; this.verbose = false; this.keepOldFiles = false; this.progressDisplay = null; this.recordBuf = new byte[this.getRecordSize()]; } /** * Set the debugging flag. * * @param debugF * The new debug setting. */ public void setDebug(boolean debugF) { this.debug = debugF; if (this.tarIn != null) { this.tarIn.setDebug(debugF); } else if (this.tarOut != null) { this.tarOut.setDebug(debugF); } } /** * Returns the verbosity setting. * * @return The current verbosity setting. */ public boolean isVerbose() { return this.verbose; } /** * Set the verbosity flag. * * @param verbose * The new verbosity setting. */ public void setVerbose(boolean verbose) { this.verbose = verbose; } /** * Set the current progress display interface. This allows the programmer to use a custom class to display the * progress of the archive's processing. * * @param display * The new progress display interface. * @see TarProgressDisplay */ public void setTarProgressDisplay(TarProgressDisplay display) { this.progressDisplay = display; } /** * Set the flag that determines whether existing files are kept, or overwritten during extraction. * * @param keepOldFiles * If true, do not overwrite existing files. */ public void setKeepOldFiles(boolean keepOldFiles) { this.keepOldFiles = keepOldFiles; } /** * Set the ascii file translation flag. If ascii file translatio is true, then the MIME file type will be consulted * to determine if the file is of type 'text/*'. If the MIME type is not found, then the TransFileTyper is consulted * if it is not null. If either of these two checks indicates the file is an ascii text file, it will be translated. * The translation converts the local operating system's concept of line ends into the UNIX line end, '\n', which is * the defacto standard for a TAR archive. This makes text files compatible with UNIX, and since most tar * implementations for other platforms, compatible with most other platforms. * * @param asciiTranslate * If true, translate ascii text files. */ public void setAsciiTranslation(boolean asciiTranslate) { this.asciiTranslate = asciiTranslate; } /** * Set the object that will determine if a file is of type ascii text for translation purposes. * * @param transTyper * The new TransFileTyper object. */ public void setTransFileTyper(TarTransFileTyper transTyper) { this.transTyper = transTyper; } /** * Set user and group information that will be used to fill in the tar archive's entry headers. Since Java currently * provides no means of determining a user name, user id, group name, or group id for a given File, TarArchive * allows the programmer to specify values to be used in their place. * * @param userId * The user Id to use in the headers. * @param userName * The user name to use in the headers. * @param groupId * The group id to use in the headers. * @param groupName * The group name to use in the headers. */ public void setUserInfo(int userId, String userName, int groupId, String groupName) { this.userId = userId; this.userName = userName; this.groupId = groupId; this.groupName = groupName; } /** * Get the user id being used for archive entry headers. * * @return The current user id. */ public int getUserId() { return this.userId; } /** * Get the user name being used for archive entry headers. * * @return The current user name. */ public String getUserName() { return this.userName; } /** * Get the group id being used for archive entry headers. * * @return The current group id. */ public int getGroupId() { return this.groupId; } /** * Get the group name being used for archive entry headers. * * @return The current group name. */ public String getGroupName() { return this.groupName; } /** * Get the current temporary directory path. Because Java's File did not support temporary files until version 1.2, * TarArchive manages its own concept of the temporary directory. The temporary directory defaults to the 'user.dir' * System property. * * @return The current temporary directory path. */ public String getTempDirectory() { return this.tempPath; } /** * Set the current temporary directory path. * * @param path * The new temporary directory path. */ public void setTempDirectory(String path) { this.tempPath = path; } /** * Get the archive's record size. Because of its history, tar supports the concept of buffered IO consisting of * BLOCKS of RECORDS. This allowed tar to match the IO characteristics of the physical device being used. Of course, * in the Java world, this makes no sense, WITH ONE EXCEPTION - archives are expected to be propertly "blocked". * Thus, all of the horrible TarBuffer support boils down to simply getting the "boundaries" correct. * * @return The record size this archive is using. */ public int getRecordSize() { if (this.tarIn != null) { return this.tarIn.getRecordSize(); } else if (this.tarOut != null) { return this.tarOut.getRecordSize(); } return TarBuffer.DEFAULT_RCDSIZE; } /** * Get a path for a temporary file for a given File. The temporary file is NOT created. The algorithm attempts to * handle filename collisions so that the name is unique. * * @return The temporary file's path. */ private String getTempFilePath(File eFile) { String pathStr = this.tempPath + File.separator + eFile.getName() + ".tmp"; for (int i = 1; i < 5; ++i) { File f = new File(pathStr); if (!f.exists()) { break; } pathStr = this.tempPath + File.separator + eFile.getName() + "-" + i + ".tmp"; } return pathStr; } /** * Close the archive. This simply calls the underlying tar stream's close() method. */ public void closeArchive() throws IOException { if (this.tarIn != null) { this.tarIn.close(); } else if (this.tarOut != null) { this.tarOut.close(); } } /** * Perform the "list" command and list the contents of the archive. NOTE That this method uses the progress display * to actually list the conents. If the progress display is not set, nothing will be listed! */ public void listContents() throws IOException { for (;;) { TarEntry entry = this.tarIn.getNextEntry(); if (entry == null) { if (this.debug) { System.err.println("READ EOF RECORD"); } break; } if (this.progressDisplay != null) { this.progressDisplay.showTarProgressMessage(entry.getName()); } } } /** * Perform the "extract" command and extract the contents of the archive. * * @param destDir * The destination directory into which to extract. */ public void extractContents(File destDir) throws IOException { for (;;) { TarEntry entry = this.tarIn.getNextEntry(); if (entry == null) { if (this.debug) { System.err.println("READ EOF RECORD"); } break; } this.extractEntry(destDir, entry); } } /** * Extract an entry from the archive. This method assumes that the tarIn stream has been properly set with a call to * getNextEntry(). * * @param destDir * The destination directory into which to extract. * @param entry * The TarEntry returned by tarIn.getNextEntry(). */ private void extractEntry(File destDir, TarEntry entry) throws IOException { if (this.verbose) { if (this.progressDisplay != null) { this.progressDisplay.showTarProgressMessage(entry.getName()); } } String name = entry.getName(); name = name.replace('/', File.separatorChar); File destFile = new File(destDir, name); if (entry.isDirectory()) { if (!destFile.exists()) { if (!destFile.mkdirs()) { throw new IOException("error making directory path '" + destFile.getPath() + "'"); } } } else { File subDir = new File(destFile.getParent()); if (!subDir.exists()) { if (!subDir.mkdirs()) { throw new IOException("error making directory path '" + subDir.getPath() + "'"); } } if (this.keepOldFiles && destFile.exists()) { if (this.verbose) { if (this.progressDisplay != null) { this.progressDisplay.showTarProgressMessage("not overwriting " + entry.getName()); } } } else { boolean asciiTrans = false; FileOutputStream out = new FileOutputStream(destFile); if (this.asciiTranslate) { MimeType mime = null; String contentType = null; try { contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(destFile); mime = new MimeType(contentType); if (mime.getPrimaryType().equalsIgnoreCase("text")) { asciiTrans = true; } else if (this.transTyper != null) { if (this.transTyper.isAsciiFile(entry.getName())) { asciiTrans = true; } } } catch (MimeTypeParseException ex) { } if (this.debug) { System.err.println("EXTRACT TRANS? '" + asciiTrans + "' ContentType='" + contentType + "' PrimaryType='" + mime.getPrimaryType() + "'"); } } PrintWriter outw = null; if (asciiTrans) { outw = new PrintWriter(out); } byte[] rdbuf = new byte[32 * 1024]; for (;;) { int numRead = this.tarIn.read(rdbuf); if (numRead == -1) { break; } if (asciiTrans) { for (int off = 0, b = 0; b < numRead; ++b) { if (rdbuf[b] == 10) { String s = new String(rdbuf, off, (b - off)); outw.println(s); off = b + 1; } } } else { out.write(rdbuf, 0, numRead); } } if (asciiTrans) { outw.close(); } else { out.close(); } } } } /** * Write an entry to the archive. This method will call the putNextEntry() and then write the contents of the entry, * and finally call closeEntry() for entries that are files. For directories, it will call putNextEntry(), and then, * if the recurse flag is true, process each entry that is a child of the directory. * * @param entry * The TarEntry representing the entry to write to the archive. * @param recurse * If true, process the children of directory entries. */ public void writeEntry(TarEntry oldEntry, boolean recurse) throws IOException { boolean asciiTrans = false; boolean unixArchiveFormat = oldEntry.isUnixTarFormat(); File tFile = null; File eFile = oldEntry.getFile(); // Work on a copy of the entry so we can manipulate it. // Note that we must distinguish how the entry was constructed. // TarEntry entry = (TarEntry) oldEntry.clone(); if (this.verbose) { if (this.progressDisplay != null) { this.progressDisplay.showTarProgressMessage(entry.getName()); } } if (this.asciiTranslate && !entry.isDirectory()) { MimeType mime = null; String contentType = null; try { contentType = FileTypeMap.getDefaultFileTypeMap().getContentType(eFile); mime = new MimeType(contentType); if (mime.getPrimaryType().equalsIgnoreCase("text")) { asciiTrans = true; } else if (this.transTyper != null) { if (this.transTyper.isAsciiFile(eFile)) { asciiTrans = true; } } } catch (MimeTypeParseException ex) { // IGNORE THIS ERROR... } if (this.debug) { System.err.println("CREATE TRANS? '" + asciiTrans + "' ContentType='" + contentType + "' PrimaryType='" + mime.getPrimaryType() + "'"); } if (asciiTrans) { String tempFileName = this.getTempFilePath(eFile); tFile = new File(tempFileName); BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(eFile))); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tFile)); for (;;) { String line = in.readLine(); if (line == null) { break; } out.write(line.getBytes()); out.write((byte) '\n'); } in.close(); out.flush(); out.close(); entry.setSize(tFile.length()); eFile = tFile; } } String newName = null; if (this.rootPath != null) { if (entry.getName().startsWith(this.rootPath)) { newName = entry.getName().substring(this.rootPath.length() + 1); } } if (this.pathPrefix != null) { newName = (newName == null) ? this.pathPrefix + "/" + entry.getName() : this.pathPrefix + "/" + newName; } if (newName != null) { entry.setName(newName); } this.tarOut.putNextEntry(entry); if (entry.isDirectory()) { if (recurse) { TarEntry[] list = entry.getDirectoryEntries(); for (int i = 0; i < list.length; ++i) { TarEntry dirEntry = list[i]; if (unixArchiveFormat) { dirEntry.setUnixTarFormat(); } this.writeEntry(dirEntry, recurse); } } } else { FileInputStream in = new FileInputStream(eFile); byte[] eBuf = new byte[32 * 1024]; for (;;) { int numRead = in.read(eBuf, 0, eBuf.length); if (numRead == -1) { break; } this.tarOut.write(eBuf, 0, numRead); } in.close(); if (tFile != null) { tFile.delete(); } this.tarOut.closeEntry(); } } }