blob: 104ebb0bba71627c5cace21cfa0717e54a9bbf94 [file] [log] [blame]
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed 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 com.google.devtools.build.android.ziputils;
import static com.google.devtools.build.android.ziputils.DirectoryEntry.CENOFF;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDOFF;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSIZ;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDSUB;
import static com.google.devtools.build.android.ziputils.EndOfCentralDirectory.ENDTOT;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* API for writing to a zip archive. This does not currently perform compression,
* but merely provides the facilities for creating a zip archive. The client must
* ensure that file content is written conforming to the created headers.
*/
public class ZipOut {
/**
* Central directory output buffer block size
*/
private static final int DIR_BLOCK_SIZE = 1024 * 1024;
private final String filename;
// To ensure good performance when writing to a remote file system
// we need to use asynchronous output. However, because we may want this
// to run on and off devices, we can't depend on java.nio.AsynchronousFileChannel (JDK 1.7).
// Instead, we use a FileChannel (JDK 1.4), and use a single-thread pool,
// to execute writes serially, but non-blocking from our client's
// point-of-view. All data written, are assumed to remain unchanged until the write is complete.
// ZipIn is designed to not reuse internal buffers, to make direct data transfer safe.
// This optimizes the common cases where input is processed serially.
private final FileChannel fileChannel;
private final ExecutorService executor;
private final List<Future<?>> futures;
private final List<CentralDirectory> centralDirectory;
private CentralDirectory current;
private int fileOffset = 0;
private int entryCount = 0;
private boolean finished = false;
private final boolean verbose = false;
/**
* Creates a {@code ZipOut} for writing to file, with the given (nick)name.
*
* @param channel File channel open for output.
* @param filename File name or nickname.
* @throws java.io.IOException
*/
public ZipOut(FileChannel channel, String filename) throws IOException {
this.executor = Executors.newSingleThreadExecutor();
this.futures = new ArrayList<>();
this.fileChannel = channel;
this.filename = filename;
centralDirectory = new ArrayList<>();
fileOffset = (int) fileChannel.position();
}
/**
* Returns a writable copy of the given
* {@link com.google.devtools.build.android.ziputils.DirectoryEntry}, backed by an internal
* direct byte buffer, allocated over storage for this files central directory. The file offset
* is set, and must not be changed. The variable entry data (filename, extra data and comment),
* must not be changed (in a way that changes the total size of the directory entry).
* @param entry directory entry to copy.
* @return a writable directory entry view, over the provided byte buffer.
*/
public DirectoryEntry nextEntry(DirectoryEntry entry) {
entryCount++;
int size = entry.getSize();
if (current == null || current.buffer.remaining() < size) {
ByteBuffer buffer = ByteBuffer.allocateDirect(DIR_BLOCK_SIZE);
current = CentralDirectory.viewOf(buffer);
centralDirectory.add(current);
}
return current.nextEntry(entry).set(CENOFF, fileOffset);
}
/**
* Writes content to the current entry. Content is written as-is, and the client is responsible
* for compression, consistent with the storage method set in the current directory entry. The
* client must first write an appropriate local file header, and if necessary, complete the entry
* with a data descriptor. If the header indicates that the content is compressed, the client is
* responsible for compressing the data before writing.
*
* <p>Data is written serially, but asynchronously, to the output file. The client must not change
* the underlying data after it has been scheduled for writing. Usually, a client will release
* any references to the data, so that storage may be eligible for GC, once the write operation
* has completed. It's safe to pass references to views of data obtained from a {#link ZipIn},
* object, because {@code ZipIn} doesn't reuse internal buffers.
*
* @param content
*/
public synchronized void write(ByteBuffer content) {
fileOffset += content.remaining();
futures.add(executor.submit(new OutputTask(content)));
}
/**
* Writes a {@link com.google.devtools.build.android.ziputils.View} to the current entry.
* Used to write a {@link com.google.devtools.build.android.ziputils.LocalFileHeader}
* before the content, and if needed, a
* {@link com.google.devtools.build.android.ziputils.DataDescriptor} after the content.
* <P>
* See also {@link #write(java.nio.ByteBuffer)}.
* </P>
*
* @param view the view to write as part of the current entry.
* @throws java.io.IOException
*/
public void write(View<?> view) throws IOException {
view.at(fileOffset).buffer.rewind();
write(view.buffer);
}
/**
* Returns the file position for the next write operation. Because writes are asynchronous, this
* may not be the actual position of the underlying file channel.
* @return the file position position for the next write operation.
*/
public int fileOffset() {
return fileOffset;
}
/**
* Writes out the central directory. This doesn't close the output file.
*/
public void finish() throws IOException {
if (finished) {
return;
}
finished = true;
int cdOffset = fileOffset;
for (CentralDirectory cd : centralDirectory) {
//cd.finish().buffer.rewind();
cd.buffer.flip();
write(cd.buffer);
}
int size = fileOffset - cdOffset;
verbose("ZipOut finishing: " + filename);
verbose("-- CDIR: " + cdOffset + " count: " + entryCount);
verbose("-- EOCD: " + fileOffset + " size: " + size);
EndOfCentralDirectory eocd = EndOfCentralDirectory.allocate(null)
.set(ENDSUB, (short) entryCount)
.set(ENDTOT, (short) entryCount)
.set(ENDSIZ, size)
.set(ENDOFF, cdOffset)
.at(fileOffset);
eocd.buffer.rewind();
write(eocd.buffer);
verbose("-- size: " + fileOffset);
}
/**
* Closes the output file. If this object has not been finished yet, this method will call
* {@link #finish()} before closing the output channel.
*
* @throws java.io.IOException
*/
public void close() throws IOException {
if (!finished) {
finish();
}
try {
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
for (Future<?> f : futures) {
try {
f.get();
} catch (ExecutionException ex) {
throw new IOException(ex.getCause().getMessage());
}
}
} catch (InterruptedException ex) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
fileChannel.close();
}
/**
* Helper class to write asynchronously to the output channel.
*/
private class OutputTask implements Runnable {
final ByteBuffer buffer;
public OutputTask(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
fileChannel.write(buffer);
} catch (IOException ex) {
throw new IllegalStateException("Unexpected IOException writing to output channel");
}
}
}
private void verbose(String msg) {
if (verbose) {
System.out.println(msg);
}
}
}