/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.archiver; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileAttributes; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.io.BufferedRandomOutputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import org.apache.tools.bzip2.CBZip2OutputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.GZIPOutputStream; /** * Archiver is an abstract class that represents a generic file archiver and abstracts the underlying * compression method and specifics of the format. * * <p>Subclasses implement specific archive formats (Zip, Tar...) but cannot be instantiated directly. * Instead, the <code>getArchiver</code> methods can be used to retrieve an Archiver * instance for a specified archive format. A list of available archive formats can be dynamically retrieved * using {@link #getFormats(boolean) getFormats}. * * <p>Archive formats fall into 2 categories: * <ul> * <li><i>Single entry formats:</i> Formats that can only store one entry without any directory structure, e.g. Gzip or Bzip2. * <li><i>Many entries formats:</i> Formats that can store multiple entries along with a directory structure, e.g. Zip or Tar. * </ul> * * @author Maxence Bernard */ public abstract class Archiver { /** Zip archive format (many entries format) */ public final static int ZIP_FORMAT = 0; /** Gzip archive format (single entry format) */ public final static int GZ_FORMAT = 1; /** Bzip2 archive format (single entry format) */ public final static int BZ2_FORMAT = 2; /** Tar archive format without any compression (many entries format) */ public final static int TAR_FORMAT = 3; /** Tar archive compressed with Gzip format (many entries format) */ public final static int TAR_GZ_FORMAT = 4; /** Tar archive compressed with Bzip2 format (many entries format) */ public final static int TAR_BZ2_FORMAT = 5; /** Boolean array describing for each format if it can store more than one entry */ private final static boolean SUPPORTS_MANY_ENTRIES[] = { true, false, false, true, true, true }; /** Array of single entry formats: many entries formats are considered to be single entry formats as well */ private final static int SINGLE_ENTRY_FORMATS[] = { ZIP_FORMAT, GZ_FORMAT, BZ2_FORMAT, TAR_FORMAT, TAR_GZ_FORMAT, TAR_BZ2_FORMAT }; /** Array of many entries formats */ private final static int MANY_ENTRIES_FORMATS[] = { ZIP_FORMAT, TAR_FORMAT, TAR_GZ_FORMAT, TAR_BZ2_FORMAT }; /** Array of format names */ private final static String FORMAT_NAMES[] = { "Zip", "Gzip", "Bzip2", "Tar", "Tar/Gzip", "Tar/Bzip2" }; /** Array of format extensions */ private final static String FORMAT_EXTENSIONS[] = { "zip", "gz", "bz2", "tar", "tar.gz", "tar.bz2" }; /** The underlying stream this archiver is writing to */ protected OutputStream out; /** Archive format of this Archiver */ protected int format; /** Archive format's name of this Archiver */ protected String formatName; /** * Creates a new Archiver. * * @param out the OutputStream this Archiver will write to */ Archiver(OutputStream out) { this.out = out; } /** * Returns the <code>OutputStream</code> this Archiver is writing to. * * @return the OutputStream this Archiver is writing to */ public OutputStream getOutputStream() { return out; } /** * Returns the archiver format used by this Archiver. See format constants. */ public int getFormat() { return this.format; } /** * Sets the archiver format used by this Archiver, for internal use only. */ private void setFormat(int format) { this.format = format; } /** * Returns the name of the archive format used by this Archiver. */ public String getFormatName() { return FORMAT_NAMES[this.format]; } /** * Returns true if the format used by this Archiver can store more than one entry. */ public boolean supportsManyFiles() { return formatSupportsManyFiles(this.format); } /** * Returns true if the format used by this Archiver can store an optional comment. */ public boolean supportsComment() { return formatSupportsComment(this.format); } /** * Sets an optional comment in the archive, the {@link #supportsComment()} or * {@link #formatSupportsComment(int)} must first be called to make sure * the archive format supports comment, otherwise calling this method will have no effect. * * <p>Implementation note: Archiver implementations must override this method to handle comments * * @param comment the comment to be stored in the archive */ public void setComment(String comment) { // No-op } /** * Normalizes the entry path, that is : * <ul> * <li>replace any \ character occurrence by / as this usually is the default separator for archive files * <li>if the entry is a directory, add a trailing slash to the path if it doesn't have one already * </ul> */ protected String normalizePath(String entryPath, boolean isDirectory) { // Replace any \ character by / entryPath = entryPath.replace('\\', '/'); // If entry is a directory, make sure the path contains a trailing / if(isDirectory && !entryPath.endsWith("/")) entryPath += "/"; return entryPath; } //////////////////// // Static methods // //////////////////// /** * Returns an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to. * <code>null</code> is returned if the specified format is not valid. * * <p>This method will first attempt to get a {@link RandomAccessOutputStream} if the given file is able to supply * one, and if not, fall back to a regular <code>OutputStream</code>. Note that if the file exists, its contents * will be overwritten. Write bufferring is used under the hood to improve performance.</p> * * @param file the AbstractFile which the returned Archiver will write entries to * @param format an archive format * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ; * null if the specified format is not valid. * @throws IOException if the file cannot be opened for write, or if an error occurred while intializing the archiver * @throws UnsupportedFileOperationException if the underlying filesystem does not support write operations */ public static Archiver getArchiver(AbstractFile file, int format) throws IOException, UnsupportedFileOperationException { OutputStream out = null; if(file.isFileOperationSupported(FileOperation.RANDOM_WRITE_FILE)) { try { // Important: if the file exists, it has to be overwritten as AbstractFile#getRandomAccessOutputStream() // does NOT overwrite the file. This fixes bug #30. if(file.exists()) file.delete(); out = new BufferedRandomOutputStream(file.getRandomAccessOutputStream()); } catch(IOException e) { // Fall back to a regular OutputStream } } if(out==null) out = new BufferedOutputStream(file.getOutputStream()); return getArchiver(out, format); } /** * Returns an Archiver for the specified format and that uses the given <code>OutputStream</code> to write entries to. * <code>null</code> is returned if the specified format is not valid. Whenever possible, a * {@link RandomAccessOutputStream} should be supplied as some formats take advantage of having a random write access. * * @param out the OutputStream which the returned Archiver will write entries to * @param format an archive format * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ; * null if the specified format is not valid. * @throws IOException if an error occurred while intializing the archiver */ public static Archiver getArchiver(OutputStream out, int format) throws IOException { Archiver archiver; switch(format) { case ZIP_FORMAT: archiver = new ZipArchiver(out); break; case GZ_FORMAT: archiver = new SingleFileArchiver(new GZIPOutputStream(out)); break; case BZ2_FORMAT: archiver = new SingleFileArchiver(createBzip2OutputStream(out)); break; case TAR_FORMAT: archiver = new TarArchiver(out); break; case TAR_GZ_FORMAT: archiver = new TarArchiver(new GZIPOutputStream(out)); break; case TAR_BZ2_FORMAT: archiver = new TarArchiver(createBzip2OutputStream(out)); break; default: return null; } archiver.setFormat(format); return archiver; } /** * Creates and returns a Bzip2 <code>OutputStream</code> using the given <code>OutputStream</code> as the underlying * stream. * * @param out the underlying stream * @return a Bzip2 OutputStream * @throws IOException if an error occurred while initializing the Bzip2 OutputStream */ protected static OutputStream createBzip2OutputStream(OutputStream out) throws IOException { // Writes the 2 magic bytes 'BZ', as required by CBZip2OutputStream. A quote from CBZip2OutputStream's Javadoc: // "Attention: The caller is resonsible to write the two BZip2 magic bytes "BZ" to the specified stream // prior to calling this constructor." out.write('B'); out.write('Z'); return new CBZip2OutputStream(out); } /** * Returns an array of available archive formats, single entry formats or many entries formats * depending on the value of the specified boolean parameter. * * @param manyEntries if true, a list of many entries formats (a subset of single entry formats) will be returned */ public static int[] getFormats(boolean manyEntries) { return manyEntries? MANY_ENTRIES_FORMATS : SINGLE_ENTRY_FORMATS; } /** * Returns the name of the given archive format. The returned name can be used for display in a GUI. * * @param format an archive format */ public static String getFormatName(int format) { return FORMAT_NAMES[format]; } /** * Returns the default archive format extension. Note: some formats such as Tar/Gzip have several common * extensions (e.g. tar.gz or tgz), the most common one will be returned. * * @param format an archive format */ public static String getFormatExtension(int format) { return FORMAT_EXTENSIONS[format]; } /** * Returns true if the specified archive format supports storage of more than one entry. * * @param format an archive format */ public static boolean formatSupportsManyFiles(int format) { return SUPPORTS_MANY_ENTRIES[format]; } /** * Returns true if the specified archive format can store an optional comment. * * @param format an archive format */ public static boolean formatSupportsComment(int format) { return format==ZIP_FORMAT; } ////////////////////// // Abstract methods // ////////////////////// /** * Creates a new entry in the archive using the given relative path and file attributes, and returns an * <code>OutputStream</code> to write the entry's contents. The specified file attributes are used to determine * whether the entry is a directory or a regular file, and to set the entry's size, permissions and date. * * <p>If the entry is a regular file (not a directory), an OutputStream which can be used to write the contents * of the entry will be returned, <code>null</code> otherwise. The OutputStream <b>must not</b> be closed once * it has been used (Archiver takes care of this), only the {@link #close() close} method has to be called when * all entries have been created.</p> * * <p>If this Archiver uses a single entry format, the specified path and file won't be used at all. * Also in this case, this method must be invoked only once (single entry), it will throw an IOException * if invoked more than once.</p> * * @param entryPath the path to be used to create the entry in the archive. This parameter is simply ignored if the * archive is a single entry format. * @param attributes used to determine whether the entry is a directory or regular file, and to retrieve its * date and size * @return <code>OutputStream</code> to write the entry's contents. * @throws IOException if this Archiver failed to write the entry, or in the case of a single entry archiver, if * this method was called more than once. */ public abstract OutputStream createEntry(String entryPath, FileAttributes attributes) throws IOException; /** * Closes the underlying OuputStream and ressources used by this Archiver to write the archive. This method * must be called when all entries have been added to the archive. */ public abstract void close() throws IOException; }