Add async/progress zip compression

This commit is contained in:
Caelan Sayler
2024-01-03 19:01:30 +00:00
parent 8ebc959bc9
commit 04d2a5a4e9
2 changed files with 102 additions and 47 deletions

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Velopack.Compression
@@ -17,22 +18,30 @@ namespace Velopack.Compression
ZipFile.ExtractToDirectory(inputFile, outputDirectory);
}
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress, bool deterministic = true)
public static void CreateZipFromDirectory(ILogger logger, string outputFile, string directoryToCompress, Action<int> progress = null)
{
progress ??= (x => { });
logger.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
if (deterministic) {
DeterministicCreateFromDirectory(directoryToCompress, outputFile, null, false, Encoding.UTF8);
// we have stopped using ZipFile so we can add async and determinism.
// ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
DeterministicCreateFromDirectory(directoryToCompress, outputFile, null, false, Encoding.UTF8, progress);
}
} else {
ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
}
public static async Task CreateZipFromDirectoryAsync(ILogger logger, string outputFile, string directoryToCompress, Action<int> progress = null)
{
progress ??= (x => { });
logger.Info($"Compressing '{directoryToCompress}' to '{outputFile}' using System.IO.Compression...");
// we have stopped using ZipFile so we can add async and determinism.
// ZipFile.CreateFromDirectory(directoryToCompress, outputFile);
await DeterministicCreateFromDirectoryAsync(directoryToCompress, outputFile, null, false, Encoding.UTF8, progress).ConfigureAwait(false);
}
private static char s_pathSeperator = '/';
private static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static void DeterministicCreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel? compressionLevel, bool includeBaseDirectory, Encoding entryNameEncoding)
private static void DeterministicCreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel? compressionLevel, bool includeBaseDirectory, Encoding entryNameEncoding, Action<int> progress)
{
sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
destinationArchiveFileName = Path.GetFullPath(destinationArchiveFileName);
@@ -49,17 +58,74 @@ namespace Velopack.Compression
.OrderBy(f => f.FullName)
.ToArray();
foreach (FileSystemInfo item in files) {
for (var i = 0; i < files.Length; i++) {
var item = files[i];
flag = false;
int length = item.FullName.Length - fullName.Length;
string text = EntryFromPath(item.FullName, fullName.Length, length);
if (item is FileInfo) {
DoCreateEntryFromFile(zipArchive, item.FullName, text, compressionLevel);
var sourceFileName = item.FullName;
var entryName = text;
using Stream stream = File.Open(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
ZipArchiveEntry zipArchiveEntry = zipArchive.CreateEntry(entryName);
zipArchiveEntry.LastWriteTime = ZipFormatMinDate;
using (Stream destination2 = zipArchiveEntry.Open()) {
stream.CopyTo(destination2);
}
} else if (item is DirectoryInfo possiblyEmptyDir && IsDirEmpty(possiblyEmptyDir)) {
var entry = zipArchive.CreateEntry(text + s_pathSeperator);
entry.LastWriteTime = ZipFormatMinDate;
}
progress((int) ((double) i / files.Length * 100));
}
if (includeBaseDirectory && flag) {
string text = EntryFromPath(directoryInfo.Name, 0, directoryInfo.Name.Length);
var entry = zipArchive.CreateEntry(text + s_pathSeperator);
entry.LastWriteTime = ZipFormatMinDate;
}
}
private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel? compressionLevel, bool includeBaseDirectory, Encoding entryNameEncoding, Action<int> progress)
{
sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
destinationArchiveFileName = Path.GetFullPath(destinationArchiveFileName);
using ZipArchive zipArchive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create, entryNameEncoding);
bool flag = true;
DirectoryInfo directoryInfo = new DirectoryInfo(sourceDirectoryName);
string fullName = directoryInfo.FullName;
if (includeBaseDirectory && directoryInfo.Parent != null) {
fullName = directoryInfo.Parent.FullName;
}
var files = directoryInfo
.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)
.OrderBy(f => f.FullName)
.ToArray();
for (var i = 0; i < files.Length; i++) {
var item = files[i];
flag = false;
int length = item.FullName.Length - fullName.Length;
string text = EntryFromPath(item.FullName, fullName.Length, length);
if (item is FileInfo) {
var sourceFileName = item.FullName;
var entryName = text;
using Stream stream = File.Open(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
ZipArchiveEntry zipArchiveEntry = zipArchive.CreateEntry(entryName);
zipArchiveEntry.LastWriteTime = ZipFormatMinDate;
using (Stream destination2 = zipArchiveEntry.Open()) {
await stream.CopyToAsync(destination2).ConfigureAwait(false);
}
} else if (item is DirectoryInfo possiblyEmptyDir && IsDirEmpty(possiblyEmptyDir)) {
var entry = zipArchive.CreateEntry(text + s_pathSeperator);
entry.LastWriteTime = ZipFormatMinDate;
}
progress((int) ((double) i / files.Length * 100));
}
if (includeBaseDirectory && flag) {
@@ -101,31 +167,5 @@ namespace Velopack.Compression
return true;
}
internal static ZipArchiveEntry DoCreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel? compressionLevel)
{
if (destination == null) {
throw new ArgumentNullException("destination");
}
if (sourceFileName == null) {
throw new ArgumentNullException("sourceFileName");
}
if (entryName == null) {
throw new ArgumentNullException("entryName");
}
using Stream stream = File.Open(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
ZipArchiveEntry zipArchiveEntry = (compressionLevel.HasValue ? destination.CreateEntry(entryName, compressionLevel.Value) : destination.CreateEntry(entryName));
zipArchiveEntry.LastWriteTime = ZipFormatMinDate;
using (Stream destination2 = zipArchiveEntry.Open()) {
stream.CopyTo(destination2);
}
return zipArchiveEntry;
}
}
}

View File

@@ -166,7 +166,21 @@ namespace Velopack
public virtual async Task DownloadUpdatesAsync(
UpdateInfo updates, Action<int> progress = null, bool ignoreDeltas = false, CancellationToken cancelToken = default)
{
progress ??= (_ => { });
// the progress delegate may very likely invoke into the client main thread for UI updates, so
// let's try to reduce the spam. report only on even numbers and only if the progress has changed.
int lastProgress = 0;
void reportProgress(int x)
{
int result = (int) (Math.Round(x / 2d, MidpointRounding.AwayFromZero) * 2d);
if (result != lastProgress) {
lastProgress = result;
progress(result);
}
}
var targetRelease = updates?.TargetFullRelease;
if (targetRelease == null) {
throw new ArgumentException("Must pass a valid UpdateInfo object with a non-null TargetFullRelease", nameof(updates));
@@ -210,18 +224,19 @@ namespace Velopack
Log.Warn("No base package available. Attempting delta update using application files.");
Utility.CopyFiles(Locator.AppContentDir, deltaStagingDir);
}
progress(10);
await DownloadAndApplyDeltaUpdates(deltaStagingDir, updates, x => progress(Utility.CalculateProgress(x, 10, 90)))
reportProgress(10);
await DownloadAndApplyDeltaUpdates(deltaStagingDir, updates, x => reportProgress(Utility.CalculateProgress(x, 10, 80)))
.ConfigureAwait(false);
progress(90);
reportProgress(80);
Log.Info("Delta updates completed, creating final update package.");
File.Delete(incompleteFile);
EasyZip.CreateZipFromDirectory(Log, incompleteFile, deltaStagingDir);
await EasyZip.CreateZipFromDirectoryAsync(Log, incompleteFile, deltaStagingDir, x => reportProgress(Utility.CalculateProgress(x, 80, 100)))
.ConfigureAwait(false);
File.Delete(completeFile);
File.Move(incompleteFile, completeFile);
Log.Info("Delta release preparations complete. Package moved to: " + completeFile);
progress(100);
reportProgress(100);
return; // success!
}
}
@@ -235,13 +250,13 @@ namespace Velopack
Log.Info($"Downloading full release ({targetRelease.OriginalFilename})");
File.Delete(incompleteFile);
await Source.DownloadReleaseEntry(targetRelease, incompleteFile, progress).ConfigureAwait(false);
await Source.DownloadReleaseEntry(targetRelease, incompleteFile, reportProgress).ConfigureAwait(false);
Log.Info("Verifying package checksum...");
VerifyPackageChecksum(targetRelease, incompleteFile);
File.Delete(completeFile);
File.Move(incompleteFile, completeFile);
Log.Info("Full release download complete. Package moved to: " + completeFile);
progress(100);
reportProgress(100);
} finally {
if (VelopackRuntimeInfo.IsWindows) {
try {
@@ -252,11 +267,11 @@ namespace Velopack
if (zip.UpdateExeBytes == null) {
Log.Error("Update.exe not found in package, skipping extraction.");
} else {
#if NET5_0_OR_GREATER
await Utility.RetryAsync(() => File.WriteAllBytesAsync(updateExe, zip.UpdateExeBytes)).ConfigureAwait(false);
#else
Utility.Retry(() => File.WriteAllBytes(updateExe, zip.UpdateExeBytes));
#endif
await Utility.RetryAsync(async () => {
using var ms = new MemoryStream(zip.UpdateExeBytes);
using var fs = File.Create(updateExe);
await ms.CopyToAsync(fs).ConfigureAwait(false);
}).ConfigureAwait(false);
}
} catch (Exception ex) {
Log.Error(ex, "Failed to extract new Update.exe");