Skip to content
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

[API Proposal]: Add an option to create parent directories in System.IO.File APIs #110919

Open
Gnbrkm41 opened this issue Dec 23, 2024 · 1 comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO untriaged New issue has not been triaged by the area owner

Comments

@Gnbrkm41
Copy link
Contributor

Background and motivation

I find it a common task to create (then subsequently write into) a file, given a complete path to be saved.
For example, when given a list of relative paths to files, I might want to save all the files while preserving the directory hierarchy, which might look like this:

/foo/a/1/blah.txt -> C:\archive\foo\a\1\blah.txt
/foo/a/2/blah.txt -> C:\archive\foo\a\2\blah.txt
/foo/b/1/blah.txt -> ...
/foo/b/2/blah.txt -> ...
/bar/a/1/blah.txt -> ...

The APIs under the System.IO.File class is usually good enough for quick and dirty codes that involve simple read/write/creations of files (e.g., scripting)
However, one needs to make sure that all of the parent directories exist, otherwise they get hit with a DirectoryNotFoundException in the face when part of the directories are missing:
Image

Popular workaround this involves the use of FileInfo, accessing its Directory property to call Create which ensures that all the subdirectories leading to the file exists (and does nothing if they already exist)

string filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Some", "NonExistentFolder", "a.mp4");
(new FileInfo(filePath)).Directory.Create()
File.WriteAllText(filePath, text)

(See https://stackoverflow.com/questions/2955402/how-do-i-create-directory-if-it-doesnt-exist-to-create-a-file)
But to me this feels weird because I'm creating a FileInfo and a DirectoryInfo object which then I immediately throw away.

An alternative would be to create the directory without the FileInfo, which is also suggested in the above StackOverflow post:

Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllText(filePath, text)

Looking at libraries from different languages / frameworks, Node.js seems to have a popular file system library, fs-extra, that has a method called outputFile which creates the parent directory if it does not exist.

I feel like I'm running into this problem quite frequently in a lot of different places; Often when a file creation fails because of missing directories I just want it to create one. It sure would be nice if there is a way to make these methods create parent directories if it does not exist...

API Proposal

namespace System.IO;

public static class File
{
    // Existing APIs
    public static void AppendAllLines(string path, IEnumerable<string> contents);
    public static void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding);
    public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, CancellationToken cancellationToken = default);
    public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, CancellationToken cancellationToken = default);
    public static void AppendAllText(string path, string? contents);
    public static void AppendAllText(string path, string? contents, Encoding encoding);
    public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default);
    public static Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default);
    public static StreamWriter AppendText(string path);
    public static FileStream Create(string path, int bufferSize, FileOptions options);
    public static FileStream Create(string path);
    public static FileStream Create(string path, int bufferSize);
    public static StreamWriter CreateText(string path);
    public static FileStream OpenWrite(string path);
    public static void WriteAllBytes(string path, byte[] bytes);
    public static Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default);
    public static void WriteAllLines(string path, IEnumerable<string> contents);
    public static void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding);
    public static void WriteAllLines(string path, string[] contents);
    public static void WriteAllLines(string path, string[] contents, Encoding encoding);
    public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, CancellationToken cancellationToken = default);
    public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, CancellationToken cancellationToken = default);
    public static void WriteAllText(string path, string? contents);
    public static void WriteAllText(string path, string? contents, Encoding encoding);
    public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, CancellationToken cancellationToken = default);
    public static Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default);
    // New APIs
+    public static void AppendAllLines(string path, IEnumerable<string> contents, bool createDirectories);
+    public static void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories);
+    public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+    public static Task AppendAllLinesAsync(string path, IEnumerable<string> contents, bool createDirectories, CancellationToken cancellationToken = default);
+    public static void AppendAllText(string path, string? contents, bool createDirectories);
+    public static void AppendAllText(string path, string? contents, Encoding encoding, bool createDirectories);
+    public static Task AppendAllTextAsync(string path, string? contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+    public static Task AppendAllTextAsync(string path, string? contents, bool createDirectories, CancellationToken cancellationToken = default);
+    public static StreamWriter AppendText(string path, bool createDirectories);
+    public static FileStream Create(string path, int bufferSize, FileOptions options, bool createDirectories);
+    public static FileStream Create(string path, bool createDirectories);
+    public static FileStream Create(string path, int bufferSize, bool createDirectories);
+    public static StreamWriter CreateText(string path, bool createDirectories);
+    public static FileStream OpenWrite(string path, bool createDirectories);
+    public static void WriteAllBytes(string path, byte[] bytes, bool createDirectories);
+    public static Task WriteAllBytesAsync(string path, byte[] bytes, bool createDirectories, CancellationToken cancellationToken = default);
+    public static void WriteAllLines(string path, IEnumerable<string> contents, bool createDirectories);
+    public static void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories);
+    public static void WriteAllLines(string path, string[] contents, bool createDirectories);
+    public static void WriteAllLines(string path, string[] contents, Encoding encoding, bool createDirectories);
+    public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+    public static Task WriteAllLinesAsync(string path, IEnumerable<string> contents, bool createDirectories, CancellationToken cancellationToken = default);
+    public static void WriteAllText(string path, string? contents, bool createDirectories);
+    public static void WriteAllText(string path, string? contents, Encoding encoding, bool createDirectories);
+    public static Task WriteAllTextAsync(string path, string? contents, Encoding encoding, bool createDirectories, CancellationToken cancellationToken = default);
+    public static Task WriteAllTextAsync(string path, string? contents, bool createDirectories, CancellationToken cancellationToken = default);
}

Any method that involves creation of a file are included in the list.

Open questions

  • Should Open methods get included in this? Depending on the FileMode it may not end up creating a new file.
  • Should OpenHandle be included in this? The use of file handles IMO falls into more of an advanced API.
  • CreateSymbolicLink?
  • Copy / Move / Replace?
  • Perhaps a better name than createDirectories (singular createDirectory?)

API Usage

// string filePath = ...

// Previously:
(new FileInfo(filePath)).Directory.Create()
File.WriteAllText(filePath, text)
// Alternatively:
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllText(filePath, text)

// Simplified, per proposal
File.WriteAllText(filePath, text, createDirectories: true)

Alternative Designs

None from what I can think of currently...

Risks

  • Could it be possible that some sort of path traversal attack is made possible with improper use of this API?
  • The operation is not going to be atomic. There is no OS that supports this in one go, so it would always have to be a two-step process involving creating the directories then creating the file
  • If called repeatedly against the same parent directory, there would be unnecessary attempts to create the parent directory (but IMO this is more of an improper use than something fundamentally wrong with the design)
  • What to do if we successfully create the directory but somehow fail creating the file?
@Gnbrkm41 Gnbrkm41 added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Dec 23, 2024
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Dec 23, 2024
@Gnbrkm41 Gnbrkm41 changed the title [API Proposal]: Add a 'createDirectories option to System.IO.File` APIs [API Proposal]: Add a createDirectories option to System.IO.File APIs Dec 23, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

@Gnbrkm41 Gnbrkm41 changed the title [API Proposal]: Add a createDirectories option to System.IO.File APIs [API Proposal]: Add an option to create parent directories in System.IO.File APIs Dec 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.IO untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

1 participant