Skip to content

Commit 75fa8e0

Browse files
authored
Merge pull request #2311 from millenomi/filemanager-replaceitem
2 parents 42b387c + c512c91 commit 75fa8e0

File tree

5 files changed

+384
-11
lines changed

5 files changed

+384
-11
lines changed

CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
#include <sys/types.h>
6565
#include <sys/stat.h>
6666
#include <linux/stat.h>
67+
#include <linux/fs.h>
6768
#define AT_STATX_SYNC_AS_STAT 0x0000 /* - Do whatever stat() does */
6869
#endif //__GLIBC_PREREQ(2. 28)
6970
#endif // TARGET_OS_LINUX
@@ -591,6 +592,12 @@ _stat_with_btime(const char *filename, struct stat *buffer, struct timespec *bti
591592
return lstat(filename, buffer);
592593
}
593594
#endif // __NR_statx
595+
596+
static unsigned int const _CF_renameat2_RENAME_EXCHANGE = 1 << 1;
597+
static int _CF_renameat2(int olddirfd, const char *_Nonnull oldpath,
598+
int newdirfd, const char *_Nonnull newpath, unsigned int flags) {
599+
return syscall(SYS_renameat2, olddirfd, oldpath, newdirfd, newpath, flags);
600+
}
594601
#endif // TARGET_OS_LINUX
595602

596603
#if __HAS_STATX

Foundation/FileManager+POSIX.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,120 @@ internal func _contentsEqual(atPath path1: String, andPath path2: String) -> Boo
11851185
throw _NSErrorWithErrno(error, reading: false, path: path)
11861186
}
11871187
}
1188+
1189+
internal func _replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = [], allowPlatformSpecificSyscalls: Bool = true) throws -> URL? {
1190+
1191+
// 1. Make a backup, if asked to.
1192+
var backupItemURL: URL?
1193+
if let backupItemName = backupItemName {
1194+
let url = originalItemURL.deletingLastPathComponent().appendingPathComponent(backupItemName)
1195+
try copyItem(at: originalItemURL, to: url)
1196+
backupItemURL = url
1197+
}
1198+
1199+
// 2. Make sure we have a copy of the original attributes if we're being asked to preserve them (the default)
1200+
let originalAttributes = try attributesOfItem(atPath: originalItemURL.path)
1201+
let newAttributes = try attributesOfItem(atPath: newItemURL.path)
1202+
1203+
func applyPostprocessingRequiredByOptions() throws {
1204+
if !options.contains(.usingNewMetadataOnly) {
1205+
var attributesToReapply: [FileAttributeKey: Any] = [:]
1206+
attributesToReapply[.creationDate] = originalAttributes[.creationDate]
1207+
attributesToReapply[.posixPermissions] = originalAttributes[.posixPermissions]
1208+
try setAttributes(attributesToReapply, ofItemAtPath: originalItemURL.path)
1209+
}
1210+
1211+
// As the very last step, if not explicitly asked to keep the backup, remove it.
1212+
if let backupItemURL = backupItemURL, !options.contains(.withoutDeletingBackupItem) {
1213+
try removeItem(at: backupItemURL)
1214+
}
1215+
}
1216+
1217+
if allowPlatformSpecificSyscalls {
1218+
// First, a little OS-specific detour.
1219+
// Blindly try these operations first, and fall back to the non-OS-specific code below if they all fail.
1220+
#if canImport(Darwin)
1221+
do {
1222+
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
1223+
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
1224+
// Note that Darwin allows swapping a file with a directory this way.
1225+
if renameatx_np(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, UInt32(RENAME_SWAP)) == 0 {
1226+
return nil
1227+
} else {
1228+
return errno
1229+
}
1230+
}
1231+
}
1232+
1233+
if let finalErrno = finalErrno, finalErrno != ENOTSUP {
1234+
throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL)
1235+
} else if finalErrno == nil {
1236+
try applyPostprocessingRequiredByOptions()
1237+
return originalItemURL
1238+
}
1239+
}
1240+
#endif
1241+
1242+
#if canImport(Glibc)
1243+
do {
1244+
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
1245+
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
1246+
if let originalFS = originalFS,
1247+
let newItemFS = newItemFS {
1248+
if _CF_renameat2(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, _CF_renameat2_RENAME_EXCHANGE) == 0 {
1249+
return nil
1250+
} else {
1251+
return errno
1252+
}
1253+
} else {
1254+
return Int32(EINVAL)
1255+
}
1256+
}
1257+
}
1258+
1259+
// ENOTDIR is raised if the objects are directories; EINVAL may indicate that the filesystem does not support the operation.
1260+
if let finalErrno = finalErrno, finalErrno != ENOTDIR && finalErrno != EINVAL {
1261+
throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL)
1262+
} else if finalErrno == nil {
1263+
try applyPostprocessingRequiredByOptions()
1264+
return originalItemURL
1265+
}
1266+
}
1267+
#endif
1268+
}
1269+
1270+
// 3. Replace!
1271+
// Are they both regular files?
1272+
let originalType = originalAttributes[.type] as? FileAttributeType
1273+
let newType = newAttributes[.type] as? FileAttributeType
1274+
if originalType == newType, originalType == .typeRegular {
1275+
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
1276+
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
1277+
// This is an atomic operation in many OSes, but is not guaranteed to be atomic by the standard.
1278+
if rename(newItemFS, originalFS) == 0 {
1279+
return nil
1280+
} else {
1281+
return errno
1282+
}
1283+
}
1284+
}
1285+
if let theErrno = finalErrno {
1286+
throw _NSErrorWithErrno(theErrno, reading: false, url: originalItemURL)
1287+
}
1288+
} else {
1289+
// Only perform a replacement of different object kinds nonatomically.
1290+
let uniqueName = UUID().uuidString
1291+
let tombstoneURL = newItemURL.deletingLastPathComponent().appendingPathComponent(uniqueName)
1292+
try moveItem(at: originalItemURL, to: tombstoneURL)
1293+
try moveItem(at: newItemURL, to: originalItemURL)
1294+
try removeItem(at: tombstoneURL)
1295+
}
1296+
1297+
// 4. Reapply attributes if asked to preserve, and delete the backup if not asked otherwise.
1298+
try applyPostprocessingRequiredByOptions()
1299+
1300+
return originalItemURL
1301+
}
11881302
}
11891303

11901304
extension FileManager.NSPathDirectoryEnumerator {

Foundation/FileManager.swift

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,80 @@ open class FileManager : NSObject {
138138

139139
You may pass only one of the values from the NSSearchPathDomainMask enumeration, and you may not pass NSAllDomainsMask.
140140
*/
141-
open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL {
142-
let urls = self.urls(for: directory, in: domain)
143-
guard let url = urls.first else {
144-
// On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap.
145-
// We simulate that behavior by throwing a private error.
146-
throw URLForDirectoryError.directoryUnknown
141+
open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor reference: URL?, create
142+
shouldCreate: Bool) throws -> URL {
143+
var url: URL
144+
145+
if directory == .itemReplacementDirectory {
146+
// We mimic Darwin here — .itemReplacementDirectory has a number of requirements for use and not meeting them is a programmer error and should panic out.
147+
precondition(domain == .userDomainMask)
148+
let referenceURL = reference!
149+
150+
// If the temporary directory and the reference URL are on the same device, use a subdirectory in the temporary directory. Otherwise, use a temporary directory at the same path as the filesystem that contains this file if it's writable. Fall back to the temporary directory if the latter doesn't work.
151+
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
152+
let useTemporaryDirectory: Bool
153+
154+
let maybeVolumeIdentifier = try? temporaryDirectory.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier as? AnyHashable
155+
let maybeReferenceVolumeIdentifier = try? referenceURL.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier as? AnyHashable
156+
157+
if let volumeIdentifier = maybeVolumeIdentifier,
158+
let referenceVolumeIdentifier = maybeReferenceVolumeIdentifier {
159+
useTemporaryDirectory = volumeIdentifier == referenceVolumeIdentifier
160+
} else {
161+
useTemporaryDirectory = !isWritableFile(atPath: referenceURL.deletingPathExtension().path)
162+
}
163+
164+
// This is the same name Darwin uses.
165+
if useTemporaryDirectory {
166+
url = temporaryDirectory.appendingPathComponent("TemporaryItems")
167+
} else {
168+
url = referenceURL.deletingPathExtension()
169+
}
170+
} else {
171+
172+
let urls = self.urls(for: directory, in: domain)
173+
guard let theURL = urls.first else {
174+
// On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap.
175+
// We simulate that behavior by throwing a private error.
176+
throw URLForDirectoryError.directoryUnknown
177+
}
178+
url = theURL
179+
}
180+
181+
var nameStorage: String?
182+
183+
func itemReplacementDirectoryName(forAttempt attempt: Int) -> String {
184+
let name: String
185+
if let someName = nameStorage {
186+
name = someName
187+
} else {
188+
// Sanitize the process name for filesystem use:
189+
var someName = ProcessInfo.processInfo.processName
190+
let characterSet = CharacterSet.alphanumerics.inverted
191+
while let whereIsIt = someName.rangeOfCharacter(from: characterSet, options: [], range: nil) {
192+
someName.removeSubrange(whereIsIt)
193+
}
194+
name = someName
195+
nameStorage = someName
196+
}
197+
198+
if attempt == 0 {
199+
return "(A Document Being Saved By \(name))"
200+
} else {
201+
return "(A Document Being Saved By \(name) \(attempt + 1)"
202+
}
147203
}
148204

149-
if shouldCreate {
205+
// To avoid races, on Darwin, the item replacement directory is _ALWAYS_ created, even if create is false.
206+
if shouldCreate || directory == .itemReplacementDirectory {
150207
var attributes: [FileAttributeKey : Any] = [:]
151208

152209
switch _SearchPathDomain(domain) {
153210
case .some(.user):
154-
attributes[.posixPermissions] = 0700
211+
attributes[.posixPermissions] = 0o700
155212

156213
case .some(.system):
157-
attributes[.posixPermissions] = 0755
214+
attributes[.posixPermissions] = 0o755
158215
attributes[.ownerAccountID] = 0 // root
159216
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
160217
attributes[.ownerAccountID] = 80 // on Darwin, the admin group's fixed ID.
@@ -164,7 +221,29 @@ open class FileManager : NSObject {
164221
break
165222
}
166223

167-
try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
224+
if directory == .itemReplacementDirectory {
225+
226+
try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
227+
var attempt = 0
228+
229+
while true {
230+
do {
231+
let attemptedURL = url.appendingPathComponent(itemReplacementDirectoryName(forAttempt: attempt))
232+
try createDirectory(at: attemptedURL, withIntermediateDirectories: false)
233+
url = attemptedURL
234+
break
235+
} catch {
236+
if let error = error as? NSError, error.domain == NSCocoaErrorDomain, error.code == CocoaError.fileWriteFileExists.rawValue {
237+
attempt += 1
238+
} else {
239+
throw error
240+
}
241+
}
242+
}
243+
244+
} else {
245+
try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
246+
}
168247
}
169248

170249
return url
@@ -939,9 +1018,21 @@ open class FileManager : NSObject {
9391018

9401019
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
9411020
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
942-
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws {
1021+
#if os(Windows)
1022+
@available(Windows, deprecated, message: "Not yet implemented")
1023+
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws -> URL? {
9431024
NSUnimplemented()
9441025
}
1026+
#else
1027+
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws -> URL? {
1028+
return try _replaceItem(at: originalItemURL, withItemAt: newItemURL, backupItemName: backupItemName, options: options)
1029+
}
1030+
#endif
1031+
1032+
@available(*, unavailable, message: "Returning an object through an autoreleased pointer is not supported in swift-corelibs-foundation. Use replaceItem(at:withItemAt:backupItemName:options:) instead.", renamed: "replaceItem(at:withItemAt:backupItemName:options:)")
1033+
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: FileManager.ItemReplacementOptions = [], resultingItemURL resultingURL: UnsafeMutablePointer<NSURL?>?) throws {
1034+
NSUnsupported()
1035+
}
9451036

9461037
internal func _tryToResolveTrailingSymlinkInPath(_ path: String) -> String? {
9471038
// destinationOfSymbolicLink(atPath:) will fail if the path is not a symbolic link

0 commit comments

Comments
 (0)