Repository navigation
FileInfo/DirectoryInfo: enable transparent handling of symbolic links #52908
Description
Background and Motivation
Users may not be aware that the fileName/path they are providing is a symbolic link.
This proposal makes it possible to operate on such path without the user having to manually handle symbolic links using the APIs added in #24271.
Proposed API
A new constructor on FileInfo allows to specify whether the instance should return information about the target instead of the symbolic link.
class FileInfo
{
public FileInfo(string fileName, bool followLinks) { }
}
class DirectoryInfo
{
public DirectoryInfo(string path, bool followLinks) { }
}The argument causes information for the link target to be returned for the following properties. When there is no target they throw no such file.
class FileInfo
{
public bool IsReadOnly { get { throw null; } set { } }
public long Length { get { throw null; } }
}
class FileSystemInfo
{
public FileAttributes Attributes { get { throw null; } set { } }
public DateTime CreationTime { get { throw null; } set { } }
public DateTime CreationTimeUtc { get { throw null; } set { } }
public abstract bool Exists { get; }
public DateTime LastAccessTime { get { throw null; } set { } }
public DateTime LastAccessTimeUtc { get { throw null; } set { } }
public DateTime LastWriteTime { get { throw null; } set { } }
public DateTime LastWriteTimeUtc { get { throw null; } set { } }
// from https://github.com/dotnet/runtime/issues/24271
// always returns `false` when `followLinks` is `true`.
public bool IsSymbolicLink { get; }
}It does not affect the following operations. Which are either:
- always working on the target, or
- working on the 'fileName'/'path' argument.
class DirectoryInfo
{
// applies to 'path':
public DirectoryInfo? Parent { get { throw null; } }
public DirectoryInfo Root { get { throw null; } }
// applies to 'target':
public void Create() { }
public DirectoryInfo CreateSubdirectory(string path) { throw null; }
public IEnumerable<DirectoryInfo> EnumerateDirectories() { throw null; }
public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern) { throw null; }
public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPattern, SearchOption searchOption) { throw null; }
public IEnumerable<FileInfo> EnumerateFiles() { throw null; }
public IEnumerable<FileInfo> EnumerateFiles(string searchPattern) { throw null; }
public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public IEnumerable<FileInfo> EnumerateFiles(string searchPattern, SearchOption searchOption) { throw null; }
public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos() { throw null; }
public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern) { throw null; }
public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public IEnumerable<FileSystemInfo> EnumerateFileSystemInfos(string searchPattern, SearchOption searchOption) { throw null; }
public DirectoryInfo[] GetDirectories() { throw null; }
public DirectoryInfo[] GetDirectories(string searchPattern) { throw null; }
public DirectoryInfo[] GetDirectories(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public DirectoryInfo[] GetDirectories(string searchPattern, SearchOption searchOption) { throw null; }
public FileInfo[] GetFiles() { throw null; }
public FileInfo[] GetFiles(string searchPattern) { throw null; }
public FileInfo[] GetFiles(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public FileInfo[] GetFiles(string searchPattern, SearchOption searchOption) { throw null; }
public FileSystemInfo[] GetFileSystemInfos() { throw null; }
public FileSystemInfo[] GetFileSystemInfos(string searchPattern) { throw null; }
public FileSystemInfo[] GetFileSystemInfos(string searchPattern, EnumerationOptions enumerationOptions) { throw null; }
public FileSystemInfo[] GetFileSystemInfos(string searchPattern, SearchOption searchOption) { throw null; }
// applies to 'path':
public void Delete(bool recursive) { }
public override void Delete() { }
public void MoveTo(string destDirName) { }
}
class FileInfo
{
// applies to 'fileName'
public DirectoryInfo? Directory { get { throw null; } }
public string? DirectoryName { get { throw null; } }
// applies to 'target':
public StreamWriter AppendText() { throw null; }
public FileInfo CopyTo(string destFileName) { throw null; }
public FileInfo CopyTo(string destFileName, bool overwrite) { throw null; }
public FileStream Create() { throw null; }
public StreamWriter CreateText() { throw null; }
public void Decrypt() { }
public void Encrypt() { }
public FileStream Open(FileMode mode) { throw null; }
public FileStream Open(FileMode mode, FileAccess access) { throw null; }
public FileStream Open(FileMode mode, FileAccess access, FileShare share) { throw null; }
public FileStream OpenRead() { throw null; }
public StreamReader OpenText() { throw null; }
public FileStream OpenWrite() { throw null; }
public FileInfo Replace(string destinationFileName, string? destinationBackupFileName) { throw null; }
public FileInfo Replace(string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) { throw null; }
// applies to 'path':
public override void Delete() { }
public void MoveTo(string destFileName) { }
public void MoveTo(string destFileName, bool overwrite) { }
}
class FileSystemInfo
{
// applies to 'fileName'/'path':
protected string FullPath;
protected string OriginalPath;
public string Extension { get { throw null; } }
public virtual string FullName { get { throw null; } }
public abstract string Name { get; }
}Usage Examples
FileInfo file = new FileInfo("/tmp/my-file", followLinks: true);
DateTime lastWriteTime = file.LastWriteTimeUtc;
while (true)
{
Thread.Sleep(1000);
file.Refresh();
DateTime writeTime = file.LastWriteTimeUtc;
if (writeTime != lastWriteTime)
{
Console.WriteLine("The file has changed");
lastWriteTime = writeTime;
}
}Implementation
On Linux, this means the information for the properties is retrieved using stat instead of vstat.
For Delete, the final target is first located, e.g. by calling realpath, and that is then deleted.
edits:
- added
IsSymbolicLinkfrom Proposed API for symbolic links #24271 - renamed
followLinktofollowLinks. - updated proposal to make
Deletetarget based. - added implementation section which covers Linux
- revert
Deleteto be path based again. - place
MoveTounder path-based
Activity
mklement0 commentedon May 19, 2021
tmds commentedon May 19, 2021
I don't think this is problematic. I think it is desired to not expose a symlink was involved.
The path that is passed to the constructor is treated as an invariant.
No such file exception.
Return information about the final target.
For Unix, it should be about using
statinstead oflstat. I don't think we needrealpath.I don't know about Windows.
iSazonov commentedon May 19, 2021
This seems less flexible and more confusing than #24271. Especially disturbing is that we set the behavior "before", but then we don't even know about it (i.e. if then there are two code paths, we can't choose the right one). In other words it's not so easy to implement the "find all symbolic links" scenario.
tmds commentedon May 19, 2021
@iSazonov this is about working with files and directories regardless whether the path is or is not using a symbolic link.
#24271 is about working with symbolic links.
These are different use-cases.
mklement0 commentedon May 19, 2021
tmds commentedon May 19, 2021
How should these properties behave when the path does not exist, or when the link is broken?
Should these properties update on
Refresh?I think it is valid for these properties to reflect the path that was passed to the constructor and maintain the behavior that has always existed here. Changing that can also cause problems.
If the final target is desired, maybe add a property for that. For example:
string RealPath { get; }(which can call realpath on Linux).They reflect the target independent of whether the path is a link or not.
mklement0 commentedon May 19, 2021
tmds commentedon May 19, 2021
So the constructor throws if the path does not exist, or the link is broken?
And Refresh would be performed against the target path?
I don't understand the problem.
followLinkis meant to make the properties reflect the target of the link, not the link itself.Some properties are proposed to keep matching the path that was passed to the constructor. There is no guessing.
mklement0 commentedon May 19, 2021
iSazonov commentedon May 20, 2021
It looks like an insidious trick :-) when some properties and methods return information about the source object and others about the target object. This approach opens up unlimited ways for bugs in applications.
This is all the more useless because most APIs already work transparently with symbolic links.
(And please add more use-cases in OP - your current demo says nothing.)
tmds commentedon May 20, 2021
I'd take the path as is, just like it happens now.
FileInfoconstructor doesn't throw if the path does not exist.@iSazonov you say this causes bugs. @mklement0 you call it problematic. Can you demonstrate issues with some code?
I propose to keep invariant that has existed between the constructor argument and properties. Changing those definitely creates opportunity for bugs with existing code.
I don't see the value of making these properties reflect the target path.
.Get[Link]Target(returnFinalTarget: true)can be used to get it.Except the ones that are part of this proposal.
Other example:
16 remaining items
jozkee commentedon Jul 23, 2021
I wonder if we could instead add APIs for querying link's target file/directory information and return that in a minimal wrapper (FileSystemEntry) and keep the APIs focused in doing as less allocations as possible.
We could have the following APIs:
Notes:
FileSystemEntryis ref struct and also is contained in theSystem.IO.Enumeraitonnamespace, therefore it may not be a good idea to use it as the return type.cc @ericstj @carlossanlop
ericstj commentedon Jul 23, 2021
Would it make sense to consider this scenario as an extension of
System.IO.Enumeraitonif that namespace is already dealing with this type of thing for non-symlinks?jozkee commentedon Jul 23, 2021
Do you mean if
FileSystemEntryalready returns the information of the link target? If so, it does not, it returns information for the link itself.ericstj commentedon Jul 23, 2021
I mean why not add link target support to the enumeration APIs.
jozkee commentedon Jul 23, 2021
We can add support for enumeration scenarios as well, I was thinkking in an instance method that returns a new
FileSystemEntryor returns the same instance if not a link:I don't know if we should care for the non-final target scenario.
iSazonov commentedon Jul 26, 2021
I think an enhancement of FileSystemEntry looks right direction as implementation details for both enumeration and explicit (sym)link manipulations. See my old comment #52666 (comment).
To resolve conflicts between properties (like LastAccessTimeUtc) of link itself and link target it would great to have
public class SymbolicLink.tmds commentedon Aug 17, 2021
My focus is to eliminate the system calls that hop links in
ResolveLinkTargetand perform a singlestatcall.For
FileInfoI don't think there are many allocations besides theFileInfoinstance itself.FileInfobeing a class makes it usable in more cases than aref struct.Using
FileInfoalso makes it easy to change existing code where the intent was to get info for the target rather than the link.System.IO.Enumerationmay also need improving/extending to better support links. So maybe both are warranted.dotnet-policy-service commentedon Feb 16, 2026
Due to lack of recent activity, this issue has been marked as a candidate for backlog cleanup. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will undo this process.
This process is part of our issue cleanup automation.
iSazonov commentedon Feb 17, 2026
This could be useful.