io.milton.zsync.UploadReader Maven / Gradle / Ivy
The newest version!
/*
* 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 io.milton.zsync;
import io.milton.common.RangeUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.List;
import java.util.NoSuchElementException;
import org.apache.commons.lang.StringUtils;
import io.milton.http.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An object that performs the server side operations needed to assemble the
* file from a ZSync PUT.
*
*
* These operations consist of copying byte ranges into the new file. The
* {@link #moveBlocks} method copies ranges from the previous file according to
* a list of RelocateRanges, while the {@link #sendRanges} method copies the new
* data received in the upload. Both of these methods are overloaded with
* versions that accept File rather than byte[] arguments for dealing with large
* files that should not be loaded into memory all at once.
*
* To assemble the file from a ZSync upload, the server should construct an
* UploadReader, passing to the constructor the file to be updated and an
* InputStream containing the upload data. It should then invoke the
* {@link #assemble()} method, which will return a temporary file that was
* created.
*
* @author Nick
*
*/
public class UploadReader {
private static final Logger log = LoggerFactory.getLogger(UploadReader.class);
/**
* Copies blocks of data from the in array to the out array.
*
* @param in The byte array containing the server's file being replaced
* @param rlist The List of RelocateRanges received from the upload
* @param blockSize The block size used in rlist
* @param out The byte array of the file being assembled
*/
public static void moveBlocks(byte[] in, List rlist, int blockSize, byte[] out) {
for (RelocateRange reloc : rlist) {
int startBlock = (int) reloc.getBlockRange().getStart().longValue();
int finishBlock = (int) reloc.getBlockRange().getFinish().longValue();
int startByte = startBlock * blockSize;
int newOffset = (int) reloc.getOffset();
int numBytes = (finishBlock - startBlock) * blockSize;
System.arraycopy(in, startByte, out, newOffset, numBytes);
}
}
/**
* Copies blocks of data from the input File to the output File. For each
* RelocateRange A-B/C in relocRanges, the block starting at A and ending at
* B-1 is copied from inFile and written to byte C of outFile.
*
* @param inFile The server's File being replaced
* @param relocRanges The Enumeration of RelocateRanges parsed from the
* Upload's relocStream
* @param blocksize The block size used in relocRanges
* @param outFile The File being assembled
* @throws IOException
*/
public static void moveBlocks(File inFile, Enumeration relocRanges, int blocksize, File outFile) throws IOException {
/*
* Because transferFrom can supposedly throw Exceptions when copying large Files,
* this method invokes moveRange to copy incrementally
*/
/*The FileChannels should be obtained from a RandomAccessFile rather than a
*Stream, or the position() method will not work correctly
*/
try (FileChannel rc = new RandomAccessFile(inFile, "r").getChannel();
FileChannel wc = new RandomAccessFile(outFile, "rw").getChannel()) {
while (relocRanges.hasMoreElements()) {
moveRange(rc, relocRanges.nextElement(), blocksize, wc);
}
}
}
/**
* Copies a Range of blocks from rc into a new offset of wc
*
* @param rc A FileChannel for the input File
* @param reloc The RelocateRange specifying the Range to be copied and its
* new offset
* @param blockSize The block size used by reloc
* @param wc The FileChannel for the output File
* @throws IOException
*/
private static void moveRange(FileChannel rc, RelocateRange reloc,
int blockSize, FileChannel wc) throws IOException {
long MAX_BUFFER = 16384;
long startBlock = reloc.getBlockRange().getStart();
long finishBlock = reloc.getBlockRange().getFinish();
long bytesLeft = (finishBlock - startBlock) * blockSize; //bytes left to copy
long readAtOnce = 0; //number of bytes to attempt to read
long bytesRead = 0; //number of bytes actually read
long currOffset = reloc.getOffset(); //current write position
if (finishBlock * blockSize > rc.size() || startBlock < 0) {
throw new RuntimeException("Invalid RelocateRange: Source file does not contain blocks "
+ reloc.getBlockRange().getRange());
}
rc.position(startBlock * blockSize);
while (bytesLeft > 0) {
readAtOnce = Math.min(bytesLeft, MAX_BUFFER);
/*Because transferFrom does not update the write channel's position,
* it needs to be set manually
*/
bytesRead = wc.transferFrom(rc, currOffset, readAtOnce);
bytesLeft -= bytesRead;
currOffset += bytesRead;
}
}
/**
* Copies bytes from the in array into Ranges of the out array. The in array
* is expected to contain the queued bytes in the same order as the ranges
* List.
*
* @param in An array containing the queued bytes corresponding to the
* ranges List
* @param ranges The List of target Ranges
* @param out The byte array for the file being assembled
*/
public static void sendRanges(byte[] in, List ranges, byte[] out) {
int pos = 0;
for (Range r : ranges) {
int length = (int) (r.getFinish() - r.getStart());
System.arraycopy(in, pos, out, r.getStart().intValue(), length);
pos += length;
}
}
/**
* Inserts the data from each DataRange into the output File, at the
* appropriate offset
*
* @param byteRanges The Enumeration of Range/InputStream pairs parsed from
* the Upload's dataStream
* @param outFile The output File being assembled
* @throws IOException
*/
public static void sendRanges(Enumeration byteRanges, File outFile)
throws IOException {
int BUFFER_SIZE = 16384;
byte[] buffer = new byte[BUFFER_SIZE];
try (RandomAccessFile randAccess = new RandomAccessFile(outFile, "rw")) {
while (byteRanges.hasMoreElements()) {
ByteRange byteRange = byteRanges.nextElement();
Range range = byteRange.getRange();
InputStream data = byteRange.getDataQueue();
sendBytes(data, range, buffer, randAccess);
}
}
}
/**
* Reads a number of bytes from the InputStream equal to the size of the
* specified Range and writes them into that Range of the RandomAccessFile.
*
* @param dataIn The InputStream containing the data to be copied
* @param range The target location in the RandomAccessFile
* @param buffer A byte array used to transfer data from dataIn to fileOut
* @param fileOut A RandomAccessFile for the File being assembled
* @throws IOException
*/
private static void sendBytes(InputStream dataIn, Range range, byte[] buffer,
RandomAccessFile fileOut) throws IOException {
long bytesLeft = (range.getFinish() - range.getStart());
int bytesRead = 0;
int readAtOnce = 0;
fileOut.seek(range.getStart());
while (bytesLeft > 0) {
readAtOnce = (int) Math.min(buffer.length, bytesLeft);
bytesRead = dataIn.read(buffer, 0, readAtOnce);
fileOut.write(buffer, 0, bytesRead);
bytesLeft -= bytesRead;
if (bytesLeft > 0 && bytesRead < 0) {
throw new RuntimeException("Unable to copy byte Range: " + range.getRange()
+ ". End of InputStream reached with " + bytesLeft + " bytes left.");
}
}
}
/**
* Copies the contents of the source file to the destination file and sets
* the destination file's length.
*
* @param inFile The source file
* @param outFile The destination file
* @param length The desired length of the destination file
* @throws IOException
*/
private static void copyFile(File inFile, File outFile, long length) throws IOException {
try (InputStream fIn = new FileInputStream(inFile);
OutputStream fOut = new FileOutputStream(outFile)) {
RangeUtils.sendBytes(fIn, fOut, inFile.length());
}
try (RandomAccessFile randAccess = new RandomAccessFile(outFile, "rw")){
randAccess.setLength(length);
}
}
private final File serverCopy;
private final File uploadedCopy;
private final Upload uploadData;
/**
* Constructor that parses the InputStream into an Upload object and
* initializes a temporary file that will contain the assembled upload
*
* @param serverFile The server file to be updated
* @param uploadIn A stream containing the ZSync PUT data
* @throws IOException
*/
public UploadReader(File serverFile, InputStream uploadIn) throws IOException {
this.serverCopy = serverFile;
this.uploadData = Upload.parse(uploadIn);
this.uploadedCopy = File.createTempFile("zsync-upload", "newFile");
}
/**
* Invokes the methods to put together the uploaded file.
*
* @return The assembled File
* @throws IOException
*/
public File assemble() throws IOException {
if (uploadData.getBlocksize() <= 0) {
throw new RuntimeException("Invalid blocksize specified: " + uploadData.getBlocksize());
}
if (uploadData.getFilelength() <= 0) {
throw new RuntimeException("Invalid file length specified: " + uploadData.getFilelength());
}
if (StringUtils.isBlank(uploadData.getSha1())) {
throw new RuntimeException("No SHA1 checksum provided.");
}
try (InputStream relocIn = uploadData.getRelocStream();
InputStream dataIn = uploadData.getDataStream()) {
Enumeration relocEnum = new RelocateParser(relocIn);
Enumeration dataEnum = new ByteRangeParser(dataIn);
copyFile(serverCopy, uploadedCopy, uploadData.getFilelength());
moveBlocks(serverCopy, relocEnum, (int) uploadData.getBlocksize(), uploadedCopy);
sendRanges(dataEnum, uploadedCopy);
}
return uploadedCopy;
}
/**
* Returns the expected SHA1 checksum String received in the upload
*
* @return A SHA1 checksum
*/
public String getChecksum() {
return uploadData.getSha1();
}
/**
* An object that wraps the relocate stream of Upload (
* {@link Upload#getRelocStream} )in an Enumeration of RelocateRanges. The
* relocate stream is expected to contain a comma separated list of
* RelocateRanges, e.g.
*
* 10-20/123, 100-200/789
*
*
* A few whitespaces at the beginning or end of the list are ignored, as are
* those surrounding the commas.
*
* @author Nick
*
*/
private static class RelocateParser implements Enumeration {
private InputStream relocIn;
private String nextToken;
private final byte[] COMMA = new byte[1];
/**
* Constructs the Enumeration of RelocateRanges from an InputStream
*
* @param relocIn An InputStream obtained from
* {@link Upload#getRelocStream()}
*/
public RelocateParser(InputStream relocIn) {
try {
this.relocIn = relocIn;
this.COMMA[0] = ",".getBytes(Upload.CHARSET)[0];
this.nextToken = Upload.readToken(relocIn, COMMA, 64);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public boolean hasMoreElements() {
return !StringUtils.isBlank(nextToken);
}
@Override
public RelocateRange nextElement() {
if (!this.hasMoreElements()) {
throw new NoSuchElementException("No more RelocateRanges");
}
try {
RelocateRange reloc = RelocateRange.parse(nextToken);
nextToken = Upload.readToken(relocIn, COMMA, 64);
return reloc;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
/**
* An object that wraps the data stream portion of an Upload in an
* Enumeration of ByteRanges.
*
* In order for the parsing to work, the proper number of bytes must be read
* from each ByteRange returned by {@link #nextElement()} prior to the next
* invocation of {@link #hasMoreElements()}.
*
* @author Nick
*
*/
private static class ByteRangeParser implements Enumeration {
/*The dataStream portion of an Upload*/
private final InputStream dataQueue;
/*The Range of the next ByteRange. A null value means that the next Range has not
*been loaded or that the end of the data section has been reached.
*/
private Range nextRange;
/*Whether an attempt has been made to read the next Range KV pair*/
private boolean rangeloaded;
private final byte[] COLON = {":".getBytes(Upload.CHARSET)[0]};
/**
* Constructs the Enumeration from the specified InputStream
*
* @param in The InputStream obtained from
* {@link Upload#getDataStream()}
* @throws UnsupportedEncodingException
*/
public ByteRangeParser(InputStream in) throws UnsupportedEncodingException {
this.dataQueue = in;
this.rangeloaded = false;
}
@Override
public boolean hasMoreElements() {
/*
* If rangeloaded == false, attempt to read the next Range KV pair and set rangeloaded = true.
* If rangeloaded == true and nextRange == null, there are no further ByteRanges.
*
*/
try {
if (rangeloaded) {
return nextRange != null;
}
String nextKey = Upload.readToken(dataQueue, COLON, 64).trim();
if (StringUtils.isBlank(nextKey)) {
nextRange = null;
} else if (!nextKey.equalsIgnoreCase(Upload.RANGE)) {
throw new RuntimeException("Invalid key. Expected: " + Upload.RANGE
+ "\tActual: " + nextKey);
} else {
nextRange = Range.parse(Upload.readValue(dataQueue, 64).trim());
}
rangeloaded = true;
return nextRange != null;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
@Override
public ByteRange nextElement() {
if (!hasMoreElements()) {
throw new NoSuchElementException("No more ByteRanges");
}
this.rangeloaded = false; //Reset rangeloaded
return new ByteRange(nextRange, dataQueue);
}
}
}