java – SevenZipCompressor code that uses Commons-Compress to compress and uncompress 7zip archives


I wrote a Compressor utility class to generate 7zip files containing everything in a specific directory. The problem is that it’s an order of magnitude slower than both archiving the directory using the native Windows 7zip.exe AND archiving to a .zip file using a ZipCompressor that uses the same algorithm but with the default Java zip classes.

/*
 * SevenZipCompressor.java
 *
 * Date 20/08/2020
 *
 * Copyright Ikan Software N.V. 2003 - 2020, All Rights Reserved.<br><br>
 *
 * This software is the proprietary information of Ikan Software N.V. .
 * Use is subject to license terms.
 */
package lib.util.compressors.sevenzip;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import lib.util.compressors.Compressor;
import lib.util.compressors.CompressorException;
import lib.util.compressors.Entry;

import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * The SevenZipCompressor class supplies a simple way of writing 7zip files.
 * 
 * @author nak
 */
public class SevenZipCompressor extends Compressor {
    
    /**
     * @see lib.util.compressors.Compressor#compress
     */
    public void compress(String fileName, String dirName) throws CompressorException {

       
            // Zip the directory
            File sevenZipFile = new File(fileName);
            try (SevenZOutputFile out = new SevenZOutputFile(sevenZipFile)){
                // loop over all files and add them plus their content recursively to the 7zip archive
                // Zip the directory
                File dir = new File(dirName);
                compress(sevenZipFile, out, dir, dir);

            } catch (IOException e) {
                throw new CompressorException(e);
            }        
    }
    
    

    /**
     * @see lib.util.compressors.Compressor#uncompress
     */
    public void uncompress(String fileName, String dirName) throws CompressorException {
        // Open the zipfile
        try(SevenZFile zipFile = new SevenZFile(new File(fileName));) {

            
            // Get the size of each entry
            Map<String, Integer> zipEntrySizes = new HashMap<String, Integer> ();
            Iterable<SevenZArchiveEntry> e = zipFile.getEntries();
            for(Iterator<SevenZArchiveEntry> iterator = e.iterator(); iterator.hasNext();) {
                SevenZArchiveEntry zipEntry = (SevenZArchiveEntry) iterator.next();
                zipEntrySizes.put(zipEntry.getName(), Integer.valueOf((int) zipEntry.getSize()));
            }


            // Start reading zipentries
            SevenZArchiveEntry zipEntry = null;
            while ((zipEntry = zipFile.getNextEntry()) != null) {
                
                // Zipentry is a file
                if (!zipEntry.isDirectory()) {

                    // Get the size
                    int size = (int) zipEntry.getSize();
                    if (size == -1) {
                        size = ((Integer) zipEntrySizes.get(zipEntry.getName())).intValue();
                    }

                    // Get the content
                    byte() buffer = new byte(size);
                    int bytesInBuffer = 0;
                    int bytesRead = 0;
                    while (((int) size - bytesInBuffer) > 0) {
                        bytesRead = zipFile.read(buffer, bytesInBuffer, size - bytesInBuffer);
                        if (bytesRead == -1) {
                            break;
                        }
                        bytesInBuffer += bytesRead;
                    }

                    String zipEntryName = zipEntry.getName();
                    // replace all "" with "/"
                    zipEntryName = zipEntryName.replace('\', '/');

                    // Get the full path name
                    File file = new File(dirName, zipEntryName);

                    // Create the parent directory
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }

                    // Save file
                    FileOutputStream fos = new FileOutputStream(file.getPath());
                    fos.write(buffer, 0, bytesInBuffer);
                    fos.close();

                    // Set modification date to the date in the zipEntry
                    file.setLastModified(zipEntry.getLastModifiedDate().getTime());
                }
                // Zipentry is a directory
                else {

                    String zipEntryName = zipEntry.getName();
                    // replace all "" with "/"
                    zipEntryName = zipEntryName.replace('\', '/');

                    // Create the directory
                    File dir = new File(dirName, zipEntryName);
                    dir.setLastModified(zipEntry.getLastModifiedDate().getTime());
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                }
            }
        } catch (IOException ioe) {
            throw new CompressorException(ioe);
        }
    }

    /**
     * @see lib.util.compressors.Compressor#getEntries
     */
    public List<Entry> getEntries(String fileName, boolean calculateCrc) throws CompressorException {

        // List to return all entries
        List<Entry> entries = new ArrayList<Entry>();
        
        try {

            // Open the zipfile
            SevenZFile zipFile = new SevenZFile(new File(fileName));

            // Get the size of each entry
            Iterable<SevenZArchiveEntry> iterable = zipFile.getEntries();
            for(Iterator<SevenZArchiveEntry> iterator = iterable.iterator(); iterator.hasNext();) {
                SevenZArchiveEntry zipEntry = iterator.next();
                Entry entry = new Entry();
                entry.setName(zipEntry.getName());
                if (calculateCrc) {
                    entry.setCrc(zipEntry.getCrcValue());
                } else {
                    entry.setCrc(-1);
                }
                entry.setDirectory(zipEntry.isDirectory());
                // 7z is a very Windows specific format, using NTFSTimestamps instead of Java time.
                entry.setTime(zipEntry.getLastModifiedDate().getTime());
                entry.setSize(zipEntry.getSize());
                entries.add(entry);
            }

            // Close zipFile
            zipFile.close();
        
            // Sort entries by ascending name
            sortEntries(entries);
            
            // Return entries
            return entries;
            
        } catch (IOException ioe) {
            throw new CompressorException(ioe);
        }
    }
    
    /**
     * Add a new entry to the zip file.
     * 
     * @param zos the output stream filter for writing files in the ZIP file format
     * @param name the name of the entry.
     * @param lastModified the modification date 
     * @param buffer an array of bytes
     * @throws IOException
     */
    private void addEntry(SevenZOutputFile zos, String name, File inputFile, byte() buffer) throws IOException {
        SevenZArchiveEntry zipEntry = zos.createArchiveEntry(inputFile, name);
        if (buffer != null) {
            zipEntry.setSize(buffer.length);
        } 
        zipEntry.setLastModifiedDate(new Date(inputFile.lastModified()));
        zos.putArchiveEntry(zipEntry);
        if (buffer != null) {
            zos.write(buffer);
        }
        zos.closeArchiveEntry();
    }

    /**
     * Zip the files of the given directory.
     * 
     * @param zipFile the File which is used to store the compressed data
     * @param zos the output stream filter for writing files in the ZIP file format
     * @param dir the directory to zip
     * @param relativeDir the name of each zip entry will be relative to this directory
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void compress(File zipFile, SevenZOutputFile zos, File dir, File relativeDir) throws FileNotFoundException, IOException {

        // Create an array of File objects
        File() fileList = dir.listFiles();

        // Directory is not empty
        if (fileList.length != 0) {

            // Loop through File array
            for (int i = 0; i < fileList.length; i++) {

                // The zipfile itself may not be added
                if (!zipFile.equals(fileList(i))) {
                    // Directory
                    if (fileList(i).isDirectory()) {
                        compress(zipFile, zos, fileList(i), relativeDir);
                    }
                    // File
                    else {
                        byte() buffer = getFileContents(fileList(i));
                        if (buffer != null) {
                            // Get the path names
                            String filePath = fileList(i).getPath();
                            String relativeDirPath = relativeDir.getPath();

                            // Convert the absolute path name to a relative path name
                            if (filePath.startsWith(relativeDirPath)) {
                                filePath = filePath.substring(relativeDirPath.length());
                                if (filePath.startsWith("/") || filePath.startsWith("\")) {
                                    if (filePath.length() == 1) {
                                        filePath = "";
                                    } else {
                                        filePath = filePath.substring(1);
                                    }
                                }
                            }

                            // Add the entry
                            addEntry(zos, filePath, fileList(i), buffer);
                        }
                    }
                }
            }
        }
        // Directory is empty
        else {
            // Get the path names
            String filePath = dir.getPath();
            String relativeDirPath = relativeDir.getPath();

            // Convert the absolute path name to a relative path name
            if (filePath.startsWith(relativeDirPath)) {
                filePath = filePath.substring(relativeDirPath.length());
                if (filePath.startsWith("/") || filePath.startsWith("\")) {
                    if (filePath.length() == 1) {
                        filePath = "";
                    } else {
                        filePath = filePath.substring(1);
                    }
                }
            }

            // Add the entry
            if (!filePath.endsWith("\") && !filePath.endsWith("/")) {
                addEntry(zos, filePath + "/", dir, null);
            }
            else {
                addEntry(zos, filePath, dir, null);
            }
        }
    }

    /**
     * Read the contents of a file for zipping.
     * 
     * @param file the File to read
     * @return an array of bytes
     * @throws FileNotFoundException
     * @throws IOException
     */
    private byte() getFileContents(File file) throws FileNotFoundException, IOException {

        FileInputStream fis = new FileInputStream(file);
        long len = file.length();
        byte() buffer = new byte((int) len);
        fis.read(buffer);
        fis.close();
        return buffer;
    }
}

I doubt that Commons-Compress is so significantly slower than just the native 7zip.exe written by Igor Pavlov when used properly, so any feedback on how to speed this up is appreciated. For reference: a directory with 900 MB of data that would take roughly 45 seconds to compress using 7zip.exe takes over 5 minutes with this code.