Выборочное извлечение записей из zip-файла в S3 без загрузки всего файла - PullRequest
0 голосов
/ 03 ноября 2019

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

Решение Python здесь: Чтение ZIP-файлов из S3 без загрузки всего файла , кажется, работает. Эквивалентные базовые возможности в Java кажутся менее снисходительными, поэтому мне пришлось внести различные корректировки.

В прилагаемом коде вы можете видеть, что я успешно получаю центральный каталог и записываю его ввременный файл, который Java ZipFile может использовать для итерации записей zip с компакт-диска.

Однако я застрял при надувании отдельной записи. Текущий код генерирует исключение плохого заголовка. Нужно ли давать инфлятору локальный заголовок файла + сжатый контент или просто сжатый контент? Я пробовал оба, но я явно либо не использую корректор Inflator и / или не даю ему то, что он ожидает.

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.util.IOUtils;

public class S3ZipTest {
    private AmazonS3 s3;

    public S3ZipTest(String bucket, String key) throws Exception {
        s3 = getClient();
        ObjectMetadata metadata = s3.getObjectMetadata(bucket, key);
        runTest(bucket, key, metadata.getContentLength());
    }

    private void runTest(String bucket, String key, long size) throws Exception {
        // fetch the last 22 bytes (end-of-central-directory record; assuming the comment field is empty)
        long start = size - 22;
        GetObjectRequest req = new GetObjectRequest(bucket, key).withRange(start);
        System.out.println("eocd start: " + start);

        // fetch the end of cd record
        S3Object s3Object = s3.getObject(req);
        byte[] eocd = IOUtils.toByteArray(s3Object.getObjectContent());

        // get the start offset and size of the central directory
        int cdSize = byteArrayToLeInt(Arrays.copyOfRange(eocd, 12, 16));
        int cdStart = byteArrayToLeInt(Arrays.copyOfRange(eocd, 16, 20));

        System.out.println("cdStart: " + cdStart);
        System.out.println("cdSize: " + cdSize);

        // get the full central directory
        req = new GetObjectRequest(bucket, key).withRange(cdStart, cdStart + cdSize - 1);
        s3Object = s3.getObject(req);
        byte[] cd = IOUtils.toByteArray(s3Object.getObjectContent());

        // write the full dir + eocd:
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        // write cd
        out.write(cd);

        // write eocd, resetting the cd start to 0 since that is 
        // where it will appear in our new temp file
        byte[] b = leIntToByteArray(0);
        eocd[16] = b[0];
        eocd[17] = b[1];
        eocd[18] = b[2];
        eocd[19] = b[3];
        out.write(eocd);
        out.flush();

        byte[] cdbytes = out.toByteArray();

        // here we are writing the CD + EOCD to a temp file.
        // ZipFile can read the entries from this file.
        // ZipInputStream and commons compress will not- they seem upset that the data isn't actually here
        File tempFile = new File("temp.zip");
        FileOutputStream output = new FileOutputStream(tempFile);
        output.write(cdbytes);
        output.flush();
        output.close();

        ZipFile zipFile = new ZipFile(tempFile);
        Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
        long offset = 0;
        while (zipEntries.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) zipEntries.nextElement();
            long fileSize = 0;
            long extra = entry.getExtra() == null ? 0 : entry.getExtra().length;
            offset += 30 + entry.getName().length() + extra;
            if (!entry.isDirectory()) {
                fileSize = entry.getCompressedSize();
                System.out.println(entry.getName() + " offset=" + offset + " size" + fileSize);
                // not working
                // getEntryContent(bucket, key, offset, fileSize, (int)entry.getSize());
            }
            offset += fileSize;
        }
        zipFile.close();
    }

    private void getEntryContent(String bucket, String key, long offset, long compressedSize, int fullSize) throws Exception {
        //HERE is where things go bad.
        //my guess was that we need to get past the local header for an entry to the actual 
        //start of deflated content and then read all the content and pass to the Inflator.
        //this yields java.util.zip.DataFormatException: incorrect header check

        System.out.print("reading " + compressedSize +  " bytes starting from offset " + offset);
        GetObjectRequest req = new GetObjectRequest(bucket, key).withRange(offset, offset + compressedSize);
        S3Object s3Object = s3.getObject(req);
        byte[] con = IOUtils.toByteArray(s3Object.getObjectContent());
        Inflater inf = new Inflater();
        inf.setInput(con);
        byte[] inflatedContent = new byte[fullSize];
        int sz = inf.inflate(inflatedContent);
        System.out.println("inflated: " + sz);
        // write inflatedContent to file or whatever...
    }

    public static int byteArrayToLeInt(byte[] b) {
        final ByteBuffer bb = ByteBuffer.wrap(b);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        return bb.getInt();
    }

    public static byte[] leIntToByteArray(int i) {
        final ByteBuffer bb = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        bb.putInt(i);
        return bb.array();
    }

    protected AmazonS3 getClient() {
        AmazonS3 client = AmazonS3ClientBuilder
            .standard()
            .withRegion("us-east-1")
            .build();
        return client;
    }

    public static void main(String[] args) {
        try {
            new S3ZipTest("alexa-public", "test.zip");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Edit

СравнениеПри вычислениях Python в соответствии с теми, что были в моем коде Java, я понял, что Java отключена на 4. entry.getExtra().length может сообщить, например, 24, как и утилита строки zipinfo cmd для той же записи. Отчеты Python 28. Я не до конца понимаю расхождение, но в спецификации PKWare для дополнительного поля упоминается «2-байтовый идентификатор и 2-байтовое поле размера данных». В любом случае, добавив значение выдумки 4, оно заработало, но я бы хотел немного больше понять, что происходит - добавление случайных значений выдумки, чтобы заставить вещи работать, не решается: offset += 30 + entry.getName().length() + extra + 4;

1 Ответ

0 голосов
/ 05 ноября 2019

Мой общий подход был здравым, но сдерживался отсутствием деталей, возвращаемых из ZipFile Java. Например, иногда в конце сжатых данных есть дополнительные 16 байтов до начала следующего локального заголовка. Ничто в ZipFile не может помочь с этим.

zip4j представляется лучшим вариантом и предоставляет такие методы, как: header.getOffsetLocalHeader(), который удаляет некоторые из подверженных ошибкам вычислений.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...