feat(NIOFileSystem): Add idempotent directory creation behavior (#3404) (#3410)

The createDirectory function will succeed without error if the target
directory already exists.

###  Motivation

This change addresses issue #3404. Currently,
`fileSystem.createDirectory` fails if the target directory already
exists, forcing users to write boilerplate try/catch blocks to handle
this common and expected case.

The goal is to make this function's behavior idempotent.

### Modifications

To achieve this, I've made the following changes:

**(Implementation)** A new private helper function,
`_handleCreateDirectoryFileExists`, was introduced. This function is
responsible for:

1. Performing a `stat` call on the path that failed.
2. Checking if the existing item is a directory (`S_IFDIR`).
3. Returning a success result if it's a directory, or re-throwing the
original `.fileExists` error if it's a file or another type of entity.

**(Logic)** The core `_createDirectory` function was updated to call
this new helper function whenever `Syscall.mkdir` fails with an `EEXIST`
(`.fileExists`) error. This check is applied in both internal loops to
correctly handle cases where either an intermediate directory or the
final target directory already exists.

### Result
With this change, users can now call the function
`fileSystem.createDirectory` and the operation will succeed even if the
directory is already present, leading to cleaner and more predictable
code.
This commit is contained in:
Hamza Hassanain
2025-10-20 15:49:26 +01:00
committed by GitHub
parent 767ea9ee09
commit 7124f0963a
3 changed files with 59 additions and 0 deletions
+14
View File
@@ -825,6 +825,20 @@ extension FileSystem {
break loop
case let .failure(errno):
if errno == .fileExists {
switch self._info(forFileAt: path, infoAboutSymbolicLink: false) {
case let .success(maybeInfo):
if let info = maybeInfo, info.type == .directory {
break loop
} else {
// A file exists at this path.
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
case .failure:
// Unable to determine what exists at this path.
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
}
guard createIntermediateDirectories, errno == .noSuchFileOrDirectory else {
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
+14
View File
@@ -839,6 +839,20 @@ extension FileSystem {
break loop
case let .failure(errno):
if errno == .fileExists {
switch self._info(forFileAt: path, infoAboutSymbolicLink: false) {
case let .success(maybeInfo):
if let info = maybeInfo, info.type == .directory {
break loop
} else {
// A file exists at this path.
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
case .failure:
// Unable to determine what exists at this path.
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
}
guard createIntermediateDirectories, errno == .noSuchFileOrDirectory else {
return .failure(.mkdir(errno: errno, path: path, location: .here()))
}
@@ -505,6 +505,37 @@ final class FileSystemTests: XCTestCase {
}
}
func testCreateDirectoryIsIdempotentWhenAlreadyExists() async throws {
let path = try await self.fs.temporaryFilePath()
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
try await self.fs.withDirectoryHandle(atPath: path) { dir in
let info = try await dir.info()
XCTAssertEqual(info.type, .directory)
XCTAssertGreaterThan(info.size, 0)
}
}
func testCreateDirectoryThroughSymlinkToExistingDirectoryIsIdempotent() async throws {
let realDir = try await self.fs.temporaryFilePath()
try await self.fs.createDirectory(at: realDir, withIntermediateDirectories: false)
let linkPath = try await self.fs.temporaryFilePath()
try await self.fs.createSymbolicLink(at: linkPath, withDestination: realDir)
try await self.fs.createDirectory(at: linkPath, withIntermediateDirectories: false)
try await self.fs.withDirectoryHandle(atPath: linkPath) { dir in
let info = try await dir.info()
XCTAssertEqual(info.type, .directory)
XCTAssertGreaterThan(info.size, 0)
}
}
func testCurrentWorkingDirectory() async throws {
let directory = try await self.fs.currentWorkingDirectory
XCTAssert(!directory.underlying.isEmpty)