Skip to content

Commit 3c454b7

Browse files
committed
FileManager: Fix copyItem(atPath:toPath:) for directories and symlinks.
- When copying directories dont recurse into copyItems() just iterate through the results of subpathsOfDirectory(). - Copy the contents of the symlink not the file that it points to, creating a new symlink.
1 parent 90acdc7 commit 3c454b7

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

Foundation/FileManager.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,16 +442,34 @@ open class FileManager : NSObject {
442442
else {
443443
return
444444
}
445+
446+
func copyNonDirectory(srcPath: String, dstPath: String, fileType: FileAttributeType) throws {
447+
if fileType == .typeSymbolicLink {
448+
let destination = try destinationOfSymbolicLink(atPath: srcPath)
449+
try createSymbolicLink(atPath: dstPath, withDestinationPath: destination)
450+
} else if fileType == .typeRegular {
451+
if createFile(atPath: dstPath, contents: contents(atPath: srcPath), attributes: nil) == false {
452+
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileWriteUnknown.rawValue, userInfo: [NSFilePathErrorKey : NSString(string: dstPath)])
453+
}
454+
}
455+
}
456+
445457
if fileType == .typeDirectory {
446458
try createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil)
447459
let subpaths = try subpathsOfDirectory(atPath: srcPath)
448460
for subpath in subpaths {
449-
try copyItem(atPath: srcPath + "/" + subpath, toPath: dstPath + "/" + subpath)
461+
let src = srcPath + "/" + subpath
462+
let dst = dstPath + "/" + subpath
463+
if let attrs = try? attributesOfItem(atPath: src), let fileType = attrs[.type] as? FileAttributeType {
464+
if fileType == .typeDirectory {
465+
try createDirectory(atPath: dst, withIntermediateDirectories: false, attributes: nil)
466+
} else {
467+
try copyNonDirectory(srcPath: src, dstPath: dst, fileType: fileType)
468+
}
469+
}
450470
}
451471
} else {
452-
if createFile(atPath: dstPath, contents: contents(atPath: srcPath), attributes: nil) == false {
453-
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileWriteUnknown.rawValue, userInfo: [NSFilePathErrorKey : NSString(string: dstPath)])
454-
}
472+
try copyNonDirectory(srcPath: srcPath, dstPath: dstPath, fileType: fileType)
455473
}
456474
}
457475

TestFoundation/TestFileManager.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ class TestFileManager : XCTestCase {
516516
let fm = FileManager.default
517517
let srcPath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)"
518518
let destPath = NSTemporaryDirectory() + "testdir\(NSUUID().uuidString)"
519-
519+
520520
func cleanup() {
521521
ignoreError { try fm.removeItem(atPath: srcPath) }
522522
ignoreError { try fm.removeItem(atPath: destPath) }
@@ -552,8 +552,13 @@ class TestFileManager : XCTestCase {
552552
cleanup()
553553
createDirectory(atPath: srcPath)
554554
createDirectory(atPath: "\(srcPath)/tempdir")
555+
createDirectory(atPath: "\(srcPath)/tempdir/subdir")
556+
createDirectory(atPath: "\(srcPath)/tempdir/subdir/otherdir")
557+
createDirectory(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir")
555558
createFile(atPath: "\(srcPath)/tempdir/tempfile")
556559
createFile(atPath: "\(srcPath)/tempdir/tempfile2")
560+
createFile(atPath: "\(srcPath)/tempdir/subdir/otherdir/extradir/tempfile2")
561+
557562
do {
558563
try fm.copyItem(atPath: srcPath, toPath: destPath)
559564
} catch let error {
@@ -563,16 +568,36 @@ class TestFileManager : XCTestCase {
563568
XCTAssertTrue(directoryExists(atPath: "\(destPath)/tempdir"))
564569
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/tempfile"))
565570
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/tempfile2"))
566-
571+
XCTAssertTrue(directoryExists(atPath: "\(destPath)/tempdir/subdir/otherdir/extradir"))
572+
XCTAssertTrue(fm.fileExists(atPath: "\(destPath)/tempdir/subdir/otherdir/extradir/tempfile2"))
573+
567574
if (false == directoryExists(atPath: destPath)) {
568575
return
569576
}
570577
do {
571578
try fm.copyItem(atPath: srcPath, toPath: destPath)
579+
XCTFail("Copy overwrites a file/folder that already exists")
572580
} catch {
573-
return
581+
// ignore
582+
}
583+
584+
// Test copying a symlink
585+
let srcLink = srcPath + "/testlink"
586+
let destLink = destPath + "/testlink"
587+
do {
588+
try fm.createSymbolicLink(atPath: srcLink, withDestinationPath: "linkdest")
589+
try fm.copyItem(atPath: srcLink, toPath: destLink)
590+
XCTAssertEqual(try fm.destinationOfSymbolicLink(atPath: destLink), "linkdest")
591+
} catch {
592+
XCTFail("\(error)")
593+
}
594+
595+
do {
596+
try fm.copyItem(atPath: srcLink, toPath: destLink)
597+
XCTFail("Creating link where one already exists")
598+
} catch {
599+
// ignore
574600
}
575-
XCTFail("Copy overwrites a file/folder that already exists")
576601
}
577602

578603
func test_homedirectoryForUser() {

0 commit comments

Comments
 (0)