blob: bd7f2356316047469b837ef417fed86c208130a3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.commons.compress.archivers.zip;
import org.apache.commons.compress.utils.FileNameUtils;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
/**
* Used internally by {@link ZipArchiveOutputStream} when creating a split archive.
*
* @since 1.20
*/
class ZipSplitOutputStream extends OutputStream {
private OutputStream outputStream;
private File zipFile;
private final long splitSize;
private int currentSplitSegmentIndex;
private long currentSplitSegmentBytesWritten;
private boolean finished;
private final byte[] singleByte = new byte[1];
/**
* 8.5.1 Capacities for split archives are as follows:
* <p>
* Maximum number of segments = 4,294,967,295 - 1
* Maximum .ZIP segment size = 4,294,967,295 bytes (refer to section 8.5.6)
* Minimum segment size = 64K
* Maximum PKSFX segment size = 2,147,483,647 bytes
*/
private final static long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L;
private final static long ZIP_SEGMENT_MAX_SIZE = 4294967295L;
/**
* Create a split zip. If the zip file is smaller than the split size,
* then there will only be one split zip, and its suffix is .zip,
* otherwise the split segments should be like .z01, .z02, ... .z(N-1), .zip
*
* @param zipFile the zip file to write to
* @param splitSize the split size
*/
public ZipSplitOutputStream(final File zipFile, final long splitSize) throws IllegalArgumentException, IOException {
if (splitSize < ZIP_SEGMENT_MIN_SIZE || splitSize > ZIP_SEGMENT_MAX_SIZE) {
throw new IllegalArgumentException("zip split segment size should between 64K and 4,294,967,295");
}
this.zipFile = zipFile;
this.splitSize = splitSize;
this.outputStream = Files.newOutputStream(zipFile.toPath());
// write the zip split signature 0x08074B50 to the zip file
writeZipSplitSignature();
}
/**
* Some data can not be written to different split segments, for example:
* <p>
* 4.4.1.5 The end of central directory record and the Zip64 end
* of central directory locator record MUST reside on the same
* disk when splitting or spanning an archive.
*
* @param unsplittableContentSize
* @throws IllegalArgumentException
* @throws IOException
*/
public void prepareToWriteUnsplittableContent(final long unsplittableContentSize) throws IllegalArgumentException, IOException {
if (unsplittableContentSize > this.splitSize) {
throw new IllegalArgumentException("The unsplittable content size is bigger than the split segment size");
}
final long bytesRemainingInThisSegment = this.splitSize - this.currentSplitSegmentBytesWritten;
if (bytesRemainingInThisSegment < unsplittableContentSize) {
openNewSplitSegment();
}
}
@Override
public void write(final int i) throws IOException {
singleByte[0] = (byte)(i & 0xff);
write(singleByte);
}
@Override
public void write(final byte[] b) throws IOException {
write(b, 0, b.length);
}
/**
* Write the data to zip split segments, if the remaining space of current split segment
* is not enough, then a new split segment should be created
*
* @param b data to write
* @param off offset of the start of data in param b
* @param len the length of data to write
* @throws IOException
*/
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
if (len <= 0) {
return;
}
if (currentSplitSegmentBytesWritten >= splitSize) {
openNewSplitSegment();
write(b, off, len);
} else if (currentSplitSegmentBytesWritten + len > splitSize) {
final int bytesToWriteForThisSegment = (int) splitSize - (int) currentSplitSegmentBytesWritten;
write(b, off, bytesToWriteForThisSegment);
openNewSplitSegment();
write(b, off + bytesToWriteForThisSegment, len - bytesToWriteForThisSegment);
} else {
outputStream.write(b, off, len);
currentSplitSegmentBytesWritten += len;
}
}
@Override
public void close() throws IOException {
if (!finished) {
finish();
}
}
/**
* The last zip split segment's suffix should be .zip
*
* @throws IOException
*/
private void finish() throws IOException {
if (finished) {
throw new IOException("This archive has already been finished");
}
final String zipFileBaseName = FileNameUtils.getBaseName(zipFile.getName());
final File lastZipSplitSegmentFile = new File(zipFile.getParentFile(), zipFileBaseName + ".zip");
outputStream.close();
if (!zipFile.renameTo(lastZipSplitSegmentFile)) {
throw new IOException("Failed to rename " + zipFile + " to " + lastZipSplitSegmentFile);
}
finished = true;
}
/**
* Create a new zip split segment and prepare to write to the new segment
*
* @throws IOException
*/
private void openNewSplitSegment() throws IOException {
File newFile;
if (currentSplitSegmentIndex == 0) {
outputStream.close();
newFile = createNewSplitSegmentFile(1);
if (!zipFile.renameTo(newFile)) {
throw new IOException("Failed to rename " + zipFile + " to " + newFile);
}
}
newFile = createNewSplitSegmentFile(null);
outputStream.close();
outputStream = Files.newOutputStream(newFile.toPath());
currentSplitSegmentBytesWritten = 0;
zipFile = newFile;
currentSplitSegmentIndex++;
}
/**
* Write the zip split signature (0x08074B50) to the head of the first zip split segment
*
* @throws IOException
*/
private void writeZipSplitSignature() throws IOException {
outputStream.write(ZipArchiveOutputStream.DD_SIG);
currentSplitSegmentBytesWritten += ZipArchiveOutputStream.DD_SIG.length;
}
/**
* Create the new zip split segment, the last zip segment should be .zip, and the zip split segments' suffix should be
* like .z01, .z02, .z03, ... .z99, .z100, ..., .z(N-1), .zip
* <p>
* 8.3.3 Split ZIP files are typically written to the same location
* and are subject to name collisions if the spanned name
* format is used since each segment will reside on the same
* drive. To avoid name collisions, split archives are named
* as follows.
* <p>
* Segment 1 = filename.z01
* Segment n-1 = filename.z(n-1)
* Segment n = filename.zip
* <p>
* NOTE:
* The zip split segment begin from 1,2,3,... , and we're creating a new segment,
* so the new segment suffix should be (currentSplitSegmentIndex + 2)
*
* @param zipSplitSegmentSuffixIndex
* @return
* @throws IOException
*/
private File createNewSplitSegmentFile(final Integer zipSplitSegmentSuffixIndex) throws IOException {
final int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex == null ? (currentSplitSegmentIndex + 2) : zipSplitSegmentSuffixIndex;
final String baseName = FileNameUtils.getBaseName(zipFile.getName());
String extension = ".z";
if (newZipSplitSegmentSuffixIndex <= 9) {
extension += "0" + newZipSplitSegmentSuffixIndex;
} else {
extension += newZipSplitSegmentSuffixIndex;
}
final File newFile = new File(zipFile.getParent(), baseName + extension);
if (newFile.exists()) {
throw new IOException("split zip segment " + baseName + extension + " already exists");
}
return newFile;
}
public int getCurrentSplitSegmentIndex() {
return currentSplitSegmentIndex;
}
public long getCurrentSplitSegmentBytesWritten() {
return currentSplitSegmentBytesWritten;
}
}