Я пытаюсь извлечь определенные элементы из массивных 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;