finishing off symlink support in easyzip

This commit is contained in:
Caelan Sayler
2024-03-20 10:01:02 +00:00
parent b2877fe4eb
commit ee5a7527f9
4 changed files with 99 additions and 43 deletions

View File

@@ -18,9 +18,20 @@ namespace Velopack.Compression
{
logger.Debug($"Extracting '{inputFile}' to '{outputDirectory}' using System.IO.Compression...");
Utility.DeleteFileOrDirectoryHard(outputDirectory);
List<ZipArchiveEntry> 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);

View File

@@ -84,29 +84,35 @@ namespace Velopack
/// Get the target of a junction point or symlink.
/// </summary>
/// <param name="linkPath">The location of the symlink or junction point</param>
/// <param name="resolve">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.</param>
public static string GetTarget(string linkPath, bool resolve = true)
/// <param name="relative">If true, the returned target path will be relative to the linkPath. Otherwise, it will be an absolute path.</param>
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.");
}

View File

@@ -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]

View File

@@ -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<ZipPackageTests>();
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()
{