From ee5a7527f962475e11076aac613aba5ef41b3ce7 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Wed, 20 Mar 2024 10:01:02 +0000 Subject: [PATCH] finishing off symlink support in easyzip --- src/Velopack/Compression/EasyZip.cs | 45 ++++++++++++------------ src/Velopack/Internal/SymbolicLink.cs | 38 +++++++++++--------- test/Velopack.Tests/SymbolicLinkTests.cs | 14 +++++--- test/Velopack.Tests/ZipPackageTests.cs | 45 ++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/src/Velopack/Compression/EasyZip.cs b/src/Velopack/Compression/EasyZip.cs index faa5ef53..2bf3edf5 100644 --- a/src/Velopack/Compression/EasyZip.cs +++ b/src/Velopack/Compression/EasyZip.cs @@ -18,9 +18,20 @@ namespace Velopack.Compression { logger.Debug($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression..."); Utility.DeleteFileOrDirectoryHard(outputDirectory); + + List symlinks = new(); using (ZipArchive archive = ZipFile.Open(inputFile, ZipArchiveMode.Read)) { foreach (ZipArchiveEntry entry in archive.Entries) { - entry.ExtractRelativeToDirectory(outputDirectory, true); + if (entry.FullName.EndsWith(SYMLINK_EXT)) { + symlinks.Add(entry); + } else { + entry.ExtractRelativeToDirectory(outputDirectory, true); + } + } + + // process symlinks after, because creating them requires the target to exist + foreach (var sym in symlinks) { + sym.ExtractRelativeToDirectory(outputDirectory, true); } } } @@ -40,25 +51,17 @@ namespace Velopack.Compression if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, VelopackRuntimeInfo.PathStringComparison)) throw new IOException("IO_ExtractingResultsInOutside"); -#if NET5_0_OR_GREATER if (source.FullName.EndsWith(SYMLINK_EXT)) { // Handle symlink extraction fileDestinationPath = fileDestinationPath.Replace(SYMLINK_EXT, string.Empty); + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); using (var reader = new StreamReader(source.Open())) { var targetPath = reader.ReadToEnd(); - var isDir = targetPath.EndsWith(s_pathSeperator.ToString(), StringComparison.OrdinalIgnoreCase); - var absoluteTargetPath = Path.GetFullPath(Path.Combine(destinationDirectoryName, targetPath)); - var relativeTargetPath = Path.GetRelativePath(Path.GetDirectoryName(fileDestinationPath)!, absoluteTargetPath); - if (isDir) { - Directory.CreateSymbolicLink(fileDestinationPath, relativeTargetPath); - } else { - Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); - File.CreateSymbolicLink(fileDestinationPath, relativeTargetPath); - } + var absolute = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(fileDestinationPath)!, targetPath)); + SymbolicLink.Create(fileDestinationPath, absolute, true, true); } return; } -#endif if (Path.GetFileName(fileDestinationPath).Length == 0) { // If it is a directory: @@ -113,12 +116,10 @@ namespace Velopack.Compression foreach (var dir in directories) { cancelToken.ThrowIfCancellationRequested(); -#if NET5_0_OR_GREATER // if dir is a symlink, write it as a file containing path to target - if ((dir.Attributes & FileAttributes.ReparsePoint) != 0) { + if (SymbolicLink.Exists(dir.FullName)) { string entryName = EntryFromPath(dir.FullName, fullName.Length, dir.FullName.Length - fullName.Length); - var targetInfo = Directory.ResolveLinkTarget(dir.FullName, true); - string symlinkTarget = Path.GetRelativePath(sourceDirectoryName, targetInfo!.FullName) + string symlinkTarget = SymbolicLink.GetTarget(dir.FullName, relative: true) .Replace(Path.DirectorySeparatorChar, s_pathSeperator) + s_pathSeperator; var entry = zipArchive.CreateEntry(entryName + SYMLINK_EXT); using (var writer = new StreamWriter(entry.Open())) { @@ -126,7 +127,7 @@ namespace Velopack.Compression } continue; } -#endif + // if directory is empty, write it as an empty entry ending in s_pathSeperator if (IsDirEmpty(dir)) { string entryName = EntryFromPath(dir.FullName, fullName.Length, dir.FullName.Length - fullName.Length); @@ -147,19 +148,17 @@ namespace Velopack.Compression int length = fileInfo.FullName.Length - fullName.Length; string entryName = EntryFromPath(fileInfo.FullName, fullName.Length, length); -#if NET5_0_OR_GREATER - if ((fileInfo.Attributes & FileAttributes.ReparsePoint) != 0) { + if (SymbolicLink.Exists(fileInfo.FullName)) { // Handle symlink: Store the symlink target instead of its content - var targetInfo = File.ResolveLinkTarget(fileInfo.FullName, true); - string symlinkTarget = Path.GetRelativePath(sourceDirectoryName, targetInfo!.FullName) - .Replace(Path.DirectorySeparatorChar, s_pathSeperator); + 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; } -#endif + // Regular file handling var sourceFileName = fileInfo.FullName; using Stream stream = File.Open(sourceFileName, FileMode.Open, FileAccess.Read, FileShare.Read); diff --git a/src/Velopack/Internal/SymbolicLink.cs b/src/Velopack/Internal/SymbolicLink.cs index d71bf44a..32536c11 100644 --- a/src/Velopack/Internal/SymbolicLink.cs +++ b/src/Velopack/Internal/SymbolicLink.cs @@ -84,29 +84,35 @@ namespace Velopack /// Get the target of a junction point or symlink. /// /// The location of the symlink or junction point - /// If true, will return the full path to the target. - /// If false, will return the link target unadulterated - so it may be a - /// relative or an absolute path. - public static string GetTarget(string linkPath, bool resolve = true) + /// If true, the returned target path will be relative to the linkPath. Otherwise, it will be an absolute path. + public static string GetTarget(string linkPath, bool relative = false) { - if (TryGetLinkFsi(linkPath, out var fsi)) { - string target; -#if NETFRAMEWORK - - target = GetTargetWin32(linkPath); -#else - target = fsi.LinkTarget!; -#endif - if (!resolve) return target; - + var target = GetUnresolvedTarget(linkPath); + if (relative) { + if (Path.IsPathRooted(target)) { + return GetRelativePath(Path.GetDirectoryName(linkPath)!, target); + } else { + return target; + } + } else { if (Path.IsPathRooted(target)) { - // if the path is absolute, we can return it as is. return Path.GetFullPath(target); } else { - // if it is a relative path, we need to resolve it as it relates to the location of linkPath return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(linkPath)!, target)); } } + } + + private static string GetUnresolvedTarget(string linkPath) + { + if (TryGetLinkFsi(linkPath, out var fsi)) { +#if NETFRAMEWORK + + return GetTargetWin32(linkPath); +#else + return fsi.LinkTarget!; +#endif + } throw new IOException("Path does not exist or is not a junction point / symlink."); } diff --git a/test/Velopack.Tests/SymbolicLinkTests.cs b/test/Velopack.Tests/SymbolicLinkTests.cs index ba437929..899466cf 100644 --- a/test/Velopack.Tests/SymbolicLinkTests.cs +++ b/test/Velopack.Tests/SymbolicLinkTests.cs @@ -95,16 +95,22 @@ public class SymbolicLinkTests var tmpFile = Path.Combine(tempFolder, "AFile"); var symFile1 = Path.Combine(tempFolder, "SymFile"); var symFile2 = Path.Combine(subDir, "SymFile2"); + var symFile3 = Path.Combine(subDir, "SymFile3"); File.WriteAllText(tmpFile, "Hello!"); SymbolicLink.Create(symFile1, tmpFile, relative: true); SymbolicLink.Create(symFile2, tmpFile, relative: true); + SymbolicLink.Create(symFile3, tmpFile, relative: false); Assert.Equal("Hello!", File.ReadAllText(symFile1)); Assert.Equal("Hello!", File.ReadAllText(symFile2)); - Assert.Equal("AFile", SymbolicLink.GetTarget(symFile1, resolve: false)); - Assert.Equal("..\\AFile", SymbolicLink.GetTarget(symFile2, resolve: false)); + Assert.Equal("AFile", SymbolicLink.GetTarget(symFile1, relative: true)); + Assert.Equal("..\\AFile", SymbolicLink.GetTarget(symFile2, relative: true)); + Assert.Equal("..\\AFile", SymbolicLink.GetTarget(symFile3, relative: true)); + Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile1, relative: false)); + Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile2, relative: false)); + Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile3, relative: false)); Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile1)); Assert.Equal(tmpFile, SymbolicLink.GetTarget(symFile2)); @@ -129,8 +135,8 @@ public class SymbolicLinkTests Assert.Equal(subSubDir, SymbolicLink.GetTarget(sym2)); Assert.Equal(subDir2, SymbolicLink.GetTarget(sym1)); - Assert.Equal("..\\..\\SubDir2", SymbolicLink.GetTarget(sym1, resolve: false)); - Assert.Equal("SubDir\\SubSub", SymbolicLink.GetTarget(sym2, resolve: false)); + Assert.Equal("..\\..\\SubDir2", SymbolicLink.GetTarget(sym1, relative: true)); + Assert.Equal("SubDir\\SubSub", SymbolicLink.GetTarget(sym2, relative: true)); } [Fact] diff --git a/test/Velopack.Tests/ZipPackageTests.cs b/test/Velopack.Tests/ZipPackageTests.cs index 4a4dd4da..4ef3a293 100644 --- a/test/Velopack.Tests/ZipPackageTests.cs +++ b/test/Velopack.Tests/ZipPackageTests.cs @@ -8,6 +8,51 @@ namespace Velopack.Tests; public class ZipPackageTests { + private readonly ITestOutputHelper _output; + public ZipPackageTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void EazyZipPreservesSymlinks() + { + using var logger = _output.BuildLoggerFor(); + + using var _1 = Utility.GetTempDirectory(out var tempDir); + using var _2 = Utility.GetTempDirectory(out var zipDir); + using var _3 = Utility.GetTempDirectory(out var extractedDir); + + var actual = Path.Combine(tempDir, "actual"); + var actualFile = Path.Combine(actual, "file.txt"); + + var other = Path.Combine(tempDir, "other"); + var symlink = Path.Combine(other, "syml"); + var symfile = Path.Combine(other, "sym.txt"); + var zipFile = Path.Combine(zipDir, "test.zip"); + + Directory.CreateDirectory(actual); + Directory.CreateDirectory(other); + File.WriteAllText(actualFile, "hello"); + SymbolicLink.Create(symlink, actual); + SymbolicLink.Create(symfile, actualFile); + + Compression.EasyZip.CreateZipFromDirectoryAsync(logger, zipFile, tempDir).GetAwaiterResult(); + Compression.EasyZip.ExtractZipToDirectory(logger, zipFile, extractedDir); + + Assert.True(File.Exists(Path.Combine(extractedDir, "actual", "file.txt"))); + Assert.Equal("hello", File.ReadAllText(Path.Combine(extractedDir, "actual", "file.txt"))); + Assert.False(SymbolicLink.Exists(Path.Combine(extractedDir, "actual", "file.txt"))); + + Assert.True(Directory.Exists(Path.Combine(extractedDir, "other", "syml"))); + Assert.True(File.Exists(Path.Combine(extractedDir, "other", "sym.txt"))); + Assert.Equal("hello", File.ReadAllText(Path.Combine(extractedDir, "other", "sym.txt"))); + Assert.True(SymbolicLink.Exists(Path.Combine(extractedDir, "other", "syml"))); + Assert.True(SymbolicLink.Exists(Path.Combine(extractedDir, "other", "sym.txt"))); + + Assert.Equal("..\\actual\\file.txt", SymbolicLink.GetTarget(Path.Combine(extractedDir, "other", "sym.txt"), relative: true)); + } + [Fact] public void HasSameFilesAndDependenciesAsPackaging() {