Как разделить огромный zip-файл на несколько томов?

Когда я создаю zip-архив через java.util.zip.*, есть ли способ разделить полученный архив на несколько томов?

Допустим, мой общий архив имеет размер filesize из 24 MB, и я хочу разделить его на 3 файла с ограничением по 10 МБ на файл.
Существует ли API-интерфейс zip, поддерживающий эту функцию? Или любые другие хорошие способы добиться этого?

Спасибо, Толлстен.


person Thollsten    schedule 28.10.2008    source источник


Ответы (5)


Проверьте: http://saloon.javaranch.com/cgi-bin/ubb/ultimatebb.cgi?ubb=get_topic&f=38&t=004618

Я не знаю ни одного общедоступного API, который помог бы вам в этом. (Хотя, если вы не хотите делать это программно, есть такие утилиты, как WinSplitter, которые это сделают)

Я не пробовал, но каждый ZipEntry при использовании ZippedInput/OutputStream имеет сжатый размер. Вы можете получить приблизительную оценку размера заархивированного файла во время его создания. Если вам нужно 2 МБ заархивированных файлов, вы можете прекратить запись в файл после того, как совокупный размер записей станет равным 1,9 МБ, при этом для файла манифеста и других элементов, связанных с zip-файлом, потребуется 0,1 МБ. Итак, в в двух словах, вы можете написать оболочку над ZippedInputStream следующим образом:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ChunkedZippedOutputStream {

    private ZipOutputStream zipOutputStream;

    private final String path;
    private final String name;

    private long currentSize;
    private int currentChunkIndex;
    private final long MAX_FILE_SIZE = 16000000; // Whatever size you want
    private final String PART_POSTFIX = ".part.";
    private final String FILE_EXTENSION = ".zip";

    public ChunkedZippedOutputStream(String path, String name) throws FileNotFoundException {
        this.path = path;
        this.name = name;
        constructNewStream();
    }

    public void addEntry(ZipEntry entry) throws IOException {
        long entrySize = entry.getCompressedSize();
        if ((currentSize + entrySize) > MAX_FILE_SIZE) {
            closeStream();
            constructNewStream();
        } else {
            currentSize += entrySize;
            zipOutputStream.putNextEntry(entry);
        }
    }

    private void closeStream() throws IOException {
        zipOutputStream.close();
    }

    private void constructNewStream() throws FileNotFoundException {
        zipOutputStream = new ZipOutputStream(new FileOutputStream(new File(path, constructCurrentPartName())));
        currentChunkIndex++;
        currentSize = 0;
    }

    private String constructCurrentPartName() {
        // This will give names is the form of <file_name>.part.0.zip, <file_name>.part.1.zip, etc.
        return name + PART_POSTFIX + currentChunkIndex + FILE_EXTENSION;
    }
}

Приведенная выше программа — это всего лишь намек на подход, а не окончательное решение ни в коем случае.

person sakana    schedule 28.10.2008
comment
Это всего лишь несколько отдельных zip-файлов, верно? Это не один многотомный zip-файл. - person mcv; 31.03.2011
comment
Мы получили этот код, работающий в ответах на этот вопрос - stackoverflow.com/questions/11104889/ - person matt freake; 02.07.2012

Если цель состоит в том, чтобы выходные данные были совместимы с pkzip и winzip, я не знаю ни одной библиотеки с открытым исходным кодом, которая делает это. У нас было аналогичное требование для одного из наших приложений, и я написал собственную реализацию (совместимую со стандартом zip). Насколько я помню, самым сложным для нас было то, что нам приходилось генерировать отдельные файлы на лету (способ работы большинства zip-утилит заключается в том, что они создают большой zip-файл, а затем возвращаются и разбивают его позже — это намного проще). Потребовалось около дня на написание и 2 дня на отладку.

Стандарт zip объясняет, как должен выглядеть формат файла. Если вы не боитесь немного закатать рукава, это определенно выполнимо. Вам нужно реализовать генератор zip-файлов самостоятельно, но вы можете использовать класс Java Deflator для создания потоков сегментов для сжатых данных. Вам придется сгенерировать заголовки файлов и разделов самостоятельно, но они всего лишь байты — ничего сложного, как только вы погрузитесь в них.

Вот спецификация почтового индекса — раздел K содержит информацию, которую вы ищете конкретно, но вам также нужно прочитать A, B, C и F. Если вы имеете дело с действительно большими файлами (мы были), вам также придется заняться Zip64, но для 24 МБ все в порядке.

Если вы хотите погрузиться и попробовать - если у вас возникнут вопросы, отправьте ответ, и я посмотрю, смогу ли я дать некоторые указатели.

person Kevin Day    schedule 29.10.2008
comment
У меня проблемы с многотомными zip-файлами. В частности, когда один компонент файла разделен между более чем файлом на диске. В файле.zx01 у меня есть заголовок файла и первая часть сжатых данных, затем в файле.zx02 у меня есть остальные сжатые данные. Но я не могу собрать файлы по какой-то причине, и я не уверен, почему. Есть ли у вас здесь опыт? - person vy32; 26.08.2010

Как бы то ни было, мне нравится везде использовать try-with-resources. Если вам нравится этот шаблон проектирования, то вам это понравится. Кроме того, это решает проблему пустых деталей, если записи больше желаемого размера детали. В худшем случае у вас будет как минимум столько частей, сколько записей.

In:

мой архив.zip

Из:

мой-архив.part1of3.zip
мой-архив.part2of3.zip
мой-архив.part3of3.zip

Примечание. Я использую ведение журнала и Apache Commons FilenameUtils, но не стесняйтесь использовать то, что есть в вашем наборе инструментов.

/**
 * Utility class to split a zip archive into parts (not volumes)
 * by attempting to fit as many entries into a single part before
 * creating a new part. If a part would otherwise be empty because
 * the next entry won't fit, it will be added anyway to avoid empty parts.
 *
 * @author Eric Draken, 2019
 */
public class Zip
{
    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

    private static final String ZIP_PART_FORMAT = "%s.part%dof%d.zip";

    private static final String EXT = "zip";

    private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() );

    /**
     * Split a large archive into smaller parts
     *
     * @param zipFile             Source zip file to split (must end with .zip)
     * @param outZipFile          Destination zip file base path. The "part" number will be added automatically
     * @param approxPartSizeBytes Approximate part size
     * @throws IOException Exceptions on file access
     */
    public static void splitZipArchive(
        @NotNull final File zipFile,
        @NotNull final File outZipFile,
        final long approxPartSizeBytes ) throws IOException
    {
        String basename = FilenameUtils.getBaseName( outZipFile.getName() );
        Path basePath = outZipFile.getParentFile() != null ? // Check if this file has a parent folder
            outZipFile.getParentFile().toPath() :
            Paths.get( "" );
        String extension = FilenameUtils.getExtension( zipFile.getName() );
        if ( !extension.equals( EXT ) )
        {
            throw new IllegalArgumentException( "The archive to split must end with ." + EXT );
        }

        // Get a list of entries in the archive
        try ( ZipFile zf = new ZipFile( zipFile ) )
        {
            // Silliness check
            long minRequiredSize = zipFile.length() / 100;
            if ( minRequiredSize > approxPartSizeBytes )
            {
                throw new IllegalArgumentException(
                    "Please select a minimum part size over " + minRequiredSize + " bytes, " +
                        "otherwise there will be over 100 parts."
                );
            }

            // Loop over all the entries in the large archive
            // to calculate the number of parts required
            Enumeration<? extends ZipEntry> enumeration = zf.entries();
            long partSize = 0;
            long totalParts = 1;
            while ( enumeration.hasMoreElements() )
            {
                long nextSize = enumeration.nextElement().getCompressedSize();
                if ( partSize + nextSize > approxPartSizeBytes )
                {
                    partSize = 0;
                    totalParts++;
                }
                partSize += nextSize;
            }

            // Silliness check: if there are more parts than there
            // are entries, then one entry will occupy one part by contract
            totalParts = Math.min( totalParts, zf.size() );

            logger.debug( "Split requires {} parts", totalParts );
            if ( totalParts == 1 )
            {
                // No splitting required. Copy file
                Path outFile = basePath.resolve(
                    String.format( ZIP_PART_FORMAT, basename, 1, 1 )
                );
                Files.copy( zipFile.toPath(), outFile );
                logger.debug( "Copied {} to {} (pass-though)", zipFile.toString(), outFile.toString() );
                return;
            }

            // Reset
            enumeration = zf.entries();

            // Split into parts
            int currPart = 1;
            ZipEntry overflowZipEntry = null;
            while ( overflowZipEntry != null || enumeration.hasMoreElements() )
            {
                Path outFilePart = basePath.resolve(
                    String.format( ZIP_PART_FORMAT, basename, currPart++, totalParts )
                );
                overflowZipEntry = writeEntriesToPart( overflowZipEntry, zf, outFilePart, enumeration, approxPartSizeBytes );
                logger.debug( "Wrote {}", outFilePart );
            }
        }
    }

    /**
     * Write an entry to the to the outFilePart
     *
     * @param overflowZipEntry    ZipEntry that didn't fit in the last part, or null
     * @param inZipFile           The large archive to split
     * @param outFilePart         The part of the archive currently being worked on
     * @param enumeration         Enumeration of ZipEntries
     * @param approxPartSizeBytes Approximate part size
     * @return Overflow ZipEntry, or null
     * @throws IOException File access exceptions
     */
    private static ZipEntry writeEntriesToPart(
        @Nullable ZipEntry overflowZipEntry,
        @NotNull final ZipFile inZipFile,
        @NotNull final Path outFilePart,
        @NotNull final Enumeration<? extends ZipEntry> enumeration,
        final long approxPartSizeBytes
    ) throws IOException
    {
        try (
            ZipOutputStream zos =
                new ZipOutputStream( new FileOutputStream( outFilePart.toFile(), false ) )
        )
        {
            long partSize = 0;
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            while ( overflowZipEntry != null || enumeration.hasMoreElements() )
            {
                ZipEntry entry = overflowZipEntry != null ? overflowZipEntry : enumeration.nextElement();
                overflowZipEntry = null;

                long entrySize = entry.getCompressedSize();
                if ( partSize + entrySize > approxPartSizeBytes )
                {
                    if ( partSize != 0 )
                    {
                        return entry;    // Finished this part, but return the dangling ZipEntry
                    }
                    // Add the entry anyway if the part would otherwise be empty
                }
                partSize += entrySize;
                zos.putNextEntry( entry );

                // Get the input stream for this entry and copy the entry
                try ( InputStream is = inZipFile.getInputStream( entry ) )
                {
                    int bytesRead;
                    while ( (bytesRead = is.read( buffer )) != -1 )
                    {
                        zos.write( buffer, 0, bytesRead );
                    }
                }
            }
            return null;    // Finished splitting
        }
    }
person Drakes    schedule 29.04.2019

Ниже приведен код моего решения для разделения zip-файла в структуре каталогов на куски в зависимости от желаемого размера. Я нашел предыдущие ответы полезными, поэтому хотел внести свой вклад с аналогичным, но немного более аккуратным подходом. Этот код работает для моих конкретных нужд, и я считаю, что есть возможности для улучшения.

import java.io.*;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

class ChunkedZip {
    private final static long MAX_FILE_SIZE = 1000 * 1000 * 1024; //  around 1GB 
    private final static String zipCopyDest = "C:\\zip2split\\copy";

    public static void splitZip(String zipFileName, String zippedPath, String coreId) throws IOException {

        System.out.println("process whole zip file..");
        FileInputStream fis = new FileInputStream(zippedPath);
        ZipInputStream zipInputStream = new ZipInputStream(fis);
        ZipEntry entry = null;
        int currentChunkIndex = 0;
        //using just to get the uncompressed size of the zipentries
        long entrySize = 0;
        ZipFile zipFile = new ZipFile(zippedPath);
        Enumeration enumeration = zipFile.entries();

        String copDest = zipCopyDest + "\\" + coreId + "_" + currentChunkIndex + ".zip";

        FileOutputStream fos = new FileOutputStream(new File(copDest));
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        ZipOutputStream zos = new ZipOutputStream(bos);
        long currentSize = 0;

        try {
            while ((entry = zipInputStream.getNextEntry()) != null && enumeration.hasMoreElements()) {

                ZipEntry zipEntry = (ZipEntry) enumeration.nextElement();
                System.out.println(zipEntry.getName());
                System.out.println(zipEntry.getSize());
                entrySize = zipEntry.getSize();

                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                //long entrySize = entry.getCompressedSize();
                //entrySize = entry.getSize(); //gives -1

                if ((currentSize + entrySize) > MAX_FILE_SIZE) {
                    zos.close();
                    //construct a new stream
                    //zos = new ZipOutputStream(new FileOutputStream(new File(zippedPath, constructCurrentPartName(coreId))));
                    currentChunkIndex++;
                    zos = getOutputStream(currentChunkIndex, coreId);
                    currentSize = 0;

                } else {
                    currentSize += entrySize;
                    zos.putNextEntry(new ZipEntry(entry.getName()));
                    byte[] buffer = new byte[8192];
                    int length = 0;
                    while ((length = zipInputStream.read(buffer)) > 0) {
                        outputStream.write(buffer, 0, length);
                    }

                    byte[] unzippedFile = outputStream.toByteArray();
                    zos.write(unzippedFile);
                    unzippedFile = null;
                    outputStream.close();
                    zos.closeEntry();
                }
                //zos.close();
            }
        } finally {
            zos.close();
        }
    }

    public static ZipOutputStream getOutputStream(int i, String coreId) throws IOException {
        System.out.println("inside of getOutputStream()..");
        ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipCopyDest + "\\" + coreId + "_" + i + ".zip"));
        // out.setLevel(Deflater.DEFAULT_COMPRESSION);
        return out;
    }

    public static void main(String args[]) throws IOException {
        String zipFileName = "Large_files_for_testing.zip";
        String zippedPath = "C:\\zip2split\\Large_files_for_testing.zip";
        String coreId = "Large_files_for_testing";
        splitZip(zipFileName, zippedPath, coreId);
    }
}
person tpky    schedule 06.07.2018

Вот мое решение:

public abstract class ZipHelper {

    public static NumberFormat formater = NumberFormat.getNumberInstance(new Locale("pt", "BR"));

    public static List<Path> zip(Collection<File> inputFiles, long maxSize) throws IOException {

        byte[] buffer = new byte[1024];
        int count = 0;
        long currentZipSize = maxSize;
        List<Path> response = new ArrayList<>();
        ZipOutputStream zip = null;
        for (File currentFile : inputFiles) {
            long nextFileSize = currentFile.length();
            long predictedZipSize = currentZipSize + nextFileSize;
            boolean needNewFile = predictedZipSize >= maxSize;
            System.out.println("[=] ZIP current (" + formater.format(currentZipSize) + ") + next file (" + formater.format(nextFileSize) + ") = predicted (" + formater.format(predictedZipSize) + ") > max (" + formater.format(maxSize) + ") ? " + needNewFile);
            if (needNewFile) {
                safeClose(zip);
                Path tmpFile = Files.createTempFile("teste-", (".part." + count++ + ".zip"));
                System.out.println("[#] Starting new file: " + tmpFile);
                zip = new ZipOutputStream(Files.newOutputStream(tmpFile));
                zip.setLevel(Deflater.BEST_COMPRESSION);
                response.add(tmpFile);
                currentZipSize = 0;
            }
            ZipEntry zipEntry = new ZipEntry(currentFile.getName());
            System.out.println("[<] Adding to ZIP: " + currentFile.getName());
            zip.putNextEntry(zipEntry);
            FileInputStream in = new FileInputStream(currentFile);
            zip.write(in.readAllBytes());
            zip.closeEntry();
            safeClose(in);
            long compressed = zipEntry.getCompressedSize();
            System.out.println("[=] Compressed current file: " + formater.format(compressed));
            currentZipSize += zipEntry.getCompressedSize();
        }
        safeClose(zip);
        return response;
    }

    public static void safeClose(Closeable... closeables) {
        if (closeables != null) {
            for (Closeable closeable : closeables) {
                if (closeable != null) {
                    try {
                        System.out.println("[X] Closing: (" + closeable.getClass() + ") - " + closeable);
                        closeable.close();
                    } catch (Throwable ex) {
                        System.err.println("[!] Error on close: " + closeable);
                        ex.printStackTrace();
                    }
                }
            }
        }
    }
}

И вывод консоли:

[?] Files to process: [\data\teste\TestFile(1).pdf, \data\teste\TestFile(2).pdf, \data\teste\TestFile(3).pdf, \data\teste\TestFile(4).pdf, \data\teste\TestFile(5).pdf, \data\teste\TestFile(6).pdf, \data\teste\TestFile(7).pdf]
[=] ZIP current (3.145.728) + next file (1.014.332) = predicted (4.160.060) > max (3.145.728) ? true
[#] Starting new file: C:\Users\Cassio\AppData\Local\Temp\teste-3319961516431535912.part.0.zip
[<] Adding to ZIP: TestFile(1).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@3d99d22e
[=] Compressed current file: 940.422
[=] ZIP current (940.422) + next file (1.511.862) = predicted (2.452.284) > max (3.145.728) ? false
[<] Adding to ZIP: TestFile(2).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@49fc609f
[=] Compressed current file: 1.475.178
[=] ZIP current (2.415.600) + next file (2.439.287) = predicted (4.854.887) > max (3.145.728) ? true
[X] Closing: (class java.util.zip.ZipOutputStream) - java.util.zip.ZipOutputStream@cd2dae5
[#] Starting new file: C:\Users\Cassio\AppData\Local\Temp\teste-8849887746791381380.part.1.zip
[<] Adding to ZIP: TestFile(3).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@4973813a
[=] Compressed current file: 2.374.718
[=] ZIP current (2.374.718) + next file (2.385.447) = predicted (4.760.165) > max (3.145.728) ? true
[X] Closing: (class java.util.zip.ZipOutputStream) - java.util.zip.ZipOutputStream@6321e813
[#] Starting new file: C:\Users\Cassio\AppData\Local\Temp\teste-6305809161676875106.part.2.zip
[<] Adding to ZIP: TestFile(4).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@79be0360
[=] Compressed current file: 2.202.203
[=] ZIP current (2.202.203) + next file (292.918) = predicted (2.495.121) > max (3.145.728) ? false
[<] Adding to ZIP: TestFile(5).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@22a67b4
[=] Compressed current file: 230.491
[=] ZIP current (2.432.694) + next file (4.197.512) = predicted (6.630.206) > max (3.145.728) ? true
[X] Closing: (class java.util.zip.ZipOutputStream) - java.util.zip.ZipOutputStream@57855c9a
[#] Starting new file: C:\Users\Cassio\AppData\Local\Temp\teste-17160527941340008316.part.3.zip
[<] Adding to ZIP: TestFile(6).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@3b084709
[=] Compressed current file: 3.020.115
[=] ZIP current (3.020.115) + next file (1.556.237) = predicted (4.576.352) > max (3.145.728) ? true
[X] Closing: (class java.util.zip.ZipOutputStream) - java.util.zip.ZipOutputStream@3224f60b
[#] Starting new file: C:\Users\Cassio\AppData\Local\Temp\teste-14050058835776413808.part.4.zip
[<] Adding to ZIP: TestFile(7).pdf
[X] Closing: (class java.io.FileInputStream) - java.io.FileInputStream@63e2203c
[=] Compressed current file: 1.460.566
[X] Closing: (class java.util.zip.ZipOutputStream) - java.util.zip.ZipOutputStream@1efed156
[>] Generated ZIP files(s): [C:\Users\Cassio\AppData\Local\Temp\teste-3319961516431535912.part.0.zip, C:\Users\Cassio\AppData\Local\Temp\teste-8849887746791381380.part.1.zip, C:\Users\Cassio\AppData\Local\Temp\teste-6305809161676875106.part.2.zip, C:\Users\Cassio\AppData\Local\Temp\teste-17160527941340008316.part.3.zip, C:\Users\Cassio\AppData\Local\Temp\teste-14050058835776413808.part.4.zip]
person codigoalvo    schedule 21.01.2021