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

Open
#110,919 2 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Stale
Tech stack
csharp

Research direction

Start with the System.IO.File creation-related APIs and the proposed overload list, then work through the open questions about Open, OpenHandle, symbolic links, and copy or move operations. Done means the API scope, option name, failure behavior, and compatibility implications are resolved well enough for an implementation plan.

Written by the indexing model from the issue text.

Description

api-suggestion area-System.IO needs-further-triage
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?
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.