Skip to content

Parity: FileManager.replaceItem(…) #2311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/stat.h>
#include <linux/fs.h>
#define AT_STATX_SYNC_AS_STAT 0x0000 /* - Do whatever stat() does */
#endif //__GLIBC_PREREQ(2. 28)
#endif // TARGET_OS_LINUX
Expand Down Expand Up @@ -591,6 +592,12 @@ _stat_with_btime(const char *filename, struct stat *buffer, struct timespec *bti
return lstat(filename, buffer);
}
#endif // __NR_statx

static unsigned int const _CF_renameat2_RENAME_EXCHANGE = 1 << 1;
static int _CF_renameat2(int olddirfd, const char *_Nonnull oldpath,
int newdirfd, const char *_Nonnull newpath, unsigned int flags) {
return syscall(SYS_renameat2, olddirfd, oldpath, newdirfd, newpath, flags);
}
#endif // TARGET_OS_LINUX

#if __HAS_STATX
Expand Down
114 changes: 114 additions & 0 deletions Foundation/FileManager+POSIX.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,120 @@ internal func _contentsEqual(atPath path1: String, andPath path2: String) -> Boo
throw _NSErrorWithErrno(error, reading: false, path: path)
}
}

internal func _replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = [], allowPlatformSpecificSyscalls: Bool = true) throws -> URL? {

// 1. Make a backup, if asked to.
var backupItemURL: URL?
if let backupItemName = backupItemName {
let url = originalItemURL.deletingLastPathComponent().appendingPathComponent(backupItemName)
try copyItem(at: originalItemURL, to: url)
backupItemURL = url
}

// 2. Make sure we have a copy of the original attributes if we're being asked to preserve them (the default)
let originalAttributes = try attributesOfItem(atPath: originalItemURL.path)
let newAttributes = try attributesOfItem(atPath: newItemURL.path)

func applyPostprocessingRequiredByOptions() throws {
if !options.contains(.usingNewMetadataOnly) {
var attributesToReapply: [FileAttributeKey: Any] = [:]
attributesToReapply[.creationDate] = originalAttributes[.creationDate]
attributesToReapply[.posixPermissions] = originalAttributes[.posixPermissions]
try setAttributes(attributesToReapply, ofItemAtPath: originalItemURL.path)
}

// As the very last step, if not explicitly asked to keep the backup, remove it.
if let backupItemURL = backupItemURL, !options.contains(.withoutDeletingBackupItem) {
try removeItem(at: backupItemURL)
}
}

if allowPlatformSpecificSyscalls {
// First, a little OS-specific detour.
// Blindly try these operations first, and fall back to the non-OS-specific code below if they all fail.
#if canImport(Darwin)
do {
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
// Note that Darwin allows swapping a file with a directory this way.
if renameatx_np(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, UInt32(RENAME_SWAP)) == 0 {
return nil
} else {
return errno
}
}
}

if let finalErrno = finalErrno, finalErrno != ENOTSUP {
throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL)
} else if finalErrno == nil {
try applyPostprocessingRequiredByOptions()
return originalItemURL
}
}
#endif

#if canImport(Glibc)
do {
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
if let originalFS = originalFS,
let newItemFS = newItemFS {
if _CF_renameat2(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, _CF_renameat2_RENAME_EXCHANGE) == 0 {
return nil
} else {
return errno
}
} else {
return Int32(EINVAL)
}
}
}

// ENOTDIR is raised if the objects are directories; EINVAL may indicate that the filesystem does not support the operation.
if let finalErrno = finalErrno, finalErrno != ENOTDIR && finalErrno != EINVAL {
throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL)
} else if finalErrno == nil {
try applyPostprocessingRequiredByOptions()
return originalItemURL
}
}
#endif
}

// 3. Replace!
// Are they both regular files?
let originalType = originalAttributes[.type] as? FileAttributeType
let newType = newAttributes[.type] as? FileAttributeType
if originalType == newType, originalType == .typeRegular {
let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in
return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in
// This is an atomic operation in many OSes, but is not guaranteed to be atomic by the standard.
if rename(newItemFS, originalFS) == 0 {
return nil
} else {
return errno
}
}
}
if let theErrno = finalErrno {
throw _NSErrorWithErrno(theErrno, reading: false, url: originalItemURL)
}
} else {
// Only perform a replacement of different object kinds nonatomically.
let uniqueName = UUID().uuidString
let tombstoneURL = newItemURL.deletingLastPathComponent().appendingPathComponent(uniqueName)
try moveItem(at: originalItemURL, to: tombstoneURL)
try moveItem(at: newItemURL, to: originalItemURL)
try removeItem(at: tombstoneURL)
}

// 4. Reapply attributes if asked to preserve, and delete the backup if not asked otherwise.
try applyPostprocessingRequiredByOptions()

return originalItemURL
}
}

extension FileManager.NSPathDirectoryEnumerator {
Expand Down
113 changes: 102 additions & 11 deletions Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,23 +138,80 @@ open class FileManager : NSObject {

You may pass only one of the values from the NSSearchPathDomainMask enumeration, and you may not pass NSAllDomainsMask.
*/
open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor url: URL?, create shouldCreate: Bool) throws -> URL {
let urls = self.urls(for: directory, in: domain)
guard let url = urls.first else {
// On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap.
// We simulate that behavior by throwing a private error.
throw URLForDirectoryError.directoryUnknown
open func url(for directory: SearchPathDirectory, in domain: SearchPathDomainMask, appropriateFor reference: URL?, create
shouldCreate: Bool) throws -> URL {
var url: URL

if directory == .itemReplacementDirectory {
// We mimic Darwin here — .itemReplacementDirectory has a number of requirements for use and not meeting them is a programmer error and should panic out.
precondition(domain == .userDomainMask)
let referenceURL = reference!

// 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.
let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
let useTemporaryDirectory: Bool

let maybeVolumeIdentifier = try? temporaryDirectory.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier as? AnyHashable
let maybeReferenceVolumeIdentifier = try? referenceURL.resourceValues(forKeys: [.volumeIdentifierKey]).volumeIdentifier as? AnyHashable

if let volumeIdentifier = maybeVolumeIdentifier,
let referenceVolumeIdentifier = maybeReferenceVolumeIdentifier {
useTemporaryDirectory = volumeIdentifier == referenceVolumeIdentifier
} else {
useTemporaryDirectory = !isWritableFile(atPath: referenceURL.deletingPathExtension().path)
}

// This is the same name Darwin uses.
if useTemporaryDirectory {
url = temporaryDirectory.appendingPathComponent("TemporaryItems")
} else {
url = referenceURL.deletingPathExtension()
}
} else {

let urls = self.urls(for: directory, in: domain)
guard let theURL = urls.first else {
// On Apple OSes, this case returns nil without filling in the error parameter; Swift then synthesizes an error rather than trap.
// We simulate that behavior by throwing a private error.
throw URLForDirectoryError.directoryUnknown
}
url = theURL
}

var nameStorage: String?

func itemReplacementDirectoryName(forAttempt attempt: Int) -> String {
let name: String
if let someName = nameStorage {
name = someName
} else {
// Sanitize the process name for filesystem use:
var someName = ProcessInfo.processInfo.processName
let characterSet = CharacterSet.alphanumerics.inverted
while let whereIsIt = someName.rangeOfCharacter(from: characterSet, options: [], range: nil) {
someName.removeSubrange(whereIsIt)
}
name = someName
nameStorage = someName
}

if attempt == 0 {
return "(A Document Being Saved By \(name))"
} else {
return "(A Document Being Saved By \(name) \(attempt + 1)"
}
}

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

switch _SearchPathDomain(domain) {
case .some(.user):
attributes[.posixPermissions] = 0700
attributes[.posixPermissions] = 0o700

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

try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
if directory == .itemReplacementDirectory {

try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
var attempt = 0

while true {
do {
let attemptedURL = url.appendingPathComponent(itemReplacementDirectoryName(forAttempt: attempt))
try createDirectory(at: attemptedURL, withIntermediateDirectories: false)
url = attemptedURL
break
} catch {
if let error = error as? NSError, error.domain == NSCocoaErrorDomain, error.code == CocoaError.fileWriteFileExists.rawValue {
attempt += 1
} else {
throw error
}
}
}

} else {
try createDirectory(at: url, withIntermediateDirectories: true, attributes: attributes)
}
}

return url
Expand Down Expand Up @@ -939,9 +1018,21 @@ open class FileManager : NSObject {

/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws {
#if os(Windows)
@available(Windows, deprecated, message: "Not yet implemented")
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws -> URL? {
NSUnimplemented()
}
#else
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = []) throws -> URL? {
return try _replaceItem(at: originalItemURL, withItemAt: newItemURL, backupItemName: backupItemName, options: options)
}
#endif

@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:)")
open func replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: FileManager.ItemReplacementOptions = [], resultingItemURL resultingURL: UnsafeMutablePointer<NSURL?>?) throws {
NSUnsupported()
}

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