Skip to content

Commit 7124f09

Browse files
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.
1 parent 767ea9e commit 7124f09

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

Sources/NIOFS/FileSystem.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,20 @@ extension FileSystem {
825825
break loop
826826

827827
case let .failure(errno):
828+
if errno == .fileExists {
829+
switch self._info(forFileAt: path, infoAboutSymbolicLink: false) {
830+
case let .success(maybeInfo):
831+
if let info = maybeInfo, info.type == .directory {
832+
break loop
833+
} else {
834+
// A file exists at this path.
835+
return .failure(.mkdir(errno: errno, path: path, location: .here()))
836+
}
837+
case .failure:
838+
// Unable to determine what exists at this path.
839+
return .failure(.mkdir(errno: errno, path: path, location: .here()))
840+
}
841+
}
828842
guard createIntermediateDirectories, errno == .noSuchFileOrDirectory else {
829843
return .failure(.mkdir(errno: errno, path: path, location: .here()))
830844
}

Sources/_NIOFileSystem/FileSystem.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,20 @@ extension FileSystem {
839839
break loop
840840

841841
case let .failure(errno):
842+
if errno == .fileExists {
843+
switch self._info(forFileAt: path, infoAboutSymbolicLink: false) {
844+
case let .success(maybeInfo):
845+
if let info = maybeInfo, info.type == .directory {
846+
break loop
847+
} else {
848+
// A file exists at this path.
849+
return .failure(.mkdir(errno: errno, path: path, location: .here()))
850+
}
851+
case .failure:
852+
// Unable to determine what exists at this path.
853+
return .failure(.mkdir(errno: errno, path: path, location: .here()))
854+
}
855+
}
842856
guard createIntermediateDirectories, errno == .noSuchFileOrDirectory else {
843857
return .failure(.mkdir(errno: errno, path: path, location: .here()))
844858
}

Tests/NIOFSIntegrationTests/FileSystemTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,37 @@ final class FileSystemTests: XCTestCase {
505505
}
506506
}
507507

508+
func testCreateDirectoryIsIdempotentWhenAlreadyExists() async throws {
509+
let path = try await self.fs.temporaryFilePath()
510+
511+
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
512+
513+
try await self.fs.createDirectory(at: path, withIntermediateDirectories: false)
514+
try await self.fs.createDirectory(at: path, withIntermediateDirectories: true)
515+
516+
try await self.fs.withDirectoryHandle(atPath: path) { dir in
517+
let info = try await dir.info()
518+
XCTAssertEqual(info.type, .directory)
519+
XCTAssertGreaterThan(info.size, 0)
520+
}
521+
}
522+
523+
func testCreateDirectoryThroughSymlinkToExistingDirectoryIsIdempotent() async throws {
524+
let realDir = try await self.fs.temporaryFilePath()
525+
try await self.fs.createDirectory(at: realDir, withIntermediateDirectories: false)
526+
527+
let linkPath = try await self.fs.temporaryFilePath()
528+
try await self.fs.createSymbolicLink(at: linkPath, withDestination: realDir)
529+
530+
try await self.fs.createDirectory(at: linkPath, withIntermediateDirectories: false)
531+
532+
try await self.fs.withDirectoryHandle(atPath: linkPath) { dir in
533+
let info = try await dir.info()
534+
XCTAssertEqual(info.type, .directory)
535+
XCTAssertGreaterThan(info.size, 0)
536+
}
537+
}
538+
508539
func testCurrentWorkingDirectory() async throws {
509540
let directory = try await self.fs.currentWorkingDirectory
510541
XCTAssert(!directory.underlying.isEmpty)

0 commit comments

Comments
 (0)