Skip to content

Commit 31fef58

Browse files
authored
Use SHCreateDirectory to create a directory with intermediate directories on Windows (#1033)
1 parent 919053f commit 31fef58

File tree

1 file changed

+66
-19
lines changed

1 file changed

+66
-19
lines changed

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -250,35 +250,82 @@ extension _FileManagerImpl {
250250

251251
try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes)
252252
}
253+
254+
#if os(Windows)
255+
/// If `path` is absolute, this is the same as `path.withNTPathRepresentation`.
256+
/// If `path` is relative, this creates an absolute path of `path` relative to `currentDirectoryPath` and runs
257+
/// `body` with that path.
258+
private func withAbsoluteNTPathRepresentation<Result>(
259+
of path: String,
260+
_ body: (UnsafePointer<WCHAR>) throws -> Result
261+
) throws -> Result {
262+
try path.withNTPathRepresentation { pwszPath in
263+
if !PathIsRelativeW(pwszPath) {
264+
// We already have an absolute path. Nothing to do
265+
return try body(pwszPath)
266+
}
267+
guard let currentDirectoryPath else {
268+
preconditionFailure("We should always have a current directory on Windows")
269+
}
270+
271+
// We have a relateive path. Make it absolute.
272+
let absoluteUrl = URL(
273+
filePath: path,
274+
directoryHint: .isDirectory,
275+
relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory)
276+
)
277+
return try absoluteUrl.path.withNTPathRepresentation { pwszPath in
278+
return try body(pwszPath)
279+
}
280+
}
281+
}
282+
#endif
253283

254284
func createDirectory(
255285
atPath path: String,
256286
withIntermediateDirectories createIntermediates: Bool,
257287
attributes: [FileAttributeKey : Any]? = nil
258288
) throws {
259289
#if os(Windows)
260-
try path.withNTPathRepresentation { pwszPath in
261-
if createIntermediates {
262-
var isDirectory: Bool = false
263-
if fileManager.fileExists(atPath: path, isDirectory: &isDirectory) {
264-
guard isDirectory else {
265-
throw CocoaError.errorWithFilePath(path, win32: ERROR_FILE_EXISTS, reading: false)
266-
}
267-
return
290+
var saAttributes: SECURITY_ATTRIBUTES =
291+
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
292+
lpSecurityDescriptor: nil,
293+
bInheritHandle: false)
294+
// `SHCreateDirectoryExW` creates intermediate directories while `CreateDirectoryW` does not.
295+
if createIntermediates {
296+
// `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current working
297+
// directory.
298+
try withAbsoluteNTPathRepresentation(of: path) { pwszPath in
299+
let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes)
300+
guard let errorCode = DWORD(exactly: errorCode) else {
301+
// `SHCreateDirectoryExW` returns `Int` but all error codes are defined in terms of `DWORD`, aka
302+
// `UInt`. We received an unknown error code.
303+
throw CocoaError.errorWithFilePath(.fileWriteUnknown, path)
268304
}
269-
270-
let parent = path.deletingLastPathComponent()
271-
if !parent.isEmpty {
272-
try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes)
305+
switch errorCode {
306+
case ERROR_SUCCESS:
307+
if let attributes {
308+
try? fileManager.setAttributes(attributes, ofItemAtPath: path)
309+
}
310+
case ERROR_ALREADY_EXISTS:
311+
var isDirectory: Bool = false
312+
if fileExists(atPath: path, isDirectory: &isDirectory), isDirectory {
313+
// A directory already exists at this path, which is not an error if we have
314+
// `createIntermediates == true`.
315+
break
316+
}
317+
// A file (not a directory) exists at the given path or the file creation failed and the item
318+
// at this path has been deleted before the call to `fileExists`. Throw the original error.
319+
fallthrough
320+
default:
321+
throw CocoaError.errorWithFilePath(path, win32: errorCode, reading: false)
273322
}
274323
}
275-
276-
var saAttributes: SECURITY_ATTRIBUTES =
277-
SECURITY_ATTRIBUTES(nLength: DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size),
278-
lpSecurityDescriptor: nil,
279-
bInheritHandle: false)
280-
guard CreateDirectoryW(pwszPath, &saAttributes) else {
281-
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
324+
} else {
325+
try path.withNTPathRepresentation { pwszPath in
326+
guard CreateDirectoryW(pwszPath, &saAttributes) else {
327+
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false)
328+
}
282329
}
283330
if let attributes {
284331
try? fileManager.setAttributes(attributes, ofItemAtPath: path)

0 commit comments

Comments
 (0)