2019-02-12差量更新--Google archive-patcher(差量文件合成)

合成代码如下

/**
   * Apply a specified patch to the specified old file, creating the specified new file.
   * @param oldFile the old file (will be read)
   * @param patchFile the patch file (will be read)
   * @param newFile the new file (will be written)
   * @throws IOException if anything goes wrong
   */
  public static void applyPatch(File oldFile, File patchFile, File newFile) throws IOException {
    // Figure out temp directory
    File tempFile = File.createTempFile("fbftool", "tmp");
    File tempDir = tempFile.getParentFile();
    tempFile.delete();
    FileByFileV1DeltaApplier applier = new FileByFileV1DeltaApplier(tempDir);
    try (FileInputStream patchIn = new FileInputStream(patchFile);
        BufferedInputStream bufferedPatchIn = new BufferedInputStream(patchIn);
        FileOutputStream newOut = new FileOutputStream(newFile);
        BufferedOutputStream bufferedNewOut = new BufferedOutputStream(newOut)) {
      applier.applyDelta(oldFile, bufferedPatchIn, bufferedNewOut);
      bufferedNewOut.flush();
    }
  }
  
  @Override
  public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)
      throws IOException {
    if (!tempDir.exists()) {
      // Be nice, try to create the temp directory. Don't bother to check return value as the code
      // will fail when it tries to create the file in a few more lines anyways.
      tempDir.mkdirs();
    }
    File tempFile = File.createTempFile("gfbfv1", "old", tempDir);
    try {
      applyDeltaInternal(oldBlob, tempFile, deltaIn, newBlobOut);
    } finally {
      tempFile.delete();
    }
  }
/**
   * Does the work for applying a delta.
   * @param oldBlob the old blob
   * @param deltaFriendlyOldBlob the location in which to store the delta-friendly old blob
   * @param deltaIn the patch stream
   * @param newBlobOut the stream to write the new blob to after applying the delta
   * @throws IOException if anything goes wrong
   */
  private void applyDeltaInternal(
      File oldBlob, File deltaFriendlyOldBlob, InputStream deltaIn, OutputStream newBlobOut)
      throws IOException {

    // First, read the patch plan from the patch stream.
    PatchReader patchReader = new PatchReader();
    PatchApplyPlan plan = patchReader.readPatchApplyPlan(deltaIn);
    writeDeltaFriendlyOldBlob(plan, oldBlob, deltaFriendlyOldBlob);
    // Apply the delta. In v1 there is always exactly one delta descriptor, it is bsdiff, and it
    // takes up the rest of the patch stream - so there is no need to examine the list of
    // DeltaDescriptors in the patch at all.
    long deltaLength = plan.getDeltaDescriptors().get(0).getDeltaLength();
    DeltaApplier deltaApplier = getDeltaApplier();
    // Don't close this stream, as it is just a limiting wrapper.
    @SuppressWarnings("resource")
    LimitedInputStream limitedDeltaIn = new LimitedInputStream(deltaIn, deltaLength);
    // Don't close this stream, as it would close the underlying OutputStream (that we don't own).
    @SuppressWarnings("resource")
    PartiallyCompressingOutputStream recompressingNewBlobOut =
        new PartiallyCompressingOutputStream(
            plan.getDeltaFriendlyNewFileRecompressionPlan(),
            newBlobOut,
            DEFAULT_COPY_BUFFER_SIZE);
    deltaApplier.applyDelta(deltaFriendlyOldBlob, limitedDeltaIn, recompressingNewBlobOut);
    recompressingNewBlobOut.flush();
  }

主要做的如下的事情

1.解析patch文件生成PatchApplyPlan对象

2.生成旧文件old.zip差量友好文件

3.应用合成算法生成new.zip的差量友好文件

4.写入zip文件流完成

先看一下patch文件的解析过程:

/**
   * Reads patch data from the specified {@link InputStream} up to but not including the first byte
   * of delta bytes, and returns a {@link PatchApplyPlan} that describes all the operations that
   * need to be performed in order to apply the patch. When this method returns, the stream is
   * positioned so that the next read will be the first byte of delta bytes corresponding to the
   * first {@link DeltaDescriptor} in the returned plan.
   * @param in the stream to read from
   * @return the plan for applying the patch
   * @throws IOException if anything goes wrong
   */
  public PatchApplyPlan readPatchApplyPlan(InputStream in) throws IOException {
    // Use DataOutputStream for ease of writing. This is deliberately left open, as closing it would
    // close the output stream that was passed in and that is not part of the method's documented
    // behavior.
    @SuppressWarnings("resource")
    DataInputStream dataIn = new DataInputStream(in);

    // Read header and flags.
    byte[] expectedIdentifier = PatchConstants.IDENTIFIER.getBytes("US-ASCII");
    byte[] actualIdentifier = new byte[expectedIdentifier.length];
    dataIn.readFully(actualIdentifier);
    if (!Arrays.equals(expectedIdentifier, actualIdentifier)) {
      throw new PatchFormatException("Bad identifier");
    }
    dataIn.skip(4); // Flags (ignored in v1)
    long deltaFriendlyOldFileSize = checkNonNegative(
        dataIn.readLong(), "delta-friendly old file size");

    // Read old file uncompression instructions.
    int numOldFileUncompressionInstructions = (int) checkNonNegative(
        dataIn.readInt(), "old file uncompression instruction count");
    List> oldFileUncompressionPlan =
        new ArrayList>(numOldFileUncompressionInstructions);
    long lastReadOffset = -1;
    for (int x = 0; x < numOldFileUncompressionInstructions; x++) {
      long offset = checkNonNegative(dataIn.readLong(), "old file uncompression range offset");
      long length = checkNonNegative(dataIn.readLong(), "old file uncompression range length");
      if (offset < lastReadOffset) {
        throw new PatchFormatException("old file uncompression ranges out of order or overlapping");
      }
      TypedRange range = new TypedRange(offset, length, null);
      oldFileUncompressionPlan.add(range);
      lastReadOffset = offset + length; // To check that the next range starts after the current one
    }

    // Read new file recompression instructions
    int numDeltaFriendlyNewFileRecompressionInstructions = dataIn.readInt();
    checkNonNegative(
        numDeltaFriendlyNewFileRecompressionInstructions,
        "delta-friendly new file recompression instruction count");
    List> deltaFriendlyNewFileRecompressionPlan =
        new ArrayList>(
            numDeltaFriendlyNewFileRecompressionInstructions);
    lastReadOffset = -1;
    for (int x = 0; x < numDeltaFriendlyNewFileRecompressionInstructions; x++) {
      long offset = checkNonNegative(
          dataIn.readLong(), "delta-friendly new file recompression range offset");
      long length = checkNonNegative(
          dataIn.readLong(), "delta-friendly new file recompression range length");
      if (offset < lastReadOffset) {
        throw new PatchFormatException(
            "delta-friendly new file recompression ranges out of order or overlapping");
      }
      lastReadOffset = offset + length; // To check that the next range starts after the current one

      // Read the JreDeflateParameters
      // Note that v1 only supports the default deflate compatibility window.
      checkRange(
          dataIn.readByte(),
          PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
          PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
          "compatibility window id");
      int level = (int) checkRange(dataIn.readUnsignedByte(), 1, 9, "recompression level");
      int strategy = (int) checkRange(dataIn.readUnsignedByte(), 0, 2, "recompression strategy");
      int nowrapInt = (int) checkRange(dataIn.readUnsignedByte(), 0, 1, "recompression nowrap");
      TypedRange range =
          new TypedRange(
              offset,
              length,
              JreDeflateParameters.of(level, strategy, nowrapInt == 0 ? false : true));
      deltaFriendlyNewFileRecompressionPlan.add(range);
    }

    // Read the delta metadata, but stop before the first byte of the actual delta.
    // V1 has exactly one delta and it must be bsdiff.
    int numDeltaRecords = (int) checkRange(dataIn.readInt(), 1, 1, "num delta records");

    List deltaDescriptors = new ArrayList(numDeltaRecords);
    for (int x = 0; x < numDeltaRecords; x++) {
      byte deltaFormatByte = (byte)
      checkRange(
          dataIn.readByte(),
          PatchConstants.DeltaFormat.BSDIFF.patchValue,
          PatchConstants.DeltaFormat.BSDIFF.patchValue,
          "delta format");
      long deltaFriendlyOldFileWorkRangeOffset = checkNonNegative(
          dataIn.readLong(), "delta-friendly old file work range offset");
      long deltaFriendlyOldFileWorkRangeLength = checkNonNegative(
          dataIn.readLong(), "delta-friendly old file work range length");
      long deltaFriendlyNewFileWorkRangeOffset = checkNonNegative(
          dataIn.readLong(), "delta-friendly new file work range offset");
      long deltaFriendlyNewFileWorkRangeLength = checkNonNegative(
          dataIn.readLong(), "delta-friendly new file work range length");
      long deltaLength = checkNonNegative(dataIn.readLong(), "delta length");
      DeltaDescriptor descriptor =
          new DeltaDescriptor(
              PatchConstants.DeltaFormat.fromPatchValue(deltaFormatByte),
              new TypedRange(
                  deltaFriendlyOldFileWorkRangeOffset, deltaFriendlyOldFileWorkRangeLength, null),
              new TypedRange(
                  deltaFriendlyNewFileWorkRangeOffset, deltaFriendlyNewFileWorkRangeLength, null),
              deltaLength);
      deltaDescriptors.add(descriptor);
    }

    return new PatchApplyPlan(
        Collections.unmodifiableList(oldFileUncompressionPlan),
        deltaFriendlyOldFileSize,
        Collections.unmodifiableList(deltaFriendlyNewFileRecompressionPlan),
        Collections.unmodifiableList(deltaDescriptors));
  }

主要就是对patch文件格式(上一篇文章有介绍写path,实际就是它的逆过程)进行解析:

1.读取文件头"GFbFv1_0",校验文件头

2.忽略4byte的标记位

3.旧文件差量友好文件的大小,校验

4.旧文件差量友好文件个数,并校验

5.读取n个旧文件待解压的偏移、长度、并校验

6.读取新文件待压缩个数,并校验

7.新文件待压缩文件的偏移、大小、压缩级别、编码策略、nowrap值,总共n个

8.新文件差量描述个数

9.读n个差量算法描述。差量算法id,旧文件应用差量算法的偏移和长度,新文件应用差量算法的偏移和长度,生成的差量文件的大小
返回PatchApplyPlan对象

写旧文件差量友好文件的过程跟上一篇一样,不复述:

/**
   * Writes the delta-friendly old blob to temporary storage.
   * @param plan the plan to use for uncompressing
   * @param oldBlob the blob to turn into a delta-friendly blob
   * @param deltaFriendlyOldBlob where to write the blob
   * @throws IOException if anything goes wrong
   */
  private void writeDeltaFriendlyOldBlob(
      PatchApplyPlan plan, File oldBlob, File deltaFriendlyOldBlob) throws IOException {
    RandomAccessFileOutputStream deltaFriendlyOldFileOut = null;
    try {
      deltaFriendlyOldFileOut =
          new RandomAccessFileOutputStream(
              deltaFriendlyOldBlob, plan.getDeltaFriendlyOldFileSize());
      DeltaFriendlyFile.generateDeltaFriendlyFile(
          plan.getOldFileUncompressionPlan(),
          oldBlob,
          deltaFriendlyOldFileOut,
          false,
          DEFAULT_COPY_BUFFER_SIZE);
    } finally {
      try {
        deltaFriendlyOldFileOut.close();
      } catch (Exception ignored) {
        // Nothing
      }
    }
  }

差量友好文件的合成使用bspatch算法写到输出流中

/**
 * An implementation of {@link DeltaApplier} that uses {@link BsPatch} to apply a bsdiff patch.
 */
public class BsDiffDeltaApplier implements DeltaApplier {

  @Override
  public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)
      throws IOException {
    RandomAccessFile oldBlobRaf = null;
    try {
      oldBlobRaf = new RandomAccessFile(oldBlob, "r");
      BsPatch.applyPatch(oldBlobRaf, newBlobOut, deltaIn);
    } finally {
      try {
        oldBlobRaf.close();
      } catch (Exception ignored) {
        // Nothing
      }
    }
  }
}

bspatch算法我们不看了,看一下这个输出流PartiallyCompressingOutputStream
,最终调用的是这个地方

/**
   * Write up to length bytes from the specified buffer to the underlying stream. For
   * simplicity, this method stops at the edges of ranges; it is always either copying OR
   * compressing bytes, but never both. All manipulation of the compression state machinery is done
   * within this method. When the end of a compression range is reached it is completely flushed to
   * the output stream, to keep things as straightforward as possible.
   * @param buffer the buffer to copy/compress bytes from
   * @param offset the offset at which to start copying/compressing
   * @param length the maximum number of bytes to copy or compress
   * @return the number of bytes of the buffer that have been consumed
   */
  private int writeChunk(byte[] buffer, int offset, int length) throws IOException {
    if (bytesTillCompressionStarts() == 0 && !currentlyCompressing()) {
      // Compression will begin immediately.
      JreDeflateParameters parameters = nextCompressedRange.getMetadata();
      if (deflater == null) {
        deflater = new Deflater(parameters.level, parameters.nowrap);
      } else if (lastDeflateParameters.nowrap != parameters.nowrap) {
        // Last deflater must be destroyed because nowrap settings do not match.
        deflater.end();
        deflater = new Deflater(parameters.level, parameters.nowrap);
      }
      // Deflater will already have been reset at the end of this method, no need to do it again.
      // Just set up the right parameters.
      deflater.setLevel(parameters.level);
      deflater.setStrategy(parameters.strategy);
      deflaterOut = new DeflaterOutputStream(normalOut, deflater, compressionBufferSize);
    }

    int numBytesToWrite;
    OutputStream writeTarget;
    if (currentlyCompressing()) {
      // Don't write past the end of the compressed range.
      numBytesToWrite = (int) Math.min(length, bytesTillCompressionEnds());
      writeTarget = deflaterOut;
    } else {
      writeTarget = normalOut;
      if (nextCompressedRange == null) {
        // All compression ranges have been consumed.
        numBytesToWrite = length;
      } else {
        // Don't write past the point where the next compressed range begins.
        numBytesToWrite = (int) Math.min(length, bytesTillCompressionStarts());
      }
    }

    writeTarget.write(buffer, offset, numBytesToWrite);
    numBytesWritten += numBytesToWrite;

    if (currentlyCompressing() && bytesTillCompressionEnds() == 0) {
      // Compression range complete. Finish the output and set up for the next run.
      deflaterOut.finish();
      deflaterOut.flush();
      deflaterOut = null;
      deflater.reset();
      lastDeflateParameters = nextCompressedRange.getMetadata();
      if (rangeIterator.hasNext()) {
        // More compression ranges await in the future.
        nextCompressedRange = rangeIterator.next();
      } else {
        // All compression ranges have been consumed.
        nextCompressedRange = null;
        deflater.end();
        deflater = null;
      }
    }

    return numBytesToWrite;
  }

1.在PartiallyCompressingOutputStream的构造函数中,获得了compressionRanges的第一个数据

2.判断写入的数据距离下一个压缩数据开始如果是0,且当前并不在压缩,则获得压缩设置,即压缩级别,编码策略,nowrap,并进行设置。并包装输出流为压缩流。

3.如果当前正在压缩,则判断当前写入的数据长度和待压缩的数据长度,取其中小的一个,设置目标输出流为压缩流,即负责压缩工作,而不是拷贝工作。

4.如果当前不在压缩,如果没有下一个压缩数据了,则直接写入对应长度的数据,如果还有下一个压缩数据,则取当前写入数据的长度和距离下一个压缩数据的偏移位置的长度,取其中小的一个,设置目标输出流为正常流,即进行拷贝工作,而不是压缩工作

5.判断当前是否正在压缩,并且当前节点所有压缩数据都已经写入完全,执行压缩流的finish和flush操作,重置压缩相关配置项,并移动待压缩的数据到下一条记录。

6.重复以上操作,直到所有数据写入完全。

你可能感兴趣的:(2019-02-12差量更新--Google archive-patcher(差量文件合成))