Skip to content

Commit e315e9b

Browse files
committed
Fix item replacement directory lookup
1 parent 7b09e1b commit e315e9b

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

Sources/FoundationEssentials/FileManager/FileManager+Directories.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,45 @@ extension _FileManagerImpl {
6262
var temporaryDirectory: URL {
6363
URL(filePath: String.temporaryDirectoryPath, directoryHint: .isDirectory)
6464
}
65+
66+
private func itemReplacementDirectory(appropriateFor reference: URL) throws -> URL {
67+
guard reference.isFileURL else {
68+
throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, reference)
69+
}
70+
71+
let temporaryItemsDirectory = temporaryDirectory.appending(component: "TemporaryItems", directoryHint: .isDirectory)
72+
let referenceDirectory = reference.deletingPathExtension()
73+
let useTemporaryDirectory: Bool
74+
if let temporaryVolumeIdentifier = try? temporaryDirectory.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier,
75+
let referenceVolumeIdentifier = try? reference.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier {
76+
useTemporaryDirectory = temporaryVolumeIdentifier.isEqual(referenceVolumeIdentifier)
77+
} else {
78+
useTemporaryDirectory = !fileManager.isWritableFile(atPath: referenceDirectory.path)
79+
}
80+
81+
let containerDirectory = useTemporaryDirectory ? temporaryItemsDirectory : referenceDirectory
82+
try fileManager.createDirectory(at: containerDirectory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
83+
84+
var attempt = 0
85+
while true {
86+
let replacementDirectory = containerDirectory.appending(component: itemReplacementDirectoryName(forAttempt: attempt), directoryHint: .isDirectory)
87+
do {
88+
try fileManager.createDirectory(at: replacementDirectory, withIntermediateDirectories: false)
89+
return replacementDirectory
90+
} catch let error as CocoaError where error.code == .fileWriteFileExists {
91+
attempt += 1
92+
}
93+
}
94+
}
95+
96+
private func itemReplacementDirectoryName(forAttempt attempt: Int) -> String {
97+
let processName = ProcessInfo.processInfo.processName.filter { $0.isLetter || $0.isNumber }
98+
let sanitizedProcessName = processName.isEmpty ? "Process" : processName
99+
if attempt == 0 {
100+
return "(A Document Being Saved By \(sanitizedProcessName))"
101+
}
102+
return "(A Document Being Saved By \(sanitizedProcessName) \(attempt + 1))"
103+
}
65104

66105
func url(
67106
for directory: FileManager.SearchPathDirectory,
@@ -85,6 +124,10 @@ extension _FileManagerImpl {
85124
domain = ._partitionedSystemDomainMask
86125
}
87126
#endif
127+
128+
if let url, domain == .userDomainMask, directory == .itemReplacementDirectory {
129+
return try itemReplacementDirectory(appropriateFor: url)
130+
}
88131

89132
let urls = Array(_SearchPathURLs(for: directory, in: domain, expandTilde: true))
90133
#if FOUNDATION_FRAMEWORK

Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,31 @@ private struct FileManagerTests {
10961096
assertSearchPaths([.itemReplacementDirectory], exists: false)
10971097
}
10981098

1099+
@Test(arguments: [false, true])
1100+
func itemReplacementDirectory(create: Bool) async throws {
1101+
try await FilePlayground {
1102+
Directory("Documents") {}
1103+
}.test { fileManager in
1104+
let reference = URL(filePath: fileManager.currentDirectoryPath, directoryHint: .isDirectory)
1105+
.appending(component: "Documents", directoryHint: .isDirectory)
1106+
let replacementDirectory = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: reference, create: create)
1107+
let secondReplacementDirectory = try fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: reference, create: create)
1108+
defer {
1109+
try? fileManager.removeItem(at: replacementDirectory)
1110+
try? fileManager.removeItem(at: secondReplacementDirectory)
1111+
}
1112+
1113+
var isDirectory = false
1114+
#expect(fileManager.fileExists(atPath: replacementDirectory.path, isDirectory: &isDirectory))
1115+
#expect(isDirectory)
1116+
isDirectory = false
1117+
#expect(fileManager.fileExists(atPath: secondReplacementDirectory.path, isDirectory: &isDirectory))
1118+
#expect(isDirectory)
1119+
#expect(replacementDirectory != reference)
1120+
#expect(secondReplacementDirectory != replacementDirectory)
1121+
}
1122+
}
1123+
10991124
#if !canImport(Darwin) && !os(Windows)
11001125
@Test(.disabled(if: ProcessInfo.processInfo.environment.keys.contains(where: { $0.starts(with: "XDG") }), "Skipping due to presence of XDG environment variables which may affect this test"))
11011126
func searchPaths_XDGEnvironmentVariables() async throws {

0 commit comments

Comments
 (0)