-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposed API for symbolic links #24271
Comments
Adding support for symbolic links is particularly important now that Windows has changed the default permissions for creating them. (In the Fall Creator's Update you no longer need to be elevated to create them) One of my concerns is how we introduce support for junctions, mounts, or possibly Mac aliases. Would we want to be more generic here, like namespace System.IO
{
public static class Path
{
// Or perhaps GetKnownLinkType()?
public static LinkType GetLinkType(string path);
public void CreateSymbolicLink(string linkPath, string targetPath);
}
public enum LinkType
{
None,
Symbolic,
Junction
}
} |
As far as I know, symbolic links are the only link type that is available on all platforms that .NET Core targets, which makes them more useful than other link types. I don't see a lot of value in supporting other link types, but perhaps I'm just being short-sighted. Windows differentiates between file and directory symbolic links, so you can't just have The |
I'm not expecting creation to become a priority, but identifying Junctions is important (over all the other reparse points, those two need special treatment when enumerating).
I didn't even realize that. Is that case useful to support?
I would think "link to" makes more sense. Creating a FileInfo for the target seems backwards from the way I'd expect people to come at this. |
If I'm creating an arbitrary directory structure with some links in it, I wouldn't want to have to figure out what order I need to create the file and directories in order to make it work. Unzipping a .zip file with symbolic links in it, for example. (That's hypothetical at this point though — .zip probably doesn't store whether the symlink is to a directory or a file.)
Maybe I'm confused. I would expect that:
|
How does PowerShell handle links? |
@carlreinke Sorry I haven't responded, I've been out on an extended vacation. @jhudsoncedaron is also interested in this area and has opened #24655. We should leverage the interest here and try and get a strong proposal together that we can easily clear through API review.
Agreed, thanks for the example.
Sorry, terminology is a little weird and I think I was confusing myself. "Create a symbolic link to me" is what I meant, which is what you're describing I think. |
I hate to disappoint you guys but "create a directory structure with some links in it" is a pill on Windows because Windows decided to demand knowing whether the target is a file or a directory at create time. |
Yeah, not sure why that is- I'll take a look and see if I can find any clues, but I suspect it has some compelling legacy reason that no longer applies. |
I'm marking this one ready for review with one tweak: public class DirectoryInfo
{
// Create a symbolic link at the given path to this directory info
public void CreateSymbolicLink( string path );
}
public class FileInfo
{
// Create a symbolic link at the given path to this file info
public void CreateSymbolicLink( string path );
} Info classes can be created for non-existent paths. We will allow creating symbolic links at the given path regardless of the info existence if the OS/FileSystem allows it. |
It seems to me that it's inconsistent if
(And similarly for |
@carlreinke : There's a bunch of other problems with this API scheme leading me to have to abandon hope of correcting it. |
@carlreinke I don't think it's a problem, but I'll bring it up at the review. I think it is more of an issue to have
@jhudsoncedaron Can you please articulate issues with these specific additional API's here so we can consider them. I spoke with @pjanotti and we believe that we should be using I'm updating the main comment to reflect the discussion. |
@JeremyKuhne : It expects the caller to follow links one at a time. To work correctly, the caller must pass an argument to the constructor that specifies whether to get information about the link or the target of the link. It is context sensitive which one the caller would want. Also, the results of getting one needs to know if its some strange type of file node or not. We probably don't need to handle fifos, block specials, etc. but we at least need to allow directory walking algorithms to know if they encountered one so they can skip over it. |
I'm fine with
I don't see why. We currently don't have a constructor that says to follow to the end so they're always information about the given path. You create a file, you have an That said I think it is probably worth adding a convenience constructor that follows links to the final path. Not having it initially, however, shouldn't block this from moving forward. The biggest issue right now is that we have no way to deal with links. Getting the basics in place for the next release to unblock people is super important I think. That window is rapidly closing. |
How many times do I have to tell you that won't work? Recursively reading the link is not the same as asking the OS for the link target information. It's the difference between
So add one. |
Could you please provide supporting links and/or some code to clarify? I don't see why recursively getting target paths for symbolic links wouldn't give you an actual file path and I'm struggling to find supporting material. I'll continue to spend time in the near term investigating the various APIs and looking at the implementation internals. |
This file really does exist on *nix systems and can be opened if you have a handle to it. But it can't be resolved by
This is not the only example, just the easiest found. When you consider that your recursive symbolic link descent doesn't match the kernel's and when you throw in things like sshfs it's quickly best to go ahead and just assume that |
Thanks, I'll play around with this a bit more and respond to the thread. |
Why does one get ~$ ls -l /dev/stdin
lrwxrwxrwx 1 root root 15 Jan 18 15:15 /dev/stdin -> /proc/self/fd/0
~$ ls -l /proc/self/fd/0
lrwx------ 1 jeremy jeremy 0 Jan 18 15:29 /proc/self/fd/0 -> /dev/tty1
~$ ls -l /dev/tty1
crw-rw---- 1 jeremy tty 4, 1 Jan 18 15:15 /dev/tty1
~$ readlink /dev/stdin
/proc/self/fd/0
~$ readlink /proc/self/fd/0
/dev/tty1
~$ readlink /dev/tty1
~$ readlink -f /dev/stdin
/dev/tty1 Working with any of the intermediate paths seems to work fine. |
Because the link is actually to the file by direct reference, and the path displayed is merely the path used to open the file when it was opened (i.e. potentially stale (file moved or deleted out from under you), wrong namespace ( Paths in /proc are often passed from process to process to prevent somebody getting way too clever and finding a way to hijack them. This only works because of the fact that /proc symbolic links are directly resolved. This results in the file that was intended even if somebody else renames it out of the way. Therefore, if the API tries to do We also note that any userspace virtual filesystem can choose to do this. |
I don't understand how your example shows that walking can't be done. Running |
One of the tests shows that Also, consider the following C#:
If this were written as a test case it should pass. |
But where would
In the other proposal
FileInfo fi = new FileInfo(@"F:\test\links\e");
using (var tempfile = new FileStream(fi.FullName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Delete | FileShare.ReadWrite))
{
File.Delete(fi.FullName);
using (var fs = new FileStream(@"F:\test\links\a", FileMode.Open, FileAccess.Read)) { }
using (var fs = new FileStream(@"F:\test\links\b", FileMode.Open, FileAccess.Read)) { }
using (var fs = new FileStream(@"F:\test\links\c", FileMode.Open, FileAccess.Read)) { }
// Target still exists, but can't be deleted
fi.Refresh();
Console.WriteLine($"File {(fi.Exists ? "exists" : "doesn't exist")}.");
// Access denied if the file is deleted and all of the handles aren't closed
// using (var fs = new FileStream(@"F:\test\links\d", FileMode.Open, FileAccess.Read)) { }
}
// Target will no longer exist
fi.Refresh();
Console.WriteLine($"File {(fi.Exists ? "exists" : "doesn't exist")}."); I don't think that you can create a handle on a file marked for deletion on Windows. I might be remembering incorrectly, however. It is an interesting test case for link handling- I'll check how GetFinalPathNameByHandle/etc. deal with a handle opened on the link itself. |
You must be having trouble reproducing. In this case, stdin was a pipe handle where cat was piped to ls. Try
That's a property of NTFS. If you fix your share modes to include FileShareDelete in all those opens and try it with a UNC path to a set-top NAS box it will work (or the path might go away immediately leaving you unable to open it). The general point being is |
It may be, but it is important. If we're suggesting you can find the state of the link target by opening a handle on it and you can't- we'll have to jump through some serious hoops. We already use FindFirstFile as a workaround for this issue in our code. I'm going to try to see if I can use GetFinalPathNameByHandle to implement the same hack- I'll respond back to the thread when I've finished looking at it. In any case, we can't rely on leaning too heavy on the link info as the only way to get it programmatically on Windows is through DeviceIoControl, which won't fly for WinRT at this point. I still want to expose the data where we can though. Providing access to file info through handles has been on my bucket list for some time. Providing a static method on Here is what I'm currently thinking: public class FileSystemInfo
{
public bool IsSymbolicLink { get; }
// The string value of the target for symbolic links
public string SymbolicLinkTarget { get; }
public void CreateSymbolicLink(string linkPath);
public static FileSystemInfo CreateFromHandle(SafeFileHandle handle);
}
public class FileInfo
{
public FileInfo(string fileName, FileSystemInfoFlags flags);
// this pointer or FileInfo on the target if this info is a SymbolicLink
public FileInfo TargetInfo { get; }
}
public class DirectoryInfo
{
public DirectoryInfo(string fileName, FileSystemInfoFlags flags);
// this pointer or DirectoryInfo on the target if this info is a SymbolicLink
public DirectoryInfo TargetInfo { get; }
}
[Flags]
public enum FileSystemInfoFlags
{
// The class will contain information about the final target of symbolic links
FollowSymbolicLinks = 0x1;
// Other options in future
} |
(assuming you mean because it has a pending delete) I was planning on having FileInfo return the status of "you don't have permission to resolve the link target" in this case, same as if the link target referred to a directory you can't traverse. I already use this technique on Windows native code to see through links. Oh that reminds me. The behavior of
Looks good to me. |
So on checking, my intention to follow the behavior for no permissions to directory for a link to a deleted file is actually great. If you're checking if the file exists because you want to prompt for overwrite If you want to do a file type decision ladder (is this a file or directory I was passed), If you want to perform a file open operation just open the file first, then if you don't pass FileShare.Delete you don't have to worry about the zombie state in the first place. |
This comment has been minimized.
This comment has been minimized.
@mklement0 It is |
Why do |
Although the hybrid/composite approach may look attractive, it seem is a breaking change and it would be a completely unacceptable. From a practical point of view it is better to have an API that does exactly what is expected and I wouldn't want to confuse the behavior even more. |
So on .NET 5, reading the times from a link give you the times on the link itself, but when you set them, they send up setting the targets? Yikes! It would be ideal to be (effectively) all hybrid behavior or none at all. (By "effectively" I mean: with the caveats like Delete affecting the link itself, which would be the expected behavior in a hardlink scenario, ignoring that most platforms disallow directory hardlinks). But I don't think we can go in the direction of all hybrid by default, given that that is a pretty significant breaking change. An opt-in option certainly would be possible. Going in the direction of non-hybrid by default is also a breaking change (from the timestamp setters). I would not be very concerned about fixing the timestamp setters. It is hard justify the current behavior as anything but a bug. While there may be a few users that would be adversely affected by fixing it, I would imagine just as many if not more users would effectively getting a minor bug fixed. (There seems to be a PR in progress for this: #52639). The whole situation here is complicated, since in many simple cases you really do only care about the the target of the symlink, and a fair few APIs and syscalls do a decent job of allowing you to ignore that something is a symlink. But in other cases it is critical that you know a path is a symlink. There have been security bugs in applications on multiple occasions resulting from confusing a symlink with a normal file or normal directory. This makes me want to lean in the direction of not trying to do additional clever things with symlinks by default beyond the normal "by default opening a symlink opens the target" (in both the file contents, and directory traversal sense of "opening"). |
If needing to modify the properties of a newly created symlink would be common, returning an object representing it would be justified.
Going all-hybrid by default:
The only unfortunate aspect of a hybrid representation is that in order to avoid a truly serious breaking change, the
And for that an
Excellent point, I'm sorry I missed that; what drew me to
Since
|
As MSFT team members said it is better to have separate types because there are a lot of reparse points on Windows (and new ones may appear on Unix) and I agreed with this. So we could introduce SymbolicLink(Info). In this case, this class should not have any hybrid behavior and work with the entity itself - change name, target, time, attributes. etc. Then some magical behavior of FileInfo/DirectoryInfo/FileSystemInfo would be more understandable (I still think we need IsSymbolicLink() there). For example, FileSystemInfo.GetTarget() could return SymbolicLinkInfo. |
Now that the APIs got approved, the discussion is now about additional/incremental improvements for which we already have issues open. So if you don't mind, and for clarity, let's continue the conversation in those issues:
|
Since you've added
As mentioned earlier, the user won't care about this return value. Consider making this return
I still wish this were a method. Not only for consistency with the other properties, but also because now
I don't understand why this is left out. |
It is the same as existing static API like Directory.CreateDirectory(). It makes no sense to use another pattern.
It is clear as this could be for symbolic links but we need to take into account Windows reparse point tags and in the case it is not yet clear how to present such API. |
Regarding an For the sake of completeness: another link-related proposal, in the context of file-system-item enumeration (opt-in/out of recursing into directory links), is #52666 |
I agree, yikes. The native APIs have the same gotchas. |
Yikes for sure, but, as @KevinCathcart has also noted, pending PRs #52639 and #52639 (both by @hamarb123) are planning to fix that. In other words: This makes As desirable as a mostly-hybrid default representation may be, @KevinCathcart has persuasively argued in #52908 (comment) that this would be a serious backward-compatibility concern, so an opt-in via a As for how the WinAPI functions handle symlinks - see Symbolic Link Effects on File Systems Functions. |
As I pointed in #52666 (comment)
|
Please check my comment in #1908 (comment). |
Thinking more about approved (#24271 (comment)) API I am sure it will work well in PowerShell. We need to make a conclusion about implementation of target resolving since ReparsePointTag list is open and new tags can be introduced:
|
So this doesn't trouble me anymore; I will be content to carry around my own static file provider as long as necessary; however a good test for your own symbolic link API would be fixing this: dotnet/aspnetcore#2774 ; the bug is due to calling new FileInfo() and getting back a length of 0 and not serving any data; should be a trivial fix if your symbolic link API is right. |
Edit by @carlossanlop: Revisited API Proposal
Original proposal:
Rationale
The ability to interact with symbolic links (symlinks) in .NET is currently limited to determining that a file is
ReparsePoint
. This proposed API provides the ability to identify, read, and create symbolic links.Proposed API
Details
The path returned from
GetSymbolicLinkTargetPath(string)
/SymbolicLinkTargetPath
will be returned exactly as it is stored in the symbolic link. It may reference a non-existent file or directory.For the purposes of this API, NTFS Junction Points are considered to be like Linux bind mounts and are not considered to be symbolic links.
Updates
GetSymbolicLinkTargetPath
andIsSymbolicLink
fromPath
toDirectory
,DirectoryInfo
,File
andFileInfo
.CreateSymbolicLink
.path
tolinkPath
where a link file's path is desired.FileSystemInfo
base class.The text was updated successfully, but these errors were encountered: