Skip to content

FileManager: Fix copyItem(atPath:toPath:) for directories and symlinks. #1515

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
Apr 15, 2018
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
26 changes: 22 additions & 4 deletions Foundation/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,16 +442,34 @@ open class FileManager : NSObject {
else {
return
}

func copyNonDirectory(srcPath: String, dstPath: String, fileType: FileAttributeType) throws {
if fileType == .typeSymbolicLink {
let destination = try destinationOfSymbolicLink(atPath: srcPath)
try createSymbolicLink(atPath: dstPath, withDestinationPath: destination)
} else if fileType == .typeRegular {
if createFile(atPath: dstPath, contents: contents(atPath: srcPath), attributes: nil) == false {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but a proper copy engine will read then write a limited portion of the source file at a time. For large files, this will result in an extreme high-water mark for memory usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this was how the previous version was doing it but I will create a PR to fix it.

throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileWriteUnknown.rawValue, userInfo: [NSFilePathErrorKey : NSString(string: dstPath)])
}
}
}

if fileType == .typeDirectory {
try createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil)
let subpaths = try subpathsOfDirectory(atPath: srcPath)
for subpath in subpaths {
try copyItem(atPath: srcPath + "/" + subpath, toPath: dstPath + "/" + subpath)
let src = srcPath + "/" + subpath
let dst = dstPath + "/" + subpath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, I don't recall—Do we not have an equivalent of -stringByAppendingPathComponent: in SCL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only on NSString it appears, maybe a helper on String could be added in NSPathUtilities.

if let attrs = try? attributesOfItem(atPath: src), let fileType = attrs[.type] as? FileAttributeType {
if fileType == .typeDirectory {
try createDirectory(atPath: dst, withIntermediateDirectories: false, attributes: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong. No recursion at all means we only go one extra level deep, no? (There should be a test for this.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original implementation was wrong because it was recursing in every directory and then additional recursing in every sub directory. It now uses subpathsOfDirectory(atPath:) at the top level to get all of the subitems. There is are tests for it at https://github.com/apple/swift-corelibs-foundation/pull/1515/files/3c454b7358312d8d10dcb68a22d577e9ec816b58#diff-fffbc0319b30adac9f635327a750b82fR555

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. I misread this as contentsOfDirectory.

Please note that subpathsOfDirectory is very expensive, as it does all the directory enumeration at once and potentially creates a VERY large array. This is a high-water mark issue, similar to the contentsAtPath one. Using an enumerator would be much preferable here, and should be a fairly simple drop-in replacement.

} else {
try copyNonDirectory(srcPath: src, dstPath: dst, fileType: fileType)
}
}
}
} else {
if createFile(atPath: dstPath, contents: contents(atPath: srcPath), attributes: nil) == false {
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileWriteUnknown.rawValue, userInfo: [NSFilePathErrorKey : NSString(string: dstPath)])
}
try copyNonDirectory(srcPath: srcPath, dstPath: dstPath, fileType: fileType)
}
}

Expand Down
33 changes: 29 additions & 4 deletions TestFoundation/TestFileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ class TestFileManager : XCTestCase {
let fm = FileManager.default
let srcPath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)"
let destPath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)"

func cleanup() {
ignoreError { try fm.removeItem(atPath: srcPath) }
ignoreError { try fm.removeItem(atPath: destPath) }
Expand Down Expand Up @@ -552,8 +552,13 @@ class TestFileManager : XCTestCase {
cleanup()
createDirectory(atPath: srcPath)
createDirectory(atPath: "\(srcPath)/tempdir")
createDirectory(atPath: "\(srcPath)/tempdir/subdir")
createDirectory(atPath: "\(srcPath)/tempdir/subdir/otherdir")
createDirectory(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir")
createFile(atPath: "\(srcPath)/tempdir/tempfile")
createFile(atPath: "\(srcPath)/tempdir/tempfile2")
createFile(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir/tempfile2")

do {
try fm.copyItem(atPath: srcPath, toPath: destPath)
} catch let error {
Expand All @@ -563,16 +568,36 @@ class TestFileManager : XCTestCase {
XCTAssertTrue(directoryExists(atPath: "\(destPath)/tempdir"))
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/tempfile"))
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/tempfile2"))

XCTAssertTrue(directoryExists(atPath: "\(destPath)/tempdir/subdir/otherdir/extradir"))
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/subdir/otherdir/extradir/tempfile2"))

if (false == directoryExists(atPath: destPath)) {
return
}
do {
try fm.copyItem(atPath: srcPath, toPath: destPath)
XCTFail("Copy overwrites a file/folder that already exists")
} catch {
return
// ignore
}

// Test copying a symlink
let srcLink = srcPath + "/testlink"
let destLink = destPath + "/testlink"
do {
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
try fm.copyItem(atPath: srcLink, toPath: destLink)
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
} catch {
XCTFail("\(error)")
}

do {
try fm.copyItem(atPath: srcLink, toPath: destLink)
XCTFail("Creating link where one already exists")
} catch {
// ignore
}
XCTFail("Copy overwrites a file/folder that already exists")
}

func test_homedirectoryForUser() {
Expand Down