mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
finishing off symlink support in easyzip
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user