mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Initial import of stuff I don't hate
This commit is contained in:
263
src/DeltaPackage.cs
Normal file
263
src/DeltaPackage.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Ionic.Zip;
|
||||
using ReactiveUIMicro;
|
||||
|
||||
namespace Squirrel.Core
|
||||
{
|
||||
public interface IDeltaPackageBuilder
|
||||
{
|
||||
ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile);
|
||||
ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile);
|
||||
}
|
||||
|
||||
public class DeltaPackageBuilder : IEnableLogger, IDeltaPackageBuilder
|
||||
{
|
||||
public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile)
|
||||
{
|
||||
Contract.Requires(basePackage != null);
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
||||
|
||||
if (basePackage.Version > newPackage.Version) {
|
||||
var message = String.Format(
|
||||
"You cannot create a delta package based on version {0} as it is a later version than {1}",
|
||||
basePackage.Version,
|
||||
newPackage.Version);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
if (basePackage.ReleasePackageFile == null) {
|
||||
throw new ArgumentException("The base package's release file is null", "basePackage");
|
||||
}
|
||||
|
||||
if (!File.Exists(basePackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The base package release does not exist", basePackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
if (!File.Exists(newPackage.ReleasePackageFile)) {
|
||||
throw new FileNotFoundException("The new package release does not exist", newPackage.ReleasePackageFile);
|
||||
}
|
||||
|
||||
string baseTempPath = null;
|
||||
string tempPath = null;
|
||||
|
||||
using (Utility.WithTempDirectory(out baseTempPath))
|
||||
using (Utility.WithTempDirectory(out tempPath)) {
|
||||
var baseTempInfo = new DirectoryInfo(baseTempPath);
|
||||
var tempInfo = new DirectoryInfo(tempPath);
|
||||
|
||||
using (var zf = new ZipFile(basePackage.ReleasePackageFile)) {
|
||||
zf.ExtractAll(baseTempInfo.FullName);
|
||||
}
|
||||
|
||||
using (var zf = new ZipFile(newPackage.ReleasePackageFile)) {
|
||||
zf.ExtractAll(tempInfo.FullName);
|
||||
}
|
||||
|
||||
// Collect a list of relative paths under 'lib' and map them
|
||||
// to their full name. We'll use this later to determine in
|
||||
// the new version of the package whether the file exists or
|
||||
// not.
|
||||
var baseLibFiles = baseTempInfo.GetAllFilesRecursively()
|
||||
.Where(x => x.FullName.ToLowerInvariant().Contains("lib" + Path.DirectorySeparatorChar))
|
||||
.ToDictionary(k => k.FullName.Replace(baseTempInfo.FullName, ""), v => v.FullName);
|
||||
|
||||
var newLibDir = tempInfo.GetDirectories().First(x => x.Name.ToLowerInvariant() == "lib");
|
||||
|
||||
newLibDir.GetAllFilesRecursively()
|
||||
.ForEach(libFile => createDeltaForSingleFile(libFile, tempInfo, baseLibFiles));
|
||||
|
||||
ReleasePackage.addDeltaFilesToContentTypes(tempInfo.FullName);
|
||||
|
||||
using (var zf = new ZipFile(outputFile)) {
|
||||
zf.AddDirectory(tempInfo.FullName);
|
||||
zf.Save();
|
||||
}
|
||||
}
|
||||
|
||||
return new ReleasePackage(outputFile);
|
||||
}
|
||||
|
||||
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
|
||||
{
|
||||
Contract.Requires(deltaPackage != null);
|
||||
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
|
||||
|
||||
string workingPath;
|
||||
string deltaPath;
|
||||
|
||||
using (Utility.WithTempDirectory(out deltaPath))
|
||||
using (Utility.WithTempDirectory(out workingPath))
|
||||
using (var deltaZip = new ZipFile(deltaPackage.InputPackageFile))
|
||||
using (var baseZip = new ZipFile(basePackage.InputPackageFile)) {
|
||||
deltaZip.ExtractAll(deltaPath);
|
||||
baseZip.ExtractAll(workingPath);
|
||||
|
||||
var pathsVisited = new List<string>();
|
||||
|
||||
var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively()
|
||||
.Select(x => x.FullName.Replace(deltaPath + Path.DirectorySeparatorChar, ""))
|
||||
.ToArray();
|
||||
|
||||
// Apply all of the .diff files
|
||||
deltaPathRelativePaths
|
||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ForEach(file => {
|
||||
pathsVisited.Add(Regex.Replace(file, @".diff$", "").ToLowerInvariant());
|
||||
applyDiffToFile(deltaPath, file, workingPath);
|
||||
});
|
||||
|
||||
// Delete all of the files that were in the old package but
|
||||
// not in the new one.
|
||||
new DirectoryInfo(workingPath).GetAllFilesRecursively()
|
||||
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
|
||||
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("{0} was in old package but not in new one, deleting", x);
|
||||
File.Delete(Path.Combine(workingPath, x));
|
||||
});
|
||||
|
||||
// Update all the files that aren't in 'lib' with the delta
|
||||
// package's versions (i.e. the nuspec file, etc etc).
|
||||
deltaPathRelativePaths
|
||||
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ForEach(x => {
|
||||
this.Log().Info("Updating metadata file: {0}", x);
|
||||
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
|
||||
});
|
||||
|
||||
using (var zf = new ZipFile(outputFile)) {
|
||||
zf.AddDirectory(workingPath);
|
||||
zf.Save();
|
||||
}
|
||||
}
|
||||
|
||||
return new ReleasePackage(outputFile);
|
||||
}
|
||||
|
||||
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary<string, string> baseFileListing)
|
||||
{
|
||||
// NB: There are three cases here that we'll handle:
|
||||
//
|
||||
// 1. Exists only in new => leave it alone, we'll use it directly.
|
||||
// 2. Exists in both old and new => write a dummy file so we know
|
||||
// to keep it.
|
||||
// 3. Exists in old but changed in new => create a delta file
|
||||
//
|
||||
// The fourth case of "Exists only in old => delete it in new"
|
||||
// is handled when we apply the delta package
|
||||
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
|
||||
|
||||
if (!baseFileListing.ContainsKey(relativePath)) {
|
||||
this.Log().Info("{0} not found in base package, marking as new", relativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var oldData = File.ReadAllBytes(baseFileListing[relativePath]);
|
||||
var newData = File.ReadAllBytes(targetFile.FullName);
|
||||
|
||||
if (bytesAreIdentical(oldData, newData)) {
|
||||
this.Log().Info("{0} hasn't changed, writing dummy file", relativePath);
|
||||
|
||||
File.Create(targetFile.FullName + ".diff").Dispose();
|
||||
File.Create(targetFile.FullName + ".shasum").Dispose();
|
||||
targetFile.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName);
|
||||
using (var of = File.Create(targetFile.FullName + ".diff")) {
|
||||
BinaryPatchUtility.Create(oldData, newData, of);
|
||||
|
||||
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
|
||||
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
|
||||
targetFile.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory)
|
||||
{
|
||||
var inputFile = Path.Combine(deltaPath, relativeFilePath);
|
||||
var finalTarget = Path.Combine(workingDirectory, Regex.Replace(relativeFilePath, @".diff$", ""));
|
||||
|
||||
var tempTargetFile = Path.GetTempFileName();
|
||||
|
||||
// NB: Zero-length diffs indicate the file hasn't actually changed
|
||||
if (new FileInfo(inputFile).Length == 0) {
|
||||
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
|
||||
using (var of = File.OpenWrite(tempTargetFile))
|
||||
using (var inf = File.OpenRead(finalTarget)) {
|
||||
this.Log().Info("Applying Diff to {0}", relativeFilePath);
|
||||
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
|
||||
}
|
||||
|
||||
try {
|
||||
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
|
||||
} catch (Exception) {
|
||||
File.Delete(tempTargetFile);
|
||||
throw;
|
||||
}
|
||||
} else {
|
||||
using (var of = File.OpenWrite(tempTargetFile))
|
||||
using (var inf = File.OpenRead(inputFile)) {
|
||||
this.Log().Info("Adding new file: {0}", relativeFilePath);
|
||||
inf.CopyTo(of);
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(finalTarget)) File.Delete(finalTarget);
|
||||
|
||||
var targetPath = Directory.GetParent(finalTarget);
|
||||
if (!targetPath.Exists) targetPath.Create();
|
||||
|
||||
File.Move(tempTargetFile, finalTarget);
|
||||
}
|
||||
|
||||
void verifyPatchedFile(string relativeFilePath, string inputFile, string tempTargetFile)
|
||||
{
|
||||
var shaFile = Regex.Replace(inputFile, @"\.diff$", ".shasum");
|
||||
var expectedReleaseEntry = ReleaseEntry.ParseReleaseEntry(File.ReadAllText(shaFile, Encoding.UTF8));
|
||||
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
|
||||
|
||||
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
|
||||
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
|
||||
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
|
||||
throw new ChecksumFailedException() {Filename = relativeFilePath};
|
||||
}
|
||||
|
||||
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
|
||||
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
|
||||
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
|
||||
throw new ChecksumFailedException() {Filename = relativeFilePath};
|
||||
}
|
||||
}
|
||||
|
||||
bool bytesAreIdentical(byte[] oldData, byte[] newData)
|
||||
{
|
||||
if (oldData == null || newData == null) {
|
||||
return oldData == newData;
|
||||
}
|
||||
if (oldData.LongLength != newData.LongLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(long i = 0; i < newData.LongLength; i++) {
|
||||
if (oldData[i] != newData[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user