diff --git a/src/lib-csharp/Compression/EasyZip.cs b/src/lib-csharp/Compression/EasyZip.cs index 814296d8..1da14aca 100644 --- a/src/lib-csharp/Compression/EasyZip.cs +++ b/src/lib-csharp/Compression/EasyZip.cs @@ -61,8 +61,10 @@ namespace Velopack.Compression if (!Utility.IsFileInDirectory(absolute, destinationDirectoryName)) { throw new IOException("IO_SymlinkTargetNotInDirectory"); } + SymbolicLink.Create(fileDestinationPath, absolute, true, true); } + return; } @@ -94,6 +96,7 @@ namespace Velopack.Compression await DeterministicCreateFromDirectoryAsync(directoryToCompress, outputFile, compressionLevel, progress, cancelToken).ConfigureAwait(false); } catch { try { File.Delete(outputFile); } catch { } + throw; } } @@ -101,7 +104,8 @@ namespace Velopack.Compression private static char s_pathSeperator = '/'; public static readonly DateTime ZipFormatMinDate = new DateTime(1980, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName, CompressionLevel compressionLevel, + private static async Task DeterministicCreateFromDirectoryAsync(string sourceDirectoryName, string destinationArchiveFileName, + CompressionLevel compressionLevel, Action progress, CancellationToken cancelToken) { Encoding entryNameEncoding = Encoding.UTF8; @@ -114,20 +118,19 @@ namespace Velopack.Compression long totalBytes = directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length); long processedBytes = 0L; - var directories = directoryInfo - .EnumerateDirectories("*", SearchOption.AllDirectories) - .Concat(new[] { directoryInfo }) - .OrderBy(f => f.FullName) - .ToArray(); + var dirsToProcess = new Stack(); + dirsToProcess.Push(directoryInfo); - foreach (var dir in directories) { + while (dirsToProcess.Count > 0) { cancelToken.ThrowIfCancellationRequested(); + var dir = dirsToProcess.Pop(); // if dir is a symlink, write it as a file containing path to target if (SymbolicLink.Exists(dir.FullName)) { if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(dir.FullName, relative: false), sourceDirectoryName)) { throw new IOException("IO_SymlinkTargetNotInDirectory"); } + string entryName = EntryFromPath(dir.FullName, fullName.Length, dir.FullName.Length - fullName.Length); string symlinkTarget = SymbolicLink.GetTarget(dir.FullName, relative: true) .Replace(Path.DirectorySeparatorChar, s_pathSeperator) + s_pathSeperator; @@ -135,6 +138,7 @@ namespace Velopack.Compression using (var writer = new StreamWriter(entry.Open())) { await writer.WriteAsync(symlinkTarget).ConfigureAwait(false); } + continue; } @@ -146,7 +150,12 @@ namespace Velopack.Compression continue; } - // if none of the above, enumerate files and add them to the archive + // if none of the above, this is just a regular folder - so we'll enumerate dirs and add them to the search stack and + // enumerate files and add them to the archive + foreach (var subdir in dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) { + dirsToProcess.Push(subdir); + } + var files = dir .EnumerateFiles("*", SearchOption.TopDirectoryOnly) .OrderBy(f => f.FullName) @@ -163,12 +172,14 @@ namespace Velopack.Compression if (!Utility.IsFileInDirectory(SymbolicLink.GetTarget(fileInfo.FullName, relative: false), sourceDirectoryName)) { throw new IOException("IO_SymlinkTargetNotInDirectory"); } + string symlinkTarget = SymbolicLink.GetTarget(fileInfo.FullName, relative: true) .Replace(Path.DirectorySeparatorChar, s_pathSeperator); var entry = zipArchive.CreateEntry(entryName + SYMLINK_EXT); using (var writer = new StreamWriter(entry.Open())) { await writer.WriteAsync(symlinkTarget).ConfigureAwait(false); } + continue; } @@ -225,4 +236,4 @@ namespace Velopack.Compression return true; } } -} +} \ No newline at end of file diff --git a/test/Velopack.Tests/SymbolicLinkTests.cs b/test/Velopack.Tests/SymbolicLinkTests.cs index 4939ae72..7ea003c9 100644 --- a/test/Velopack.Tests/SymbolicLinkTests.cs +++ b/test/Velopack.Tests/SymbolicLinkTests.cs @@ -1,4 +1,7 @@ -namespace Velopack.Tests; +using System.IO.Compression; +using Velopack.Compression; + +namespace Velopack.Tests; public class SymbolicLinkTests { @@ -220,4 +223,45 @@ public class SymbolicLinkTests Assert.Throws(() => SymbolicLink.Delete(Path.Combine(tempFolder, "AFile"))); } + + [Fact] + public async Task ComplexSymlinkDirGetsZippedCorrectly() + { + using var _1 = Utility.GetTempDirectory(out var tempFolder); + var temp = new DirectoryInfo(tempFolder); + var versions = temp.CreateSubdirectory("Versions"); + var a = versions.CreateSubdirectory("A"); + var resources = a.CreateSubdirectory("Resources"); + File.WriteAllText(Path.Combine(resources.FullName, "Info.plist"), "Hello, Resources!"); + File.WriteAllText(Path.Combine(a.FullName, "App"), "Hello, App!"); + SymbolicLink.Create(Path.Combine(versions.FullName, "Current"), a.FullName, false, true); + SymbolicLink.Create(Path.Combine(temp.FullName, "Resources"), Path.Combine(versions.FullName, "Current", "Resources"), false, true); + SymbolicLink.Create(Path.Combine(temp.FullName, "App"), Path.Combine(versions.FullName, "Current", "App"), false, true); + + using var _2 = Utility.GetTempDirectory(out var tempOutput); + var output = Path.Combine(tempOutput, "output.zip"); + + await EasyZip.CreateZipFromDirectoryAsync(NullLogger.Instance, output, tempFolder); + ZipFile.ExtractToDirectory(output, tempOutput); + + var appSym = Path.Combine(tempOutput, "App.__symlink"); + Assert.True(File.Exists(appSym)); + Assert.Equal("Versions/Current/App", File.ReadAllText(appSym)); + + var resSym = Path.Combine(tempOutput, "Resources.__symlink"); + Assert.True(File.Exists(resSym)); + Assert.Equal("Versions/Current/Resources/", File.ReadAllText(resSym)); + + Assert.True(Directory.Exists(Path.Combine(tempOutput, "Versions"))); + Assert.False(Directory.Exists(Path.Combine(tempOutput, "App"))); + Assert.False(Directory.Exists(Path.Combine(tempOutput, "Resources"))); + + Assert.True(Directory.Exists(Path.Combine(tempOutput, "Versions", "A"))); + Assert.False(Directory.Exists(Path.Combine(tempOutput, "Versions", "Current"))); + Assert.False(File.Exists(Path.Combine(tempOutput, "Versions", "Current"))); + + var currentSym = Path.Combine(tempOutput, "Versions", "Current.__symlink"); + Assert.True(File.Exists(currentSym)); + Assert.Equal("A/", File.ReadAllText(currentSym)); + } } \ No newline at end of file