Skip to content

FileInfo/DirectoryInfo: enable transparent handling of symbolic links #52908

Open
@tmds

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 IsSymbolicLink from Proposed API for symbolic links #24271
  • renamed followLink to followLinks.
  • updated proposal to make Delete target based.
  • added implementation section which covers Linux
  • revert Delete to be path based again.
  • place MoveTo under path-based

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.IOneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions